From a3b79a2701b352cf97a76a4d29ebe8da54c7d76e Mon Sep 17 00:00:00 2001 From: Rebecca Mahany-Horton Date: Fri, 12 May 2023 14:34:43 -0400 Subject: [PATCH] [TUF autoupdater] Remove osquery client dependency (#1178) --- pkg/autoupdate/tuf/autoupdate.go | 111 +++++-- pkg/autoupdate/tuf/autoupdate_test.go | 81 +++++- pkg/autoupdate/tuf/library_manager.go | 195 +++++-------- pkg/autoupdate/tuf/library_manager_test.go | 323 ++++++++++----------- pkg/autoupdate/tuf/mock_librarian_test.go | 16 +- pkg/autoupdate/tuf/mockery.md | 18 ++ 6 files changed, 419 insertions(+), 325 deletions(-) create mode 100644 pkg/autoupdate/tuf/mockery.md diff --git a/pkg/autoupdate/tuf/autoupdate.go b/pkg/autoupdate/tuf/autoupdate.go index eaadd455b..a08ebc2c8 100644 --- a/pkg/autoupdate/tuf/autoupdate.go +++ b/pkg/autoupdate/tuf/autoupdate.go @@ -6,6 +6,7 @@ package tuf import ( _ "embed" "encoding/json" + "errors" "fmt" "net/http" "os" @@ -16,6 +17,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/kit/version" "github.com/kolide/launcher/pkg/agent/types" client "github.com/theupdateframework/go-tuf/client" filejsonstore "github.com/theupdateframework/go-tuf/client/filejsonstore" @@ -47,18 +49,24 @@ type ReleaseFileCustomMetadata struct { type librarian interface { Available(binary autoupdatableBinary, targetFilename string) bool - AddToLibrary(binary autoupdatableBinary, targetFilename string, targetMetadata data.TargetFileMeta) error - TidyLibrary() + AddToLibrary(binary autoupdatableBinary, currentVersion string, targetFilename string, targetMetadata data.TargetFileMeta) error + TidyLibrary(binary autoupdatableBinary, currentVersion string) +} + +type querier interface { + Query(query string) ([]map[string]string, error) } type TufAutoupdater struct { - metadataClient *client.Client - libraryManager librarian - channel string - checkInterval time.Duration - store types.KVStore // stores autoupdater errors for kolide_tuf_autoupdater_errors table - interrupt chan struct{} - logger log.Logger + metadataClient *client.Client + libraryManager librarian + osquerier querier // used to query for current running osquery version + osquerierRetryInterval time.Duration + channel string + checkInterval time.Duration + store types.KVStore // stores autoupdater errors for kolide_tuf_autoupdater_errors table + interrupt chan struct{} + logger log.Logger } type TufAutoupdaterOption func(*TufAutoupdater) @@ -69,14 +77,16 @@ func WithLogger(logger log.Logger) TufAutoupdaterOption { } } -func NewTufAutoupdater(k types.Knapsack, metadataHttpClient *http.Client, - mirrorHttpClient *http.Client, osquerier querier, opts ...TufAutoupdaterOption) (*TufAutoupdater, error) { +func NewTufAutoupdater(k types.Knapsack, metadataHttpClient *http.Client, mirrorHttpClient *http.Client, + osquerier querier, opts ...TufAutoupdaterOption) (*TufAutoupdater, error) { ta := &TufAutoupdater{ - channel: k.UpdateChannel(), - interrupt: make(chan struct{}), - checkInterval: k.AutoupdateInterval(), - store: k.AutoupdateErrorsStore(), - logger: log.NewNopLogger(), + channel: k.UpdateChannel(), + interrupt: make(chan struct{}), + checkInterval: k.AutoupdateInterval(), + store: k.AutoupdateErrorsStore(), + osquerier: osquerier, + osquerierRetryInterval: 1 * time.Minute, + logger: log.NewNopLogger(), } for _, opt := range opts { @@ -92,9 +102,9 @@ func NewTufAutoupdater(k types.Knapsack, metadataHttpClient *http.Client, // If the update directory wasn't set by a flag, use the default location of /updates. updateDirectory := k.UpdateDirectory() if updateDirectory == "" { - updateDirectory = filepath.Join(k.RootDirectory(), "updates") + updateDirectory = DefaultLibraryDirectory(k.RootDirectory()) } - ta.libraryManager, err = newUpdateLibraryManager(k.MirrorServerURL(), mirrorHttpClient, updateDirectory, osquerier, ta.logger) + ta.libraryManager, err = newUpdateLibraryManager(k.MirrorServerURL(), mirrorHttpClient, updateDirectory, ta.logger) if err != nil { return nil, fmt.Errorf("could not init update library manager: %w", err) } @@ -138,13 +148,17 @@ func LocalTufDirectory(rootDirectory string) string { return filepath.Join(rootDirectory, tufDirectoryName) } +func DefaultLibraryDirectory(rootDirectory string) string { + return filepath.Join(rootDirectory, "updates") +} + // Execute is the TufAutoupdater run loop. It periodically checks to see if a new release // has been published; less frequently, it removes old/outdated TUF errors from the bucket // we store them in. func (ta *TufAutoupdater) Execute() (err error) { // For now, tidy the library on startup. In the future, we will tidy the library // earlier, after version selection. - ta.libraryManager.TidyLibrary() + ta.tidyLibrary() checkTicker := time.NewTicker(ta.checkInterval) defer checkTicker.Stop() @@ -171,6 +185,54 @@ func (ta *TufAutoupdater) Interrupt(_ error) { ta.interrupt <- struct{}{} } +// tidyLibrary gets the current running version for each binary (so that the current version is not removed) +// and then asks the update library manager to tidy the update library. +func (ta *TufAutoupdater) tidyLibrary() { + for _, binary := range binaries { + // Get the current running version to preserve it when tidying the available updates + currentVersion, err := ta.currentRunningVersion(binary) + if err != nil { + level.Debug(ta.logger).Log("msg", "could not get current running version", "binary", binary, "err", err) + continue + } + + ta.libraryManager.TidyLibrary(binary, currentVersion) + } +} + +// currentRunningVersion returns the current running version of the given binary. +// It will perform retries for osqueryd. +func (ta *TufAutoupdater) currentRunningVersion(binary autoupdatableBinary) (string, error) { + switch binary { + case binaryLauncher: + launcherVersion := version.Version().Version + if launcherVersion == "unknown" { + return "", errors.New("unknown launcher version") + } + return launcherVersion, nil + case binaryOsqueryd: + // The osqueryd client may not have initialized yet, so retry the version + // check a couple times before giving up + osquerydVersionCheckRetries := 5 + var err error + for i := 0; i < osquerydVersionCheckRetries; i += 1 { + var resp []map[string]string + resp, err = ta.osquerier.Query("SELECT version FROM osquery_info;") + if err == nil && len(resp) > 0 { + if osquerydVersion, ok := resp[0]["version"]; ok { + return osquerydVersion, nil + } + } + err = fmt.Errorf("error querying for osquery_info: %w; rows returned: %d", err, len(resp)) + + time.Sleep(ta.osquerierRetryInterval) + } + return "", err + default: + return "", fmt.Errorf("cannot determine current running version for unexpected binary %s", binary) + } +} + // checkForUpdate fetches latest metadata from the TUF server, then checks to see if there's // a new release that we should download. If so, it will add the release to our updates library. func (ta *TufAutoupdater) checkForUpdate() error { @@ -239,11 +301,20 @@ func (ta *TufAutoupdater) downloadUpdate(binary autoupdatableBinary, targets dat return "", fmt.Errorf("could not find release: %w", err) } + // Get the current running version if available -- don't error out if we can't + // get it, since the worst case is that we download an update whose version matches + // our install version. + var currentVersion string + currentVersion, _ = ta.currentRunningVersion(binary) + if currentVersion == versionFromTarget(binary, release) { + return "", nil + } + if ta.libraryManager.Available(binary, release) { return "", nil } - if err := ta.libraryManager.AddToLibrary(binary, release, releaseMetadata); err != nil { + if err := ta.libraryManager.AddToLibrary(binary, currentVersion, release, releaseMetadata); err != nil { return "", fmt.Errorf("could not add release %s for binary %s to library: %w", release, binary, err) } diff --git a/pkg/autoupdate/tuf/autoupdate_test.go b/pkg/autoupdate/tuf/autoupdate_test.go index ab279580b..f13226652 100644 --- a/pkg/autoupdate/tuf/autoupdate_test.go +++ b/pkg/autoupdate/tuf/autoupdate_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/Masterminds/semver" "github.com/go-kit/kit/log" "github.com/kolide/launcher/pkg/agent/storage" storageci "github.com/kolide/launcher/pkg/agent/storage/ci" @@ -20,6 +21,7 @@ import ( typesmocks "github.com/kolide/launcher/pkg/agent/types/mocks" tufci "github.com/kolide/launcher/pkg/autoupdate/tuf/ci" "github.com/kolide/launcher/pkg/threadsafebuffer" + mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -73,9 +75,10 @@ func TestExecute(t *testing.T) { mockKnapsack.On("TufServerURL").Return(tufServerUrl) mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockQuerier := newMockQuerier(t) // Set up autoupdater - autoupdater, err := NewTufAutoupdater(mockKnapsack, http.DefaultClient, http.DefaultClient, newMockQuerier(t)) + autoupdater, err := NewTufAutoupdater(mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier) require.NoError(t, err, "could not initialize new TUF autoupdater") // Confirm we pulled all config items as expected @@ -99,14 +102,19 @@ func TestExecute(t *testing.T) { launcherMetadata, err := autoupdater.metadataClient.Target(fmt.Sprintf("%s/%s/%s-%s.tar.gz", binaryLauncher, runtime.GOOS, binaryLauncher, testReleaseVersion)) require.NoError(t, err, "could not get test metadata for launcher") - // Expect that we attempt to update the library + // Expect that we attempt to tidy the library first before running execute loop mockLibraryManager := NewMocklibrarian(t) autoupdater.libraryManager = mockLibraryManager - mockLibraryManager.On("TidyLibrary").Return().Once() + currentLauncherVersion := "" // cannot determine using version package in test + currentOsqueryVersion := "1.1.1" + mockQuerier.On("Query", mock.Anything).Return([]map[string]string{{"version": currentOsqueryVersion}}, nil) + mockLibraryManager.On("TidyLibrary", binaryOsqueryd, mock.Anything).Return().Once() + + // Expect that we attempt to update the library mockLibraryManager.On("Available", binaryOsqueryd, fmt.Sprintf("osqueryd-%s.tar.gz", testReleaseVersion)).Return(false) mockLibraryManager.On("Available", binaryLauncher, fmt.Sprintf("launcher-%s.tar.gz", testReleaseVersion)).Return(false) - mockLibraryManager.On("AddToLibrary", binaryOsqueryd, fmt.Sprintf("osqueryd-%s.tar.gz", testReleaseVersion), osquerydMetadata).Return(nil) - mockLibraryManager.On("AddToLibrary", binaryLauncher, fmt.Sprintf("launcher-%s.tar.gz", testReleaseVersion), launcherMetadata).Return(nil) + mockLibraryManager.On("AddToLibrary", binaryOsqueryd, currentOsqueryVersion, fmt.Sprintf("osqueryd-%s.tar.gz", testReleaseVersion), osquerydMetadata).Return(nil) + mockLibraryManager.On("AddToLibrary", binaryLauncher, currentLauncherVersion, fmt.Sprintf("launcher-%s.tar.gz", testReleaseVersion), launcherMetadata).Return(nil) // Let the autoupdater run for a bit go autoupdater.Execute() @@ -132,6 +140,59 @@ func TestExecute(t *testing.T) { require.Contains(t, logLines[len(logLines)-1], "received interrupt, stopping") } +func Test_currentRunningVersion_launcher_errorWhenVersionIsNotSet(t *testing.T) { + t.Parallel() + + mockQuerier := newMockQuerier(t) + autoupdater := &TufAutoupdater{ + logger: log.NewNopLogger(), + osquerier: mockQuerier, + } + + // In test, version.Version() returns `unknown` for everything, which is not something + // that the semver library can parse. So we only expect an error here. + launcherVersion, err := autoupdater.currentRunningVersion("launcher") + require.Error(t, err, "expected an error fetching current running version of launcher") + require.Equal(t, "", launcherVersion) +} + +func Test_currentRunningVersion_osqueryd(t *testing.T) { + t.Parallel() + + mockQuerier := newMockQuerier(t) + autoupdater := &TufAutoupdater{ + logger: log.NewNopLogger(), + osquerier: mockQuerier, + } + + // Expect to return one row containing the version + expectedOsqueryVersion, err := semver.NewVersion("5.10.12") + require.NoError(t, err) + mockQuerier.On("Query", mock.Anything).Return([]map[string]string{{"version": expectedOsqueryVersion.Original()}}, nil).Once() + + osqueryVersion, err := autoupdater.currentRunningVersion("osqueryd") + require.NoError(t, err, "expected no error fetching current running version of osqueryd") + require.Equal(t, expectedOsqueryVersion.Original(), osqueryVersion) +} + +func Test_currentRunningVersion_osqueryd_handlesQueryError(t *testing.T) { + t.Parallel() + + mockQuerier := newMockQuerier(t) + autoupdater := &TufAutoupdater{ + logger: log.NewNopLogger(), + osquerier: mockQuerier, + osquerierRetryInterval: 1 * time.Millisecond, + } + + // Expect to return an error (five times, since we perform retries) + mockQuerier.On("Query", mock.Anything).Return(make([]map[string]string, 0), errors.New("test osqueryd querying error")) + + osqueryVersion, err := autoupdater.currentRunningVersion("osqueryd") + require.Error(t, err, "expected an error returning osquery version when querying osquery fails") + require.Equal(t, "", osqueryVersion) +} + func Test_storeError(t *testing.T) { t.Parallel() @@ -149,8 +210,9 @@ func Test_storeError(t *testing.T) { mockKnapsack.On("TufServerURL").Return(testTufServer.URL) mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockQuerier := newMockQuerier(t) - autoupdater, err := NewTufAutoupdater(mockKnapsack, http.DefaultClient, http.DefaultClient, newMockQuerier(t)) + autoupdater, err := NewTufAutoupdater(mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier) require.NoError(t, err, "could not initialize new TUF autoupdater") // Confirm we pulled all config items as expected @@ -158,7 +220,11 @@ func Test_storeError(t *testing.T) { mockLibraryManager := NewMocklibrarian(t) autoupdater.libraryManager = mockLibraryManager - mockLibraryManager.On("TidyLibrary").Return().Once() + mockQuerier.On("Query", mock.Anything).Return([]map[string]string{{"version": "1.1.1"}}, nil).Once() + + // We only expect TidyLibrary to run for osqueryd, since we can't get the current running version + // for launcher in tests. + mockLibraryManager.On("TidyLibrary", binaryOsqueryd, mock.Anything).Return().Once() // Set the check interval to something short so we can accumulate some errors autoupdater.checkInterval = 1 * time.Second @@ -187,6 +253,7 @@ func Test_storeError(t *testing.T) { require.Greater(t, errorCount, 0, "TUF autoupdater did not record error counts") mockLibraryManager.AssertExpectations(t) + mockQuerier.AssertExpectations(t) } func Test_cleanUpOldErrors(t *testing.T) { diff --git a/pkg/autoupdate/tuf/library_manager.go b/pkg/autoupdate/tuf/library_manager.go index bcee5920f..35c4f4a92 100644 --- a/pkg/autoupdate/tuf/library_manager.go +++ b/pkg/autoupdate/tuf/library_manager.go @@ -3,7 +3,6 @@ package tuf import ( "bytes" "context" - "errors" "fmt" "io" "net/http" @@ -12,23 +11,17 @@ import ( "runtime" "sort" "strings" - "time" "github.com/Masterminds/semver" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/kit/fsutil" - "github.com/kolide/kit/version" "github.com/kolide/launcher/pkg/agent" "github.com/kolide/launcher/pkg/autoupdate" "github.com/theupdateframework/go-tuf/data" tufutil "github.com/theupdateframework/go-tuf/util" ) -type querier interface { - Query(query string) ([]map[string]string, error) -} - // updateLibraryManager manages the update libraries for launcher and osquery. // It downloads and verifies new updates, and moves them to the appropriate // location in the library specified by the version associated with that update. @@ -38,17 +31,15 @@ type updateLibraryManager struct { mirrorClient *http.Client baseDir string stagingDir string - osquerier querier // used to query for current running osquery version lock *libraryLock logger log.Logger } -func newUpdateLibraryManager(mirrorUrl string, mirrorClient *http.Client, baseDir string, osquerier querier, logger log.Logger) (*updateLibraryManager, error) { +func newUpdateLibraryManager(mirrorUrl string, mirrorClient *http.Client, baseDir string, logger log.Logger) (*updateLibraryManager, error) { ulm := updateLibraryManager{ mirrorUrl: mirrorUrl, mirrorClient: mirrorClient, baseDir: baseDir, - osquerier: osquerier, lock: newLibraryLock(), logger: log.With(logger, "component", "tuf_autoupdater_library_manager"), } @@ -80,37 +71,29 @@ func (ulm *updateLibraryManager) updatesDirectory(binary autoupdatableBinary) st return filepath.Join(ulm.baseDir, string(binary)) } -// Available determines if the given target is already available, either as the currently-running -// binary or within the update library. +// Available determines if the given target is already available in the update library. func (ulm *updateLibraryManager) Available(binary autoupdatableBinary, targetFilename string) bool { - // Check to see if the current running version is the version we were requested to add; - // return early if it is, but don't error out if we can't determine the current version. - currentVersion, err := ulm.currentRunningVersion(binary) - if err != nil { - level.Debug(ulm.logger).Log("msg", "could not get current running version", "binary", binary, "err", err) - } else if currentVersion == ulm.versionFromTarget(binary, targetFilename) { - // We don't need to download the current running version because it already exists, - // either in this updates library or in the original install location. - return true - } - - return ulm.alreadyAdded(binary, targetFilename) + executablePath := ulm.PathToTargetVersionExecutable(binary, targetFilename) + return autoupdate.CheckExecutable(context.TODO(), executablePath, "--version") == nil } -// alreadyAdded checks if the given target already exists in the update library. -func (ulm *updateLibraryManager) alreadyAdded(binary autoupdatableBinary, targetFilename string) bool { - updateDirectory := filepath.Join(ulm.updatesDirectory(binary), ulm.versionFromTarget(binary, targetFilename)) - - return autoupdate.CheckExecutable(context.TODO(), executableLocation(updateDirectory, binary), "--version") == nil +// PathToTargetVersionExecutable returns the path to the executable for the desired version. +func (ulm *updateLibraryManager) PathToTargetVersionExecutable(binary autoupdatableBinary, targetFilename string) string { + versionDir := filepath.Join(ulm.updatesDirectory(binary), versionFromTarget(binary, targetFilename)) + return executableLocation(versionDir, binary) } // AddToLibrary adds the given target file to the library for the given binary, // downloading and verifying it if it's not already there. -func (ulm *updateLibraryManager) AddToLibrary(binary autoupdatableBinary, targetFilename string, targetMetadata data.TargetFileMeta) error { +func (ulm *updateLibraryManager) AddToLibrary(binary autoupdatableBinary, currentVersion string, targetFilename string, targetMetadata data.TargetFileMeta) error { // Acquire lock for modifying the library ulm.lock.Lock(binary) defer ulm.lock.Unlock(binary) + if currentVersion == versionFromTarget(binary, targetFilename) { + return nil + } + if ulm.Available(binary, targetFilename) { return nil } @@ -130,14 +113,6 @@ func (ulm *updateLibraryManager) AddToLibrary(binary autoupdatableBinary, target return nil } -// versionFromTarget extracts the semantic version for an update from its filename. -func (ulm *updateLibraryManager) versionFromTarget(binary autoupdatableBinary, targetFilename string) string { - // The target is in the form `launcher-0.13.6.tar.gz` -- trim the prefix and the file extension to return the version - prefixToTrim := fmt.Sprintf("%s-", binary) - - return strings.TrimSuffix(strings.TrimPrefix(targetFilename, prefixToTrim), ".tar.gz") -} - // stageAndVerifyUpdate downloads the update indicated by `targetFilename` and verifies it against // the given, validated local metadata. func (ulm *updateLibraryManager) stageAndVerifyUpdate(binary autoupdatableBinary, targetFilename string, localTargetMetadata data.TargetFileMeta) (string, error) { @@ -184,7 +159,7 @@ func (ulm *updateLibraryManager) stageAndVerifyUpdate(binary autoupdatableBinary // moveVerifiedUpdate untars the update and performs final checks to make sure that it's a valid, working update. // Finally, it moves the update to its correct versioned location in the update library for the given binary. func (ulm *updateLibraryManager) moveVerifiedUpdate(binary autoupdatableBinary, targetFilename string, stagedUpdate string) error { - targetVersion := ulm.versionFromTarget(binary, targetFilename) + targetVersion := versionFromTarget(binary, targetFilename) stagedVersionedDirectory := filepath.Join(ulm.stagingDir, targetVersion) if err := os.MkdirAll(stagedVersionedDirectory, 0755); err != nil { return fmt.Errorf("could not create directory %s for untarring and validating new update: %w", stagedVersionedDirectory, err) @@ -228,70 +203,17 @@ func (ulm *updateLibraryManager) removeUpdate(binary autoupdatableBinary, binary } } -// currentRunningVersion returns the current running version of the given binary. -func (ulm *updateLibraryManager) currentRunningVersion(binary autoupdatableBinary) (string, error) { - switch binary { - case binaryLauncher: - launcherVersion := version.Version().Version - if launcherVersion == "unknown" { - return "", errors.New("unknown launcher version") - } - return launcherVersion, nil - case binaryOsqueryd: - resp, err := ulm.osquerier.Query("SELECT version FROM osquery_info;") - if err != nil { - return "", fmt.Errorf("could not query for osquery version: %w", err) - } - if len(resp) < 1 { - return "", errors.New("osquery version query returned no rows") - } - osquerydVersion, ok := resp[0]["version"] - if !ok { - return "", errors.New("osquery version query did not return version") - } - - return osquerydVersion, nil - default: - return "", fmt.Errorf("cannot determine current running version for unexpected binary %s", binary) - } -} - // TidyLibrary removes unneeded files from the staged updates and updates directories. -func (ulm *updateLibraryManager) TidyLibrary() { - for _, binary := range binaries { - // Acquire lock for modifying the library - ulm.lock.Lock(binary) - defer ulm.lock.Unlock(binary) - - // First, remove old staged archives - ulm.tidyStagedUpdates(binary) - - // Get the current running version to preserve it when tidying the available updates - var currentVersion string - var err error - switch binary { - case binaryOsqueryd: - // The osqueryd client may not have initialized yet, so retry the version - // check a couple times before giving up - osquerydVersionCheckRetries := 5 - for i := 0; i < osquerydVersionCheckRetries; i += 1 { - currentVersion, err = ulm.currentRunningVersion(binary) - if err == nil { - break - } - time.Sleep(1 * time.Minute) - } - default: - currentVersion, err = ulm.currentRunningVersion(binary) - } +func (ulm *updateLibraryManager) TidyLibrary(binary autoupdatableBinary, currentVersion string) { + // Acquire lock for modifying the library + ulm.lock.Lock(binary) + defer ulm.lock.Unlock(binary) - if err != nil { - level.Debug(ulm.logger).Log("msg", "could not get current running version", "binary", binary, "err", err) - continue - } + // First, remove old staged archives + ulm.tidyStagedUpdates(binary) - ulm.tidyUpdateLibrary(binary, currentVersion) - } + // Remove any updates we no longer need + ulm.tidyUpdateLibrary(binary, currentVersion) } // tidyStagedUpdates removes all old archives from the staged updates directory. @@ -320,53 +242,84 @@ func (ulm *updateLibraryManager) tidyUpdateLibrary(binary autoupdatableBinary, c const numberOfVersionsToKeep = 3 - rawVersionsInLibrary, err := filepath.Glob(filepath.Join(ulm.updatesDirectory(binary), "*")) + versionsInLibrary, invalidVersionsInLibrary, err := ulm.sortedVersionsInLibrary(binary) if err != nil { - level.Debug(ulm.logger).Log("msg", "could not glob for updates to tidy updates library", "err", err) + level.Debug(ulm.logger).Log("msg", "could not get versions in library to tidy update library", "err", err) return } - versionsInLibrary := make([]*semver.Version, 0) - for _, rawVersion := range rawVersionsInLibrary { - v, err := semver.NewVersion(filepath.Base(rawVersion)) - if err != nil { - level.Debug(ulm.logger).Log("msg", "updates library contains invalid semver", "err", err, "library_path", rawVersion) - ulm.removeUpdate(binary, filepath.Base(rawVersion)) - continue - } - - versionsInLibrary = append(versionsInLibrary, v) + for _, invalidVersion := range invalidVersionsInLibrary { + level.Debug(ulm.logger).Log("msg", "updates library contains invalid version", "err", err, "library_path", invalidVersion) + ulm.removeUpdate(binary, invalidVersion) } if len(versionsInLibrary) <= numberOfVersionsToKeep { return } - // Sort the versions (ascending order) - sort.Sort(semver.Collection(versionsInLibrary)) - // Loop through, looking at the most recent versions first, and remove all once we hit nonCurrentlyRunningVersionsKept valid executables nonCurrentlyRunningVersionsKept := 0 for i := len(versionsInLibrary) - 1; i >= 0; i -= 1 { // Always keep the current running executable - if versionsInLibrary[i].Original() == currentRunningVersion { + if versionsInLibrary[i] == currentRunningVersion { continue } // If we've already hit the number of versions to keep, then start to remove the older ones. // We want to keep numberOfVersionsToKeep total, saving a spot for the currently running version. if nonCurrentlyRunningVersionsKept >= numberOfVersionsToKeep-1 { - ulm.removeUpdate(binary, versionsInLibrary[i].Original()) + ulm.removeUpdate(binary, versionsInLibrary[i]) + continue + } + + nonCurrentlyRunningVersionsKept += 1 + } +} + +// sortedVersionsInLibrary looks through the update library for the given binary to validate and sort all +// available versions. It returns a sorted list of the valid versions, a list of invalid versions, and +// an error only when unable to glob for versions. +func (ulm *updateLibraryManager) sortedVersionsInLibrary(binary autoupdatableBinary) ([]string, []string, error) { + rawVersionsInLibrary, err := filepath.Glob(filepath.Join(ulm.updatesDirectory(binary), "*")) + if err != nil { + return nil, nil, fmt.Errorf("could not glob for updates in library: %w", err) + } + + versionsInLibrary := make([]*semver.Version, 0) + invalidVersions := make([]string, 0) + for _, rawVersionWithPath := range rawVersionsInLibrary { + rawVersion := filepath.Base(rawVersionWithPath) + v, err := semver.NewVersion(rawVersion) + if err != nil { + invalidVersions = append(invalidVersions, rawVersion) continue } - // Only keep good executables - versionDir := filepath.Join(ulm.updatesDirectory(binary), versionsInLibrary[i].Original()) + versionDir := filepath.Join(ulm.updatesDirectory(binary), rawVersion) if err := autoupdate.CheckExecutable(context.TODO(), executableLocation(versionDir, binary), "--version"); err != nil { - ulm.removeUpdate(binary, versionsInLibrary[i].Original()) + invalidVersions = append(invalidVersions, rawVersion) continue } - nonCurrentlyRunningVersionsKept += 1 + versionsInLibrary = append(versionsInLibrary, v) + } + + // Sort the versions (ascending order) + sort.Sort(semver.Collection(versionsInLibrary)) + + // Transform versions back into strings now that we've finished sorting them + versionsInLibraryStr := make([]string, len(versionsInLibrary)) + for i, v := range versionsInLibrary { + versionsInLibraryStr[i] = v.Original() } + + return versionsInLibraryStr, invalidVersions, nil +} + +// versionFromTarget extracts the semantic version for an update from its filename. +func versionFromTarget(binary autoupdatableBinary, targetFilename string) string { + // The target is in the form `launcher-0.13.6.tar.gz` -- trim the prefix and the file extension to return the version + prefixToTrim := fmt.Sprintf("%s-", binary) + + return strings.TrimSuffix(strings.TrimPrefix(targetFilename, prefixToTrim), ".tar.gz") } diff --git a/pkg/autoupdate/tuf/library_manager_test.go b/pkg/autoupdate/tuf/library_manager_test.go index ec99bac07..81c94e571 100644 --- a/pkg/autoupdate/tuf/library_manager_test.go +++ b/pkg/autoupdate/tuf/library_manager_test.go @@ -2,7 +2,6 @@ package tuf import ( "context" - "errors" "fmt" "net/http" "net/http/httptest" @@ -13,11 +12,9 @@ import ( "sync" "testing" - "github.com/Masterminds/semver" "github.com/go-kit/kit/log" "github.com/kolide/launcher/pkg/autoupdate" tufci "github.com/kolide/launcher/pkg/autoupdate/tuf/ci" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/theupdateframework/go-tuf/data" ) @@ -26,7 +23,7 @@ func Test_newUpdateLibraryManager(t *testing.T) { t.Parallel() testBaseDir := filepath.Join(t.TempDir(), "updates") - testLibraryManager, err := newUpdateLibraryManager("", nil, testBaseDir, nil, log.NewNopLogger()) + testLibraryManager, err := newUpdateLibraryManager("", nil, testBaseDir, log.NewNopLogger()) require.NoError(t, err, "unexpected error creating new update library manager") baseDir, err := os.Stat(testBaseDir) @@ -46,23 +43,48 @@ func Test_newUpdateLibraryManager(t *testing.T) { require.True(t, launcherDownloadDir.IsDir(), "launcher download dir is not a directory") } +func TestPathToTargetVersionExecutable(t *testing.T) { + t.Parallel() + + testBaseDir := filepath.Join(t.TempDir(), "updates") + testLibrary, err := newUpdateLibraryManager("", nil, testBaseDir, log.NewNopLogger()) + require.NoError(t, err, "expected no error when creating library") + + testVersion := "1.0.7-30-abcdabcd" + testTargetFilename := fmt.Sprintf("launcher-%s.tar.gz", testVersion) + expectedPath := filepath.Join(testBaseDir, "launcher", testVersion, "launcher") + if runtime.GOOS == "darwin" { + expectedPath = filepath.Join(testBaseDir, "launcher", testVersion, "Kolide.app", "Contents", "MacOS", "launcher") + } else if runtime.GOOS == "windows" { + expectedPath = expectedPath + ".exe" + } + + actualPath := testLibrary.PathToTargetVersionExecutable(binaryLauncher, testTargetFilename) + require.Equal(t, expectedPath, actualPath, "path mismatch") +} + func TestAvailable(t *testing.T) { t.Parallel() + // Create update directories testBaseDir := t.TempDir() - mockOsquerier := newMockQuerier(t) - testLibraryManager, err := newUpdateLibraryManager("", nil, testBaseDir, mockOsquerier, log.NewNopLogger()) - require.NoError(t, err, "unexpected error creating new update library manager") + // Set up test library + testLibrary, err := newUpdateLibraryManager("", nil, testBaseDir, log.NewNopLogger()) + require.NoError(t, err, "unexpected error creating new read-only library") - // Query for the current osquery version + // Set up valid "osquery" executable runningOsqueryVersion := "5.5.7" - mockOsquerier.On("Query", mock.Anything).Return([]map[string]string{{"version": runningOsqueryVersion}}, nil).Once() - require.True(t, testLibraryManager.Available(binaryOsqueryd, fmt.Sprintf("osqueryd-%s.tar.gz", runningOsqueryVersion))) + runningTarget := fmt.Sprintf("osqueryd-%s.tar.gz", runningOsqueryVersion) + executablePath := testLibrary.PathToTargetVersionExecutable(binaryOsqueryd, runningTarget) + tufci.CopyBinary(t, executablePath) + require.NoError(t, os.Chmod(executablePath, 0755)) + + // Query for the current osquery version + require.True(t, testLibrary.Available(binaryOsqueryd, runningTarget)) // Query for a different osqueryd version - mockOsquerier.On("Query", mock.Anything).Return([]map[string]string{{"version": runningOsqueryVersion}}, nil).Once() - require.False(t, testLibraryManager.Available(binaryOsqueryd, "osqueryd-5.6.7.tar.gz")) + require.False(t, testLibrary.Available(binaryOsqueryd, "osqueryd-5.6.7.tar.gz")) } func TestAddToLibrary(t *testing.T) { @@ -109,29 +131,21 @@ func TestAddToLibrary(t *testing.T) { t.Parallel() // Set up test library manager - mockOsquerier := newMockQuerier(t) - testLibraryManager, err := newUpdateLibraryManager(tufServerUrl, http.DefaultClient, testBaseDir, mockOsquerier, log.NewNopLogger()) + testLibraryManager, err := newUpdateLibraryManager(tufServerUrl, http.DefaultClient, testBaseDir, log.NewNopLogger()) require.NoError(t, err, "unexpected error creating new update library manager") - // For osqueryd, make sure we check that the running version is not equal to the target version - if tt.binary == "osqueryd" { - mockOsquerier.On("Query", mock.Anything).Return([]map[string]string{{"version": "5.5.5"}}, nil) - } - // Request download -- make a couple concurrent requests to confirm that the lock works. var wg sync.WaitGroup for i := 0; i < 5; i += 1 { wg.Add(1) go func() { defer wg.Done() - require.NoError(t, testLibraryManager.AddToLibrary(tt.binary, tt.targetFile, tt.targetMeta), "expected no error adding to library") + require.NoError(t, testLibraryManager.AddToLibrary(tt.binary, "", tt.targetFile, tt.targetMeta), "expected no error adding to library") }() } wg.Wait() - mockOsquerier.AssertExpectations(t) - // Confirm the update was downloaded dirInfo, err := os.Stat(filepath.Join(testLibraryManager.updatesDirectory(tt.binary), testReleaseVersion)) require.NoError(t, err, "checking that update was downloaded") @@ -148,42 +162,44 @@ func TestAddToLibrary(t *testing.T) { } } -func Test_addToLibrary_alreadyRunning_osqueryd(t *testing.T) { +func TestAddToLibrary_alreadyRunning(t *testing.T) { t.Parallel() - // We only do this particular test for osqueryd because in test, version.Version() - // returns `unknown` for everything when we attempt to get the current running version - // of launcher, which is not something that the semver library can parse. + for _, binary := range binaries { + binary := binary + t.Run(string(binary), func(t *testing.T) { + t.Parallel() - testBaseDir := t.TempDir() - mockOsquerier := newMockQuerier(t) - testLibraryManager, err := newUpdateLibraryManager("", nil, testBaseDir, mockOsquerier, log.NewNopLogger()) - require.NoError(t, err, "initializing test library manager") - - // Make sure our update directories exist so we can verify they're empty later - require.NoError(t, os.MkdirAll(testLibraryManager.updatesDirectory("osqueryd"), 0755)) - - // Set osquerier to return same version we want to add to the library - testVersion := "0.12.1-abcdabcd" - expectedOsqueryVersion, err := semver.NewVersion(testVersion) - require.NoError(t, err) - - // Expect to return one row containing the version - mockOsquerier.On("Query", mock.Anything).Return([]map[string]string{{"version": expectedOsqueryVersion.Original()}}, nil).Once() - - // Ask the library manager to perform the download - require.NoError(t, testLibraryManager.AddToLibrary("osqueryd", fmt.Sprintf("osqueryd-%s.tar.gz", testVersion), data.TargetFileMeta{}), "expected no error on adding already-running version to library") - mockOsquerier.AssertExpectations(t) - - // Confirm that there is nothing in the updates directory (no update performed) - updateMatches, err := filepath.Glob(filepath.Join(testLibraryManager.updatesDirectory("osqueryd"), "*")) - require.NoError(t, err, "error globbing for matches") - require.Equal(t, 0, len(updateMatches), "expected no directories in updates directory but found: %+v", updateMatches) - - // Confirm that there is nothing in the staged updates directory (no update attempted) - stagedUpdateMatches, err := filepath.Glob(filepath.Join(testLibraryManager.stagingDir, "*")) - require.NoError(t, err, "error globbing for matches") - require.Equal(t, 0, len(stagedUpdateMatches), "expected no directories in staged updates directory but found: %+v", stagedUpdateMatches) + testBaseDir := t.TempDir() + testMirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("mirror should not have been called for download, but was: %s", r.URL.String()) + })) + defer testMirror.Close() + testLibraryManager := &updateLibraryManager{ + mirrorUrl: testMirror.URL, + mirrorClient: http.DefaultClient, + logger: log.NewNopLogger(), + baseDir: testBaseDir, + stagingDir: t.TempDir(), + lock: newLibraryLock(), + } + + // Make sure our update directory exists + require.NoError(t, os.MkdirAll(testLibraryManager.updatesDirectory(binary), 0755)) + + // Set the current running version to the version we're going to request to download + currentRunningVersion := "4.3.2" + + // Ask the library manager to perform the download + targetFilename := fmt.Sprintf("%s-%s.tar.gz", binary, currentRunningVersion) + require.Equal(t, currentRunningVersion, versionFromTarget(binary, targetFilename), "incorrectly formed target filename") + require.NoError(t, testLibraryManager.AddToLibrary(binary, currentRunningVersion, targetFilename, data.TargetFileMeta{}), "expected no error on adding already-downloaded version to library") + + // Confirm the requested version was not downloaded + _, err := os.Stat(filepath.Join(testLibraryManager.updatesDirectory(binary), currentRunningVersion)) + require.True(t, os.IsNotExist(err), "should not have downloaded currently-running version") + }) + } } func TestAddToLibrary_alreadyAdded(t *testing.T) { @@ -195,7 +211,6 @@ func TestAddToLibrary_alreadyAdded(t *testing.T) { t.Parallel() testBaseDir := t.TempDir() - mockOsquerier := newMockQuerier(t) testMirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatalf("mirror should not have been called for download, but was: %s", r.URL.String()) })) @@ -206,7 +221,6 @@ func TestAddToLibrary_alreadyAdded(t *testing.T) { logger: log.NewNopLogger(), baseDir: testBaseDir, stagingDir: t.TempDir(), - osquerier: mockOsquerier, lock: newLibraryLock(), } @@ -222,16 +236,10 @@ func TestAddToLibrary_alreadyAdded(t *testing.T) { require.NoError(t, err, "did not create binary for test") require.NoError(t, autoupdate.CheckExecutable(context.TODO(), executablePath, "--version"), "binary created for test is corrupt") - // For osqueryd, make sure we check that the running version is not equal to the target version - if binary == "osqueryd" { - mockOsquerier.On("Query", mock.Anything).Return([]map[string]string{{"version": "5.5.5"}}, nil).Once() - } - // Ask the library manager to perform the download targetFilename := fmt.Sprintf("%s-%s.tar.gz", binary, testVersion) - require.Equal(t, testVersion, testLibraryManager.versionFromTarget(binary, targetFilename), "incorrectly formed target filename") - require.NoError(t, testLibraryManager.AddToLibrary(binary, targetFilename, data.TargetFileMeta{}), "expected no error on adding already-downloaded version to library") - mockOsquerier.AssertExpectations(t) + require.Equal(t, testVersion, versionFromTarget(binary, targetFilename), "incorrectly formed target filename") + require.NoError(t, testLibraryManager.AddToLibrary(binary, "", targetFilename, data.TargetFileMeta{}), "expected no error on adding already-downloaded version to library") // Confirm the requested version is still there _, err = os.Stat(executablePath) @@ -296,18 +304,11 @@ func TestAddToLibrary_verifyStagedUpdate_handlesInvalidFiles(t *testing.T) { defer testMaliciousMirror.Close() // Set up test library manager - mockOsquerier := newMockQuerier(t) - testLibraryManager, err := newUpdateLibraryManager(testMaliciousMirror.URL, http.DefaultClient, testBaseDir, mockOsquerier, log.NewNopLogger()) + testLibraryManager, err := newUpdateLibraryManager(testMaliciousMirror.URL, http.DefaultClient, testBaseDir, log.NewNopLogger()) require.NoError(t, err, "unexpected error creating new update library manager") - // For osqueryd, make sure we check that the running version is not equal to the target version - if tt.binary == "osqueryd" { - mockOsquerier.On("Query", mock.Anything).Return([]map[string]string{{"version": "5.5.5"}}, nil).Once() - } - // Request download - require.Error(t, testLibraryManager.AddToLibrary(tt.binary, tt.targetFile, tt.targetMeta), "expected error when library manager downloads invalid file") - mockOsquerier.AssertExpectations(t) + require.Error(t, testLibraryManager.AddToLibrary(tt.binary, "", tt.targetFile, tt.targetMeta), "expected error when library manager downloads invalid file") // Confirm the update was removed after download downloadMatches, err := filepath.Glob(filepath.Join(testLibraryManager.stagingDir, "*")) @@ -322,98 +323,7 @@ func TestAddToLibrary_verifyStagedUpdate_handlesInvalidFiles(t *testing.T) { } } -func Test_currentRunningVersion_launcher_errorWhenVersionIsNotSet(t *testing.T) { - t.Parallel() - - testLibraryManager := &updateLibraryManager{ - logger: log.NewNopLogger(), - stagingDir: t.TempDir(), - } - - // In test, version.Version() returns `unknown` for everything, which is not something - // that the semver library can parse. So we only expect an error here. - launcherVersion, err := testLibraryManager.currentRunningVersion("launcher") - require.Error(t, err, "expected an error fetching current running version of launcher") - require.Equal(t, "", launcherVersion) -} - -func Test_currentRunningVersion_osqueryd(t *testing.T) { - t.Parallel() - - mockOsquerier := newMockQuerier(t) - - testLibraryManager := &updateLibraryManager{ - logger: log.NewNopLogger(), - stagingDir: t.TempDir(), - osquerier: mockOsquerier, - } - - expectedOsqueryVersion, err := semver.NewVersion("5.10.12") - require.NoError(t, err) - - // Expect to return one row containing the version - mockOsquerier.On("Query", mock.Anything).Return([]map[string]string{{"version": expectedOsqueryVersion.Original()}}, nil).Once() - - osqueryVersion, err := testLibraryManager.currentRunningVersion("osqueryd") - require.NoError(t, err, "expected no error fetching current running version of osqueryd") - require.Equal(t, expectedOsqueryVersion.Original(), osqueryVersion) -} - -func Test_currentRunningVersion_osqueryd_handlesQueryError(t *testing.T) { - t.Parallel() - - mockOsquerier := newMockQuerier(t) - - testLibraryManager := &updateLibraryManager{ - logger: log.NewNopLogger(), - osquerier: mockOsquerier, - stagingDir: t.TempDir(), - } - - // Expect to return an error - mockOsquerier.On("Query", mock.Anything).Return(make([]map[string]string, 0), errors.New("test osqueryd querying error")).Once() - - osqueryVersion, err := testLibraryManager.currentRunningVersion("osqueryd") - require.Error(t, err, "expected an error returning osquery version when querying osquery fails") - require.Equal(t, "", osqueryVersion) -} - -func Test_tidyStagedUpdates(t *testing.T) { - t.Parallel() - - for _, binary := range binaries { - binary := binary - t.Run(string(binary), func(t *testing.T) { - t.Parallel() - - testBaseDir := t.TempDir() - - // Initialize the library manager - testLibraryManager, err := newUpdateLibraryManager("", nil, testBaseDir, nil, log.NewNopLogger()) - require.NoError(t, err, "unexpected error creating new update library manager") - - // Make a file in the staged updates directory - f1, err := os.Create(filepath.Join(testLibraryManager.stagingDir, fmt.Sprintf("%s-1.2.3.tar.gz", binary))) - require.NoError(t, err, "creating fake download file") - f1.Close() - - // Confirm we made the files - matches, err := filepath.Glob(filepath.Join(testLibraryManager.stagingDir, "*")) - require.NoError(t, err, "could not glob for files in staged osqueryd download dir") - require.Equal(t, 1, len(matches)) - - // Tidy up staged updates and confirm they're removed after - testLibraryManager.tidyStagedUpdates(binary) - _, err = os.Stat(testLibraryManager.stagingDir) - require.NoError(t, err, "could not stat staged download dir") - matchesAfter, err := filepath.Glob(filepath.Join(testLibraryManager.stagingDir, "*")) - require.NoError(t, err, "could not glob for files in staged download dir") - require.Equal(t, 0, len(matchesAfter)) - }) - } -} - -func Test_tidyUpdateLibrary(t *testing.T) { +func TestTidyLibrary(t *testing.T) { t.Parallel() testCases := []struct { @@ -576,6 +486,16 @@ func Test_tidyUpdateLibrary(t *testing.T) { lock: newLibraryLock(), } + // Make a file in the staged updates directory + f1, err := os.Create(filepath.Join(testLibraryManager.stagingDir, fmt.Sprintf("%s-1.2.3.tar.gz", binary))) + require.NoError(t, err, "creating fake download file") + f1.Close() + + // Confirm we made the files + matches, err := filepath.Glob(filepath.Join(testLibraryManager.stagingDir, "*")) + require.NoError(t, err, "could not glob for files in staged osqueryd download dir") + require.Equal(t, 1, len(matches)) + // Set up existing versions for test for existingVersion, isExecutable := range tt.existingVersions { executablePath := executableLocation(filepath.Join(testLibraryManager.updatesDirectory(binary), existingVersion), binary) @@ -593,7 +513,14 @@ func Test_tidyUpdateLibrary(t *testing.T) { } // Tidy the library - testLibraryManager.tidyUpdateLibrary(binary, tt.currentlyRunningVersion) + testLibraryManager.TidyLibrary(binary, tt.currentlyRunningVersion) + + // Confirm the staging directory was tidied up + _, err = os.Stat(testLibraryManager.stagingDir) + require.NoError(t, err, "could not stat staged download dir") + matchesAfter, err := filepath.Glob(filepath.Join(testLibraryManager.stagingDir, "*")) + require.NoError(t, err, "could not glob for files in staged download dir") + require.Equal(t, 0, len(matchesAfter)) // Confirm that the versions we expect are still there for _, expectedPreservedVersion := range tt.expectedPreservedVersions { @@ -613,6 +540,65 @@ func Test_tidyUpdateLibrary(t *testing.T) { } } +func Test_sortedVersionsInLibrary(t *testing.T) { + t.Parallel() + + // Create update directories + testBaseDir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(testBaseDir, "launcher"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(testBaseDir, "osqueryd"), 0755)) + + // Create an update in the library that is invalid because it doesn't have a valid version + invalidVersion := "not_a_semver_1" + require.NoError(t, os.MkdirAll(filepath.Join(testBaseDir, "launcher", invalidVersion), 0755)) + + // Create an update in the library that is invalid because it's corrupted + corruptedVersion := "1.0.6-11-abcdabcd" + corruptedVersionDirectory := filepath.Join(testBaseDir, "launcher", corruptedVersion) + corruptedVersionLocation := executableLocation(corruptedVersionDirectory, binaryLauncher) + require.NoError(t, os.MkdirAll(filepath.Dir(corruptedVersionLocation), 0755)) + require.NoError(t, os.WriteFile( + filepath.Join(corruptedVersionLocation), + []byte("not an executable"), + 0755)) + + // Create a few valid updates in the library + olderValidVersion := "0.13.5" + middleValidVersion := "1.0.7-11-abcdabcd" + secondMiddleValidVersion := "1.0.7-16-g6e6704e1dc33" + newerValidVersion := "1.0.7" + for _, v := range []string{olderValidVersion, middleValidVersion, secondMiddleValidVersion, newerValidVersion} { + versionDir := filepath.Join(testBaseDir, "launcher", v) + executablePath := executableLocation(versionDir, binaryLauncher) + require.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0755)) + tufci.CopyBinary(t, executablePath) + require.NoError(t, os.Chmod(executablePath, 0755)) + _, err := os.Stat(executablePath) + require.NoError(t, err, "did not create binary for test") + require.NoError(t, autoupdate.CheckExecutable(context.TODO(), executablePath, "--version"), "binary created for test is corrupt") + } + + // Set up test library + testLibrary, err := newUpdateLibraryManager("", nil, testBaseDir, log.NewNopLogger()) + require.NoError(t, err, "unexpected error creating new read-only library") + + // Get sorted versions + validVersions, invalidVersions, err := testLibrary.sortedVersionsInLibrary(binaryLauncher) + require.NoError(t, err, "expected no error on sorting versions in library") + + // Confirm invalid versions are the ones we expect + require.Equal(t, 2, len(invalidVersions)) + require.Contains(t, invalidVersions, invalidVersion) + require.Contains(t, invalidVersions, corruptedVersion) + + // Confirm valid versions are the ones we expect and that they're sorted in ascending order + require.Equal(t, 4, len(validVersions)) + require.Equal(t, olderValidVersion, validVersions[0], "not sorted") + require.Equal(t, middleValidVersion, validVersions[1], "not sorted") + require.Equal(t, secondMiddleValidVersion, validVersions[2], "not sorted") + require.Equal(t, newerValidVersion, validVersions[3], "not sorted") +} + func Test_versionFromTarget(t *testing.T) { t.Parallel() @@ -661,7 +647,6 @@ func Test_versionFromTarget(t *testing.T) { } for _, testVersion := range testVersions { - libManager := &updateLibraryManager{} - require.Equal(t, testVersion.version, libManager.versionFromTarget(testVersion.binary, filepath.Base(testVersion.target))) + require.Equal(t, testVersion.version, versionFromTarget(testVersion.binary, filepath.Base(testVersion.target))) } } diff --git a/pkg/autoupdate/tuf/mock_librarian_test.go b/pkg/autoupdate/tuf/mock_librarian_test.go index c2b2c2eeb..2b74b3805 100644 --- a/pkg/autoupdate/tuf/mock_librarian_test.go +++ b/pkg/autoupdate/tuf/mock_librarian_test.go @@ -12,13 +12,13 @@ type Mocklibrarian struct { mock.Mock } -// AddToLibrary provides a mock function with given fields: binary, targetFilename, targetMetadata -func (_m *Mocklibrarian) AddToLibrary(binary autoupdatableBinary, targetFilename string, targetMetadata data.TargetFileMeta) error { - ret := _m.Called(binary, targetFilename, targetMetadata) +// AddToLibrary provides a mock function with given fields: binary, currentVersion, targetFilename, targetMetadata +func (_m *Mocklibrarian) AddToLibrary(binary autoupdatableBinary, currentVersion string, targetFilename string, targetMetadata data.TargetFileMeta) error { + ret := _m.Called(binary, currentVersion, targetFilename, targetMetadata) var r0 error - if rf, ok := ret.Get(0).(func(autoupdatableBinary, string, data.TargetFileMeta) error); ok { - r0 = rf(binary, targetFilename, targetMetadata) + if rf, ok := ret.Get(0).(func(autoupdatableBinary, string, string, data.TargetFileMeta) error); ok { + r0 = rf(binary, currentVersion, targetFilename, targetMetadata) } else { r0 = ret.Error(0) } @@ -40,9 +40,9 @@ func (_m *Mocklibrarian) Available(binary autoupdatableBinary, targetFilename st return r0 } -// TidyLibrary provides a mock function with given fields: -func (_m *Mocklibrarian) TidyLibrary() { - _m.Called() +// TidyLibrary provides a mock function with given fields: binary, currentVersion +func (_m *Mocklibrarian) TidyLibrary(binary autoupdatableBinary, currentVersion string) { + _m.Called(binary, currentVersion) } type mockConstructorTestingTNewMocklibrarian interface { diff --git a/pkg/autoupdate/tuf/mockery.md b/pkg/autoupdate/tuf/mockery.md new file mode 100644 index 000000000..57968cc55 --- /dev/null +++ b/pkg/autoupdate/tuf/mockery.md @@ -0,0 +1,18 @@ +# Quick notes about mockery usage in this package + +Mocks live in-package to avoid an import cycle (mocks must import this +package's `autoupdatableBinary` type; the package would have to import the mock +if it weren't in the package). The mock file names are suffixed with "_test" to +avoid shipping them. + +Mocks can be generated or re-generated with the following command: + +``` +mockery --name --filename=mock__test.go --exported --inpackage +``` + +For example: + +``` +mockery --name librarian --filename mock_librarian_test.go --exported --inpackage +```