diff --git a/cmd/launcher/interactive.go b/cmd/launcher/interactive.go index 0dff355a9..7828f4440 100644 --- a/cmd/launcher/interactive.go +++ b/cmd/launcher/interactive.go @@ -14,7 +14,6 @@ import ( "github.com/kolide/launcher/ee/agent/knapsack" "github.com/kolide/launcher/ee/agent/storage/inmemory" "github.com/kolide/launcher/ee/tuf" - "github.com/kolide/launcher/pkg/autoupdate" "github.com/kolide/launcher/pkg/launcher" "github.com/kolide/launcher/pkg/log/multislogger" "github.com/kolide/launcher/pkg/osquery/interactive" @@ -50,8 +49,8 @@ func runInteractive(systemMultiSlogger *multislogger.MultiSlogger, args []string if opts.OsquerydPath == "" { return errors.New("could not find osqueryd binary") } - // Fall back to old autoupdate library - opts.OsquerydPath = autoupdate.FindNewest(context.Background(), opts.OsquerydPath) + + return fmt.Errorf("finding osqueryd binary: %w", err) } else { opts.OsquerydPath = latestOsquerydBinary.Path } diff --git a/cmd/launcher/launcher.go b/cmd/launcher/launcher.go index 2302554d2..5dbfe0e44 100644 --- a/cmd/launcher/launcher.go +++ b/cmd/launcher/launcher.go @@ -332,7 +332,6 @@ func runLauncher(ctx context.Context, cancel func(), multiSlogger, systemMultiSl osqueryRunner := osqueryruntime.New( k, osqueryruntime.WithKnapsack(k), - osqueryruntime.WithOsquerydBinary(k.OsquerydPath()), osqueryruntime.WithRootDirectory(k.RootDirectory()), osqueryruntime.WithOsqueryExtensionPlugins(table.LauncherTables(k)...), osqueryruntime.WithSlogger(k.Slogger().With("component", "osquery_instance")), diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index 96e70964f..29e279de4 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -16,7 +16,6 @@ import ( "github.com/kolide/kit/logutil" "github.com/kolide/kit/version" "github.com/kolide/launcher/ee/tuf" - "github.com/kolide/launcher/pkg/autoupdate" "github.com/kolide/launcher/pkg/contexts/ctxlog" "github.com/kolide/launcher/pkg/execwrapper" "github.com/kolide/launcher/pkg/launcher" @@ -203,19 +202,10 @@ func runNewerLauncherIfAvailable(ctx context.Context, slogger *slog.Logger) erro newerBinary, err := latestLauncherPath(ctx, slogger) if err != nil { slogger.Log(ctx, slog.LevelError, - "could not check out latest launcher, will fall back to old autoupdate library", + "could not check out latest launcher", "err", err, ) - - // Fall back to legacy autoupdate library - newerBinary, err = autoupdate.FindNewestSelf(ctx) - if err != nil { - slogger.Log(ctx, slog.LevelError, - "could not check out latest launcher from legacy autoupdate library", - "err", err, - ) - return nil - } + return nil } if newerBinary == "" { diff --git a/cmd/launcher/svc_windows.go b/cmd/launcher/svc_windows.go index 6cf7234c9..221a4c816 100644 --- a/cmd/launcher/svc_windows.go +++ b/cmd/launcher/svc_windows.go @@ -16,7 +16,6 @@ import ( "github.com/kolide/kit/logutil" "github.com/kolide/kit/version" "github.com/kolide/launcher/ee/gowrapper" - "github.com/kolide/launcher/pkg/autoupdate" "github.com/kolide/launcher/pkg/contexts/ctxlog" "github.com/kolide/launcher/pkg/launcher" "github.com/kolide/launcher/pkg/log/locallogger" @@ -66,20 +65,6 @@ func runWindowsSvc(systemSlogger *multislogger.MultiSlogger, args []string) erro systemSlogger.AddHandler(localSloggerHandler) } - // Use the FindNewest mechanism to delete old - // updates. We do this here, as windows will pick up - // the update in main, which does not delete. Note - // that this will likely produce non-fatal errors when - // it tries to delete the running one. - go func() { - time.Sleep(15 * time.Second) - _ = autoupdate.FindNewest( - context.TODO(), - os.Args[0], - autoupdate.DeleteOldUpdates(), - ) - }() - // Confirm that service configuration is up-to-date checkServiceConfiguration(systemSlogger.Logger, opts) diff --git a/ee/agent/knapsack/knapsack.go b/ee/agent/knapsack/knapsack.go index 9d5896df4..20ee82468 100644 --- a/ee/agent/knapsack/knapsack.go +++ b/ee/agent/knapsack/knapsack.go @@ -12,7 +12,6 @@ import ( "github.com/kolide/launcher/ee/agent/storage" "github.com/kolide/launcher/ee/agent/types" "github.com/kolide/launcher/ee/tuf" - "github.com/kolide/launcher/pkg/autoupdate" "github.com/kolide/launcher/pkg/log/multislogger" "go.etcd.io/bbolt" ) @@ -149,7 +148,7 @@ func (k *knapsack) getKVStore(storeType storage.Store) types.KVStore { func (k *knapsack) LatestOsquerydPath(ctx context.Context) string { latestBin, err := tuf.CheckOutLatest(ctx, "osqueryd", k.RootDirectory(), k.UpdateDirectory(), k.PinnedOsquerydVersion(), k.UpdateChannel(), k.Slogger()) if err != nil { - return autoupdate.FindNewest(ctx, k.OsquerydPath()) + return k.OsquerydPath() } return latestBin.Path diff --git a/pkg/autoupdate/findnew.go b/pkg/autoupdate/findnew.go deleted file mode 100644 index 412f4a538..000000000 --- a/pkg/autoupdate/findnew.go +++ /dev/null @@ -1,349 +0,0 @@ -package autoupdate - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "sort" - "strings" - - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" - "github.com/kolide/launcher/ee/tuf" - "github.com/kolide/launcher/pkg/contexts/ctxlog" -) - -// defaultBuildTimestamp is used to set the _oldest_ allowed update. Eg, if -// there's an update with a download timestamp older than build, just -// ignore it. It's probably indicative of a machine that's been re-installed. -// -// This is a private variable. It should be set via build time -// LDFLAGS. -const defaultBuildTimestamp = "0" - -// This suffix is added to the binary path to find the updates -const updateDirSuffix = "-updates" - -type newestSettings struct { - deleteOld bool - deleteCorrupt bool - skipFullBinaryPathCheck bool - buildTimestamp string - runningExecutable string -} - -type newestOption func(*newestSettings) - -func DeleteOldUpdates() newestOption { - return func(no *newestSettings) { - no.deleteOld = true - } -} - -func DeleteCorruptUpdates() newestOption { - return func(no *newestSettings) { - no.deleteCorrupt = true - } -} - -// overrideBuildTimestamp overrides the buildTimestamp constant. This -// is to allow tests to mock that behavior. It is not exported, as it -// is expected to only be used for testing. -func overrideBuildTimestamp(ts string) newestOption { - return func(no *newestSettings) { - no.buildTimestamp = ts - } -} - -// SkipFullBinaryPathCheck skips the final check on FindNewest. This -// is desirable when being called by FindNewestSelf, otherewise we end -// up in a infineite recursion. (The recursion is saved by the exec -// check, but it's better not to trigger it. -func SkipFullBinaryPathCheck() newestOption { - return func(no *newestSettings) { - no.skipFullBinaryPathCheck = true - } -} - -// withRunningExectuable sets the current executable. This is because -// we never need to run an executable check against ourselves. (And -// doing so will trigger a fork bomb) -func withRunningExectuable(exe string) newestOption { - return func(no *newestSettings) { - no.runningExecutable = exe - } -} - -// FindNewestSelf invokes `FindNewest` with the running binary path, -// as determined by os.Executable. However, if the current running -// version is the same as the newest on disk, it will return empty string. -func FindNewestSelf(ctx context.Context, opts ...newestOption) (string, error) { - logger := log.With(ctxlog.FromContext(ctx), "caller", log.DefaultCaller) - - exPath, err := os.Executable() - if err != nil { - return "", fmt.Errorf("determine running executable path: %w", err) - } - - if exPath == "" { - return "", errors.New("can't find newest empty string") - } - - opts = append(opts, SkipFullBinaryPathCheck(), withRunningExectuable(exPath)) - - newest := FindNewest(ctx, exPath, opts...) - - if newest == "" { - return "", nil - } - - if exPath == newest { - return "", nil - } - - level.Debug(logger).Log( - "msg", "found an update", - "newest", newest, - "exPath", exPath, - ) - - return newest, nil -} - -// FindNewest takes the full path to a binary, and returns the newest -// update on disk. If there are no updates on disk, it returns the -// original path. It will return the same fullBinaryPath if that is -// the newest version. -func FindNewest(ctx context.Context, fullBinaryPath string, opts ...newestOption) string { - logger := log.With(ctxlog.FromContext(ctx), "caller", log.DefaultCaller) - - if fullBinaryPath == "" { - level.Debug(logger).Log("msg", "called with empty string") - return "" - } - - newestSettings := &newestSettings{ - buildTimestamp: defaultBuildTimestamp, - } - for _, opt := range opts { - opt(newestSettings) - } - - updateDir := getUpdateDir(fullBinaryPath) - binaryName := filepath.Base(fullBinaryPath) - - logger = log.With(logger, - "fullBinaryPath", fullBinaryPath, - "updateDir", updateDir, - "binaryName", binaryName, - ) - - // If no updates are found, the forloop is skipped, and we return either the seed fullBinaryPath or "" - possibleUpdates, err := getPossibleUpdates(ctx, updateDir, binaryName) - if err != nil { - level.Error(logger).Log("msg", "could not find possible updates", "err", err) - return fullBinaryPath - } - - // iterate backwards over files, looking for a suitable binary - foundCount := 0 - foundFile := "" - for i := len(possibleUpdates) - 1; i >= 0; i-- { - file := possibleUpdates[i] - basedir := filepath.Dir(file) - updateDownloadTime := filepath.Base(basedir) - foundExecutable := file - if strings.HasSuffix(file, ".app") { - // Add back the rest of the path to the binary that we'd stripped off to make - // timestamp comparison and old/broken updates cleanup easier. - foundExecutable = filepath.Join(file, "Contents", "MacOS", binaryName) - } - - // We only want to consider updates with a download time _newer_ - // than our build timestamp. Note that we're not comparing against - // the update's build time, only the download time. This is an - // important distinction to allow for downgrades. - if strings.Compare(newestSettings.buildTimestamp, updateDownloadTime) >= 0 { - level.Debug(logger).Log( - "msg", "update download is older than buildtime", - "dir", basedir, - "buildtime", newestSettings.buildTimestamp, - ) - - if newestSettings.deleteOld { - if err := os.RemoveAll(basedir); err != nil { - level.Error(logger).Log("msg", "error deleting old update dir", "dir", basedir, "err", err) - } - } - - continue - } - - // If we've already found at least 2 files, (newest, and presumed - // current), trigger delete routine - if newestSettings.deleteOld && foundCount >= 2 { - level.Debug(logger).Log("msg", "deleting old updates", "dir", basedir) - if err := os.RemoveAll(basedir); err != nil { - level.Error(logger).Log("msg", "error deleting old update dir", "dir", basedir, "err", err) - } - } - - // If the file is _not_ the running executable, sanity - // check that executions work. If the exec fails, - // there's clearly an issue and we should remove it. - if newestSettings.runningExecutable != foundExecutable { - if err := tuf.CheckExecutable(ctx, foundExecutable, "--version"); err != nil { - if newestSettings.deleteCorrupt { - level.Error(logger).Log("msg", "not executable. Removing", "binary", foundExecutable, "reason", err) - if err := os.RemoveAll(basedir); err != nil { - level.Error(logger).Log("msg", "error deleting broken update dir", "dir", basedir, "err", err) - } - } else { - level.Error(logger).Log("msg", "not executable. Skipping", "binary", foundExecutable, "reason", err) - } - - continue - } - } else { - // This logging is mostly here to make test coverage of the conditional clear - level.Debug(logger).Log("msg", "Skipping checkExecutable against self", "file", file) - } - - // We always want to increment the foundCount, since it's what triggers deletion. - foundCount = foundCount + 1 - - // Only set what we've found, if it's unset. - if foundFile == "" { - foundFile = foundExecutable - } - } - - if foundFile != "" { - return foundFile - } - - level.Debug(logger).Log("msg", "no updates found") - - if newestSettings.skipFullBinaryPathCheck { - return fullBinaryPath - } - - if err := tuf.CheckExecutable(ctx, fullBinaryPath, "--version"); err != nil { - level.Debug(logger).Log("msg", "fullBinaryPath not executable. Returning nil", "err", err) - return "" - } - - return fullBinaryPath -} - -// getUpdateDir returns the expected update path for a given -// binary. It should work when called with either a base executable -// `/usr/local/bin/launcher` or with an existing updated -// `/usr/local/bin/launcher-updates/1234/launcher`. -// -// It makes some string assumptions about how things are named. -func getUpdateDir(fullBinaryPath string) string { - if strings.Contains(fullBinaryPath, ".app") { - binary := filepath.Base(fullBinaryPath) - return filepath.Join(findBaseDir(fullBinaryPath), binary+updateDirSuffix) - } - - // These are cases that shouldn't really happen. But, this is - // a bare string function. So return "" when they do. - fullBinaryPath = strings.TrimSuffix(fullBinaryPath, "/") - - if fullBinaryPath == "" { - return "" - } - - // If we SplitN on updateDirSuffix, we will either get an - // array, or the full string back. This means we can forgo a - // strings.Contains, and just use the returned element - components := strings.SplitN(fullBinaryPath, updateDirSuffix, 2) - - return fmt.Sprintf("%s%s", components[0], updateDirSuffix) -} - -// Find the possible updates. filepath.Glob returns a list of things -// that match the requested pattern. We sort the list to ensure that -// we can tell which ones are earlier or later (remember, these are -// timestamps). -func getPossibleUpdates(ctx context.Context, updateDir, binaryName string) ([]string, error) { - logger := log.With(ctxlog.FromContext(ctx), "caller", log.DefaultCaller) - - // If this is launcher running on macOS, then we should have app bundles available instead -- - // check for those first. - if runtime.GOOS == "darwin" { - binarySuffix := filepath.Join("Contents", "MacOS", binaryName) - fileGlob := filepath.Join(updateDir, "*", "*.app", binarySuffix) - possibleUpdates, err := filepath.Glob(fileGlob) - - if err == nil && len(possibleUpdates) > 0 { - appBundleNames := make([]string, len(possibleUpdates)) - for i, binaryPath := range possibleUpdates { - // We trim the suffix here for compatibility with prior logic for timestamp - // comparison in the directory and cleanup for old/broken updates. The suffix - // is added back later by the caller. - appBundleNames[i] = strings.TrimSuffix(binaryPath, "/"+binarySuffix) - } - sort.Strings(appBundleNames) - return appBundleNames, nil - } - - // If the error is non-nil, something has gone very wrong -- log and then ignore the - // error so that we can fall back to previous behavior below, so that launcher is - // still able to auto-update. - if err != nil { - level.Error(logger).Log("msg", "could not glob for app bundle binaries", "err", err) - } - } - - // Either not macOS/launcher or no app bundles found. Fall back to searching for binaries. - fileGlob := filepath.Join(updateDir, "*", binaryName) - - possibleUpdates, err := filepath.Glob(fileGlob) - if err != nil { - return nil, err - } - - sort.Strings(possibleUpdates) - - return possibleUpdates, nil -} - -// findBaseDir takes a binary path, that may or may not include the -// update directory, and returns the base directory. It's used by the -// launcher runtime in finding the various binaries. -func findBaseDir(path string) string { - if path == "" { - return "" - } - - // If this is an app bundle installation, we need to adjust the directory -- otherwise we end up with a library - // of updates at /usr/local//Kolide.app/Contents/MacOS/launcher-updates. - if strings.Contains(path, ".app") { - components := strings.SplitN(path, ".app", 2) - baseDir := filepath.Dir(components[0]) // gets rid of app bundle name and trailing slash - - // If baseDir still contains an update directory (i.e. the original path was something like - // /usr/local//launcher-updates//Kolide.app/Contents/MacOS/launcher), - // then strip the update directory out. - if strings.Contains(baseDir, updateDirSuffix) { - baseDirComponents := strings.SplitN(baseDir, updateDirSuffix, 2) - baseDir = filepath.Dir(baseDirComponents[0]) - } - - // We moved the Kolide.app installation out of the bin directory, but we want the bin directory - // here -- so put the "bin" suffix back on if needed. - if !strings.HasSuffix(baseDir, "bin") { - baseDir = filepath.Join(baseDir, "bin") - } - return baseDir - } - - components := strings.SplitN(path, updateDirSuffix, 2) - return filepath.Dir(components[0]) -} diff --git a/pkg/autoupdate/findnew_test.go b/pkg/autoupdate/findnew_test.go deleted file mode 100644 index 6d3190971..000000000 --- a/pkg/autoupdate/findnew_test.go +++ /dev/null @@ -1,515 +0,0 @@ -package autoupdate - -import ( - "context" - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -// TestFindNewestSelf tests the FindNewestSelf. Hard to test this, as -// it's a light wrapper around os.Executable -func TestFindNewestSelf(t *testing.T) { - t.Parallel() - - // These tests will be deleted in https://github.com/kolide/launcher/pull/1679 - // after the next stable release. - if os.Getenv("CI") != "" { - t.Skip("skipping flaky and soon-to-be-deleted tests in CI") - } - - ctx := context.TODO() - - { - newest, err := FindNewestSelf(ctx) - require.NoError(t, err) - require.Empty(t, newest, "No updates, should be empty") - } - - // Let's try making a set of update directories - binaryPath := os.Args[0] - updatesDir := getUpdateDir(binaryPath) - require.NotEmpty(t, updatesDir) - defer os.RemoveAll(updatesDir) - for _, n := range []string{"2", "5", "3", "1"} { - require.NoError(t, os.MkdirAll(filepath.Join(updatesDir, n), 0755)) - f, err := os.Create(filepath.Join(updatesDir, n, "wrong-binary")) - require.NoError(t, err) - f.Close() - require.NoError(t, os.Chmod(f.Name(), 0755)) - } - - { - newest, err := FindNewestSelf(ctx) - require.NoError(t, err) - require.Empty(t, newest, "No correct binaries, should be empty") - } - - for _, n := range []string{"2", "3"} { - updatedBinaryPath := filepath.Join(updatesDir, n, filepath.Base(binaryPath)) - require.NoError(t, copyFile(updatedBinaryPath, binaryPath, false), "copy executable") - require.NoError(t, os.Chmod(updatedBinaryPath, 0755), "chmod") - } - - { - expectedNewest := filepath.Join(updatesDir, "3", filepath.Base(binaryPath)) - newest, err := FindNewestSelf(ctx) - require.NoError(t, err) - require.Equal(t, expectedNewest, newest, "Should find newer binary") - } - -} - -func TestGetUpdateDir(t *testing.T) { - t.Parallel() - - var tests = []struct { - in string - out string - }{ - {in: "/a/bin/path", out: "/a/bin/path-updates"}, - {in: "/a/bin/path-updates", out: "/a/bin/path-updates"}, - {in: "/a/bin/path-updates/1234/binary", out: "/a/bin/path-updates"}, - {in: "/a/bin/path/foo/bar-updates/1234/binary", out: "/a/bin/path/foo/bar-updates"}, - {in: "/a/bin/b-updates/123/b-updates/456/b", out: "/a/bin/b-updates"}, - {in: "/a/bin/path/", out: "/a/bin/path-updates"}, - {in: "/a/Test.app/Contents/MacOS/path", out: filepath.Clean("/a/bin/path-updates")}, - {in: "/a/bin/path-updates/1234/Test.app/Contents/MacOS/path", out: filepath.Clean("/a/bin/path-updates")}, - {in: "/a/bin/Test.app/Contents/MacOS/launcher-updates/1569339163/Test.app/Contents/MacOS/path", out: filepath.Clean("/a/bin/path-updates")}, - {in: "/a/bin/Test.app/Contents/MacOS/launcher", out: filepath.Clean("/a/bin/launcher-updates")}, - {in: "/a/bin/Test.app/Contents/MacOS/launcher-updates/1569339163/Test.app/Contents/MacOS/launcher", out: filepath.Clean("/a/bin/launcher-updates")}, - {in: "", out: ""}, - {in: "/", out: ""}, - } - - for _, tt := range tests { - require.Equal(t, tt.out, getUpdateDir(tt.in), "input: %s", tt.in) - } -} - -func TestFindBaseDir(t *testing.T) { - t.Parallel() - - var tests = []struct { - in string - out string - }{ - {in: "", out: ""}, - {in: "/a/path/bin/launcher", out: filepath.Clean("/a/path/bin")}, - {in: "/a/path/bin/launcher-updates/1569339163/launcher", out: filepath.Clean("/a/path/bin")}, - {in: "/a/path/bin/launcher-updates/1569339163/Test.app/Contents/MacOS/launcher", out: filepath.Clean("/a/path/bin")}, - {in: "/a/path/Test.app/Contents/MacOS/launcher", out: filepath.Clean("/a/path/bin")}, - {in: "/a/path/Test.app/Contents/MacOS/launcher-updates/1569339163/Test.app/Contents/MacOS/launcher", out: filepath.Clean("/a/path/bin")}, - {in: "/a/path/bin/Test.app/Contents/MacOS/launcher", out: filepath.Clean("/a/path/bin")}, - {in: "/a/path/bin/Test.app/Contents/MacOS/launcher-updates/1569339163/Test.app/Contents/MacOS/launcher", out: filepath.Clean("/a/path/bin")}, - } - - for _, tt := range tests { - require.Equal(t, tt.out, findBaseDir(tt.in), "input: %s", tt.in) - } -} - -func TestFindNewestEmpty(t *testing.T) { - t.Parallel() - - // These tests will be deleted in https://github.com/kolide/launcher/pull/1679 - // after the next stable release. - if os.Getenv("CI") != "" { - t.Skip("skipping flaky and soon-to-be-deleted tests in CI") - } - - tmpDir, binaryName := setupTestDir(t, emptySetup) - ctx := context.TODO() - binaryPath := filepath.Join(tmpDir, binaryName) - - // Basic tests, test with binary and no updates - require.Empty(t, FindNewest(ctx, ""), "passing empty string") - require.Empty(t, FindNewest(ctx, tmpDir), "passing directory as arg") - require.Equal(t, binaryPath, FindNewest(ctx, binaryPath), "no update directory") -} - -func TestFindNewestEmptyUpdateDirs(t *testing.T) { - t.Parallel() - - // These tests will be deleted in https://github.com/kolide/launcher/pull/1679 - // after the next stable release. - if os.Getenv("CI") != "" { - t.Skip("skipping flaky and soon-to-be-deleted tests in CI") - } - - tmpDir, binaryName := setupTestDir(t, emptyUpdateDirs) - ctx := context.TODO() - binaryPath := filepath.Join(tmpDir, binaryName) - - require.Equal(t, binaryPath, FindNewest(ctx, binaryPath), "update dir, but no updates") -} - -func TestFindNewestNonExecutable(t *testing.T) { - t.Parallel() - - if runtime.GOOS == "windows" { - t.Skip("Windows doesn't use executable bit") - } - - tmpDir, binaryName := setupTestDir(t, nonExecutableUpdates) - ctx := context.TODO() - binaryPath := filepath.Join(tmpDir, binaryName) - updatesDir := fmt.Sprintf("%s%s", binaryPath, updateDirSuffix) - - require.Equal(t, binaryPath, FindNewest(ctx, binaryPath), "update dir, but only plain files") - - expectedNewest := filepath.Join(updatesDir, "1", "binary") - require.NoError(t, os.Chmod(expectedNewest, 0755)) - if runtime.GOOS == "darwin" { - expectedNewest = filepath.Join(updatesDir, "1", "Test.app", "Contents", "MacOS", "binary") - require.NoError(t, os.Chmod(expectedNewest, 0755)) - } - - require.Equal(t, - expectedNewest, - FindNewest(ctx, binaryPath), - "Should find number 1", - ) -} - -func TestFindNewestExecutableUpdates(t *testing.T) { - t.Parallel() - - // These tests will be deleted in https://github.com/kolide/launcher/pull/1679 - // after the next stable release. - if os.Getenv("CI") != "" { - t.Skip("skipping flaky and soon-to-be-deleted tests in CI") - } - - tmpDir, binaryName := setupTestDir(t, executableUpdates) - ctx := context.TODO() - binaryPath := filepath.Join(tmpDir, binaryName) - updatesDir := fmt.Sprintf("%s%s", binaryPath, updateDirSuffix) - - expectedNewest := filepath.Join(updatesDir, "5", "binary") - if runtime.GOOS == "windows" { - expectedNewest = expectedNewest + ".exe" - } else if runtime.GOOS == "darwin" { - expectedNewest = filepath.Join(updatesDir, "5", "Test.app", "Contents", "MacOS", "binary") - } - - require.Equal(t, expectedNewest, FindNewest(ctx, binaryPath), "Should find number 5") - require.Equal(t, expectedNewest, FindNewest(ctx, expectedNewest), "already running the newest") - -} - -func TestFindNewestCleanup(t *testing.T) { - t.Parallel() - - // These tests will be deleted in https://github.com/kolide/launcher/pull/1679 - // after the next stable release. - if os.Getenv("CI") != "" { - t.Skip("skipping flaky and soon-to-be-deleted tests in CI") - } - - // delete doesn't seem to work on windows. It gets a - // "Access is denied" error". This may be a test setup - // issue, or something with an open file handle. - if runtime.GOOS == "windows" { - t.Skip("TODO: Windows deletion test is broken") - } - - tmpDir, binaryName := setupTestDir(t, executableUpdates) - ctx := context.TODO() - binaryPath := filepath.Join(tmpDir, binaryName) - updatesDir := fmt.Sprintf("%s%s", binaryPath, updateDirSuffix) - - expectedNewest := filepath.Join(updatesDir, "5", "binary") - if runtime.GOOS == "windows" { - expectedNewest = expectedNewest + ".exe" - } else if runtime.GOOS == "darwin" { - expectedNewest = filepath.Join(updatesDir, "5", "Test.app", "Contents", "MacOS", "binary") - } - - { - updatesOnDisk, err := os.ReadDir(updatesDir) - require.NoError(t, err) - require.Equal(t, 4, len(updatesOnDisk)) - require.Equal(t, expectedNewest, FindNewest(ctx, binaryPath), "Should find number 5") - } - - { - _ = FindNewest(ctx, binaryPath, DeleteOldUpdates()) - updatesOnDisk, err := os.ReadDir(updatesDir) - require.NoError(t, err) - require.Equal(t, expectedNewest, FindNewest(ctx, binaryPath), "Should find number 5") - require.Equal(t, 2, len(updatesOnDisk), "after delete") - - } -} - -func TestCheckExecutableCorruptCleanup(t *testing.T) { - t.Parallel() - - // These tests will be deleted in https://github.com/kolide/launcher/pull/1679 - // after the next stable release. - if os.Getenv("CI") != "" { - t.Skip("skipping flaky and soon-to-be-deleted tests in CI") - } - - tmpDir, binaryName := setupTestDir(t, truncatedUpdates) - ctx := context.TODO() - binaryPath := filepath.Join(tmpDir, binaryName) - updatesDir := fmt.Sprintf("%s%s", binaryPath, updateDirSuffix) - - expectedNewest := filepath.Join(updatesDir, "3", "binary") - if runtime.GOOS == "windows" { - expectedNewest = expectedNewest + ".exe" - } else if runtime.GOOS == "darwin" { - expectedNewest = filepath.Join(updatesDir, "3", "Test.app", "Contents", "MacOS", "binary") - } - - { - updatesOnDisk, err := os.ReadDir(updatesDir) - require.NoError(t, err) - require.Equal(t, 4, len(updatesOnDisk)) - require.Equal(t, expectedNewest, FindNewest(ctx, binaryPath), "Should find number 3") - } - - { - _ = FindNewest(ctx, binaryPath, DeleteCorruptUpdates()) - updatesOnDisk, err := os.ReadDir(updatesDir) - require.NoError(t, err) - require.Equal(t, 2, len(updatesOnDisk), "after cleaning corruption") - require.Equal(t, expectedNewest, FindNewest(ctx, binaryPath), "Should find number 3") - } -} - -type setupState int - -const ( - emptySetup setupState = iota - emptyUpdateDirs - nonExecutableUpdates - executableUpdates - truncatedUpdates -) - -// setupTestDir function to setup the test dirs. This work is broken -// up in stages, allowing test functions to tap into various -// points. This is setup this way to allow simpler isolation on test -// failures. -func setupTestDir(t *testing.T, stage setupState) (string, string) { - tmpDir := t.TempDir() - - // Create a test binary - binaryName := windowsAddExe("binary") - binaryPath := filepath.Join(tmpDir, binaryName) - updatesDir := fmt.Sprintf("%s%s", binaryPath, updateDirSuffix) - - require.NoError(t, copyFile(binaryPath, os.Args[0], false), "copy executable") - require.NoError(t, os.Chmod(binaryPath, 0755), "chmod") - - if stage <= emptySetup { - return tmpDir, binaryName - } - - // make some update directories - // (these are out of order, to jumble up the create times) - for _, n := range []string{"2", "5", "3", "1"} { - require.NoError(t, os.MkdirAll(filepath.Join(updatesDir, n), 0755)) - if runtime.GOOS == "darwin" { - require.NoError(t, os.MkdirAll(filepath.Join(updatesDir, n, "Test.app", "Contents", "MacOS"), 0755)) - } - } - - if stage <= emptyUpdateDirs { - return tmpDir, binaryName - } - - // Copy executable to update directories - for _, n := range []string{"2", "5", "3", "1"} { - updatedBinaryPath := filepath.Join(updatesDir, n, binaryName) - require.NoError(t, copyFile(updatedBinaryPath, binaryPath, false), "copy executable") - if runtime.GOOS == "darwin" { - updatedAppBundleBinaryPath := filepath.Join(updatesDir, n, "Test.app", "Contents", "MacOS", filepath.Base(binaryPath)) - require.NoError(t, copyFile(updatedAppBundleBinaryPath, binaryPath, false), "copy executable") - } - } - - if stage <= nonExecutableUpdates { - return tmpDir, binaryName - } - - // Make our top-level binaries executable - for _, n := range []string{"2", "5", "3", "1"} { - require.NoError(t, os.Chmod(filepath.Join(updatesDir, n, binaryName), 0755)) - if runtime.GOOS == "darwin" { - require.NoError(t, os.Chmod(filepath.Join(updatesDir, n, "Test.app", "Contents", "MacOS", binaryName), 0755)) - } - } - - if stage <= executableUpdates { - return tmpDir, binaryName - } - - for _, n := range []string{"5", "1"} { - updatedBinaryPath := filepath.Join(updatesDir, n, binaryName) - require.NoError(t, copyFile(updatedBinaryPath, binaryPath, true), "copy & truncate executable") - if runtime.GOOS == "darwin" { - require.NoError(t, copyFile(filepath.Join(updatesDir, n, "Test.app", "Contents", "MacOS", binaryName), binaryPath, true), "copy & truncate executable") - } - } - - return tmpDir, binaryName - -} - -// copyFile copies a file from srcPath to dstPath. If truncate is set, -// only half the file is copied. (This is a trivial wrapper to -// simplify setting up test cases) -func copyFile(dstPath, srcPath string, truncate bool) error { - src, err := os.Open(srcPath) - if err != nil { - return err - } - defer src.Close() - - dst, err := os.Create(dstPath) - if err != nil { - return err - } - defer dst.Close() - - if !truncate { - if _, err := io.Copy(dst, src); err != nil { - return err - } - } else { - stat, err := src.Stat() - if err != nil { - return fmt.Errorf("statting srcFile: %w", err) - } - - if _, err = io.CopyN(dst, src, stat.Size()/2); err != nil { - return fmt.Errorf("copying srcFile: %w", err) - } - } - - return nil -} - -func TestBuildTimestamp(t *testing.T) { - t.Parallel() - - if runtime.GOOS == "windows" { - t.Skip("FIXME: Windows") - } - - var tests = []struct { - buildTimestamp string - expectedNewest string - expectedOnDisk int - }{ - { - buildTimestamp: "0", - expectedNewest: "5", - expectedOnDisk: 2, - }, - { - buildTimestamp: "3", - expectedNewest: "5", - expectedOnDisk: 1, // remember, 4 is broken, so there should only be update 5 on disk - }, - { - buildTimestamp: "5", - expectedOnDisk: 0, - }, - { - buildTimestamp: "6", - expectedOnDisk: 0, - }, - } - - for _, tt := range tests { - tt := tt - t.Run("buildTimestamp="+tt.buildTimestamp, func(t *testing.T) { - t.Parallel() - - tmpDir, binaryName := setupTestDir(t, executableUpdates) - ctx := context.TODO() - - binaryPath := filepath.Join(tmpDir, binaryName) - updatesDir := binaryPath + updateDirSuffix - - returnedNewest := FindNewest( - ctx, - binaryPath, - overrideBuildTimestamp(tt.buildTimestamp), - DeleteOldUpdates(), - ) - - updatesOnDisk, err := os.ReadDir(updatesDir) - require.NoError(t, err) - require.Equal(t, tt.expectedOnDisk, len(updatesOnDisk), "remaining updates on disk") - - if tt.expectedNewest == "" { - require.Equal(t, binaryPath, returnedNewest, "Expected to get original binary path") - } else { - updateFragment := strings.TrimPrefix(strings.TrimPrefix(returnedNewest, updatesDir), "/") - expectedNewest := filepath.Join(tt.expectedNewest, "binary") - if runtime.GOOS == "darwin" { - expectedNewest = filepath.Join(tt.expectedNewest, "Test.app", "Contents", "MacOS", "binary") - } - require.Equal(t, expectedNewest, updateFragment) - } - - }) - } - -} - -// TestHelperProcess isn't a real test. It's used as a helper process -// to make a portableish binary. See -// https://github.com/golang/go/blob/master/src/os/exec/exec_test.go#L724 -// and https://npf.io/2015/06/testing-exec-command/ -func TestHelperProcess(t *testing.T) { - t.Parallel() - - // find out magic arguments - args := os.Args - for len(args) > 0 { - if args[0] == "--" { - args = args[1:] - break - } - args = args[1:] - } - if len(args) == 0 { - // Indicates an error, or just being run in the test suite. - return - } - - switch args[0] { - case "sleep": - time.Sleep(10 * time.Second) - case "exit0": - os.Exit(0) //nolint:forbidigo // Fine to use os.Exit in tests - case "exit1": - os.Exit(1) //nolint:forbidigo // Fine to use os.Exit in tests - case "exit2": - os.Exit(2) //nolint:forbidigo // Fine to use os.Exit in tests - } - - // default behavior nothing -} - -func windowsAddExe(in string) string { - if runtime.GOOS == "windows" { - return in + ".exe" - } - - return in -} diff --git a/pkg/make/builder.go b/pkg/make/builder.go index 4a1561665..ab86c9d90 100644 --- a/pkg/make/builder.go +++ b/pkg/make/builder.go @@ -20,7 +20,6 @@ import ( "path/filepath" "regexp" "runtime" - "strconv" "strings" "time" @@ -342,9 +341,6 @@ func (b *Builder) BuildCmd(src, appName string) func(context.Context) error { ldFlags = append(ldFlags, fmt.Sprintf(`-X "github.com/kolide/kit/version.goVersion=%s"`, runtime.Version())) } - // Set the build time for autoupdate.FindNewest - ldFlags = append(ldFlags, fmt.Sprintf(`-X "github.com/kolide/launcher/pkg/autoupdate.defaultBuildTimestamp=%s"`, strconv.FormatInt(time.Now().Unix(), 10))) - if len(ldFlags) != 0 { baseArgs = append(baseArgs, fmt.Sprintf("--ldflags=%s", strings.Join(ldFlags, " "))) } diff --git a/pkg/osquery/runtime/osqueryinstance.go b/pkg/osquery/runtime/osqueryinstance.go index 632a40a0b..8fa121722 100644 --- a/pkg/osquery/runtime/osqueryinstance.go +++ b/pkg/osquery/runtime/osqueryinstance.go @@ -43,17 +43,6 @@ func WithOsqueryExtensionPlugins(plugins ...osquery.OsqueryPlugin) OsqueryInstan } } -// WithOsquerydBinary is a functional option which allows the user to define the -// path of the osqueryd binary which will be launched. This should only be called -// once as only one binary will be executed. Defining the path to the osqueryd -// binary is optional. If it is not explicitly defined by the caller, an osqueryd -// binary will be looked for in the current $PATH. -func WithOsquerydBinary(path string) OsqueryInstanceOption { - return func(i *OsqueryInstance) { - i.opts.binaryPath = path - } -} - // WithRootDirectory is a functional option which allows the user to define the // path where filesystem artifacts will be stored. This may include pidfiles, // RocksDB database files, etc. If this is not defined, a temporary directory @@ -305,7 +294,6 @@ type osqueryOptions struct { // the following are options which may or may not be set by the functional // options included by the caller of LaunchOsqueryInstance augeasLensFunc func(dir string) error - binaryPath string configPluginFlag string distributedPluginFlag string extensionPlugins []osquery.OsqueryPlugin diff --git a/pkg/osquery/runtime/runner.go b/pkg/osquery/runtime/runner.go index f3979764d..4ac24eb14 100644 --- a/pkg/osquery/runtime/runner.go +++ b/pkg/osquery/runtime/runner.go @@ -6,8 +6,6 @@ import ( "fmt" "log/slog" "os" - "os/exec" - "runtime" "strings" "sync" "time" @@ -15,8 +13,6 @@ import ( "github.com/kolide/launcher/ee/agent/flags/keys" "github.com/kolide/launcher/ee/agent/types" - "github.com/kolide/launcher/ee/tuf" - "github.com/kolide/launcher/pkg/autoupdate" "github.com/kolide/launcher/pkg/backoff" "github.com/kolide/launcher/pkg/osquery/runtime/history" "github.com/kolide/launcher/pkg/osquery/table" @@ -217,23 +213,6 @@ func (r *Runner) launchOsqueryInstance() error { o := r.instance - // What binary name to look for - lookFor := "osqueryd" - if runtime.GOOS == "windows" { - lookFor = lookFor + ".exe" - } - - // If the path of the osqueryd binary wasn't explicitly defined by the caller, - // try to find it in the path. - if o.opts.binaryPath == "" { - path, err := exec.LookPath(lookFor) - if err != nil { - traces.SetError(span, fmt.Errorf("osqueryd not supplied and not found: %w", err)) - return fmt.Errorf("osqueryd not supplied and not found: %w", err) - } - o.opts.binaryPath = path - } - // If the caller did not define the directory which all of the osquery file // artifacts should be stored in, use a temporary directory. if o.opts.rootDirectory == "" { @@ -302,33 +281,9 @@ func (r *Runner) launchOsqueryInstance() error { o.opts.distributedPluginFlag = "internal_noop" } - // If we're on windows, ensure that we're looking for the .exe - if runtime.GOOS == "windows" && !strings.HasSuffix(o.opts.binaryPath, ".exe") { - o.opts.binaryPath = o.opts.binaryPath + ".exe" - } - - // before we start osqueryd, check with the update system to - // see if we have the newest version. Do this every time. If - // this proves undesirable, we can expose a function to set - // o.opts.binaryPath in the finalizer to call. - // - // FindNewest uses context as a way to get a logger, so we need to - // create and pass a ctxlog in. - var currentOsquerydBinaryPath string - currentOsquerydBinary, err := tuf.CheckOutLatest(ctx, "osqueryd", o.opts.rootDirectory, o.opts.updateDirectory, o.knapsack.PinnedOsquerydVersion(), o.opts.updateChannel, r.slogger) - if err != nil { - r.slogger.Log(ctx, slog.LevelDebug, - "could not get latest version of osqueryd from new autoupdate library, falling back", - "err", err, - ) - currentOsquerydBinaryPath = autoupdate.FindNewest( - ctx, - o.opts.binaryPath, - autoupdate.DeleteOldUpdates(), - ) - } else { - currentOsquerydBinaryPath = currentOsquerydBinary.Path - } + // The knapsack will retrieve the correct version of osqueryd from the download library if available. + // If not available, it will fall back to the configured installed version of osqueryd. + currentOsquerydBinaryPath := o.knapsack.LatestOsquerydPath(ctx) span.AddEvent("got_osqueryd_binary_path", trace.WithAttributes(attribute.String("path", currentOsquerydBinaryPath))) // Now that we have accepted options from the caller and/or determined what diff --git a/pkg/osquery/runtime/runtime_posix_test.go b/pkg/osquery/runtime/runtime_posix_test.go index 615829dc3..f4d09d8c1 100644 --- a/pkg/osquery/runtime/runtime_posix_test.go +++ b/pkg/osquery/runtime/runtime_posix_test.go @@ -50,13 +50,12 @@ func TestOsquerySlowStart(t *testing.T) { k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) slogger := multislogger.New(slog.NewJSONHandler(&logBytes, &slog.HandlerOptions{Level: slog.LevelDebug})) k.On("Slogger").Return(slogger.Logger) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return(testOsqueryBinaryDirectory) runner := New( k, WithKnapsack(k), WithRootDirectory(rootDirectory), - WithOsquerydBinary(testOsqueryBinaryDirectory), WithSlogger(slogger.Logger), WithStartFunc(func(cmd *exec.Cmd) error { err := cmd.Start() @@ -95,7 +94,7 @@ func TestExtensionSocketPath(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return(testOsqueryBinaryDirectory) extensionSocketPath := filepath.Join(rootDirectory, "sock") runner := New( @@ -103,7 +102,6 @@ func TestExtensionSocketPath(t *testing.T) { WithKnapsack(k), WithRootDirectory(rootDirectory), WithExtensionSocketPath(extensionSocketPath), - WithOsquerydBinary(testOsqueryBinaryDirectory), ) go runner.Run() diff --git a/pkg/osquery/runtime/runtime_test.go b/pkg/osquery/runtime/runtime_test.go index f30392854..4e9dcbccd 100644 --- a/pkg/osquery/runtime/runtime_test.go +++ b/pkg/osquery/runtime/runtime_test.go @@ -312,13 +312,12 @@ func TestBadBinaryPath(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("Slogger").Return(multislogger.NewNopLogger()) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return("") runner := New( k, WithKnapsack(k), WithRootDirectory(rootDirectory), - WithOsquerydBinary("/foobar"), ) assert.Error(t, runner.Run()) @@ -336,13 +335,12 @@ func TestWithOsqueryFlags(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return(testOsqueryBinaryDirectory) runner := New( k, WithKnapsack(k), WithRootDirectory(rootDirectory), - WithOsquerydBinary(testOsqueryBinaryDirectory), WithOsqueryFlags([]string{"verbose=false"}), ) go runner.Run() @@ -369,14 +367,13 @@ func TestFlagsChanged(t *testing.T) { k.On("WatchdogDelaySec").Return(120) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return(testOsqueryBinaryDirectory) // Start the runner runner := New( k, WithKnapsack(k), WithRootDirectory(rootDirectory), - WithOsquerydBinary(testOsqueryBinaryDirectory), WithOsqueryFlags([]string{"verbose=false"}), ) go runner.Run() @@ -463,13 +460,12 @@ func TestSimplePath(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return(testOsqueryBinaryDirectory) runner := New( k, WithKnapsack(k), WithRootDirectory(rootDirectory), - WithOsquerydBinary(testOsqueryBinaryDirectory), ) go runner.Run() @@ -492,13 +488,12 @@ func TestMultipleShutdowns(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return(testOsqueryBinaryDirectory) runner := New( k, WithKnapsack(k), WithRootDirectory(rootDirectory), - WithOsquerydBinary(testOsqueryBinaryDirectory), ) go runner.Run() @@ -520,13 +515,12 @@ func TestOsqueryDies(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return(testOsqueryBinaryDirectory) runner := New( k, WithKnapsack(k), WithRootDirectory(rootDirectory), - WithOsquerydBinary(testOsqueryBinaryDirectory), ) go runner.Run() require.NoError(t, err) @@ -615,13 +609,12 @@ func setupOsqueryInstanceForTests(t *testing.T) (runner *Runner, teardown func() k.On("WatchdogDelaySec").Return(120) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe() k.On("Slogger").Return(multislogger.NewNopLogger()) - k.On("PinnedOsquerydVersion").Return("") + k.On("LatestOsquerydPath", mock.Anything).Return(testOsqueryBinaryDirectory) runner = New( k, WithKnapsack(k), WithRootDirectory(rootDirectory), - WithOsquerydBinary(testOsqueryBinaryDirectory), ) go runner.Run() waitHealthy(t, runner)