diff --git a/ee/tuf/autoupdate.go b/ee/tuf/autoupdate.go index 53db0c12f..d25f6815b 100644 --- a/ee/tuf/autoupdate.go +++ b/ee/tuf/autoupdate.go @@ -21,6 +21,7 @@ import ( "time" "github.com/kolide/kit/version" + "github.com/kolide/launcher/ee/agent/flags/keys" "github.com/kolide/launcher/ee/agent/types" "github.com/kolide/launcher/pkg/traces" client "github.com/theupdateframework/go-tuf/client" @@ -86,6 +87,7 @@ type TufAutoupdater struct { osquerierRetryInterval time.Duration knapsack types.Knapsack store types.KVStore // stores autoupdater errors for kolide_tuf_autoupdater_errors table + updateChannel string updateLock *sync.Mutex interrupt chan struct{} interrupted bool @@ -115,6 +117,7 @@ func NewTufAutoupdater(ctx context.Context, k types.Knapsack, metadataHttpClient interrupt: make(chan struct{}, 1), signalRestart: make(chan error, 1), store: k.AutoupdateErrorsStore(), + updateChannel: k.UpdateChannel(), updateLock: &sync.Mutex{}, osquerier: osquerier, osquerierRetryInterval: 30 * time.Second, @@ -142,6 +145,9 @@ func NewTufAutoupdater(ctx context.Context, k types.Knapsack, metadataHttpClient return nil, fmt.Errorf("could not init update library manager: %w", err) } + // Subscribe to changes in update-related flags + ta.knapsack.RegisterChangeObserver(ta, keys.UpdateChannel) + return ta, nil } @@ -312,6 +318,32 @@ func (ta *TufAutoupdater) Do(data io.Reader) error { return nil } +// FlagsChanged satisfies the FlagsChangeObserver interface, allowing the autoupdater +// to respond to changes to autoupdate-related settings. +func (ta *TufAutoupdater) FlagsChanged(flagKeys ...keys.FlagKey) { + // No change -- this is the only setting we currently care about. + if ta.updateChannel == ta.knapsack.UpdateChannel() { + return + } + + // Update channel has changed -- update it, then check to see if we + // need to switch versions + ta.slogger.Log(context.TODO(), slog.LevelInfo, + "control server sent down new update channel value", + "new_channel", ta.knapsack.UpdateChannel(), + "old_channel", ta.updateChannel, + ) + ta.updateChannel = ta.knapsack.UpdateChannel() + if err := ta.checkForUpdate(binaries); err != nil { + ta.storeError(err) + ta.slogger.Log(context.TODO(), slog.LevelError, + "error checking for update after switching update channels", + "new_channel", ta.updateChannel, + "err", err, + ) + } +} + // 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() { @@ -462,7 +494,7 @@ func (ta *TufAutoupdater) checkForUpdate(binariesToCheck []autoupdatableBinary) // downloadUpdate will download a new release for the given binary, if available from TUF // and not already downloaded. func (ta *TufAutoupdater) downloadUpdate(binary autoupdatableBinary, targets data.TargetFiles) (string, error) { - release, releaseMetadata, err := findRelease(context.Background(), binary, targets, ta.knapsack.UpdateChannel()) + release, releaseMetadata, err := findRelease(context.Background(), binary, targets, ta.updateChannel) if err != nil { return "", fmt.Errorf("could not find release: %w", err) } diff --git a/ee/tuf/autoupdate_test.go b/ee/tuf/autoupdate_test.go index eae1b17e3..3b17e7916 100644 --- a/ee/tuf/autoupdate_test.go +++ b/ee/tuf/autoupdate_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/Masterminds/semver" + "github.com/kolide/launcher/ee/agent/flags/keys" "github.com/kolide/launcher/ee/agent/storage" storageci "github.com/kolide/launcher/ee/agent/storage/ci" "github.com/kolide/launcher/ee/agent/types" @@ -41,6 +42,8 @@ func TestNewTufAutoupdater(t *testing.T) { mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() _, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, newMockQuerier(t)) require.NoError(t, err, "could not initialize new TUF autoupdater") @@ -79,6 +82,7 @@ func TestExecute_launcherUpdate(t *testing.T) { mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("LocalDevelopmentPath").Return("") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() mockQuerier := newMockQuerier(t) // Set logger so that we can capture output @@ -169,6 +173,7 @@ func TestExecute_osquerydUpdate(t *testing.T) { mockKnapsack.On("TufServerURL").Return(tufServerUrl) mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() mockQuerier := newMockQuerier(t) // Set logger so that we can capture output @@ -243,6 +248,7 @@ func TestExecute_downgrade(t *testing.T) { mockKnapsack.On("TufServerURL").Return(tufServerUrl) mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() mockQuerier := newMockQuerier(t) // Set logger so that we can capture output @@ -328,6 +334,8 @@ func TestExecute_withInitialDelay(t *testing.T) { mockKnapsack.On("TufServerURL").Return(tufServerUrl) mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() mockQuerier := newMockQuerier(t) // Set logger so that we can capture output @@ -393,6 +401,8 @@ func TestInterrupt_Multiple(t *testing.T) { mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() mockQuerier := newMockQuerier(t) // Set up autoupdater @@ -519,6 +529,7 @@ func TestDo(t *testing.T) { mockKnapsack.On("LocalDevelopmentPath").Return("").Maybe() mockQuerier := newMockQuerier(t) mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() // Set up autoupdater autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil })) @@ -585,6 +596,7 @@ func TestDo_HandlesSimultaneousUpdates(t *testing.T) { mockKnapsack.On("LocalDevelopmentPath").Return("") mockQuerier := newMockQuerier(t) mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() // Set up autoupdater autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil })) @@ -640,6 +652,63 @@ func TestDo_HandlesSimultaneousUpdates(t *testing.T) { mockKnapsack.AssertExpectations(t) } +func TestFlagsChanged(t *testing.T) { + t.Parallel() + + testRootDir := t.TempDir() + testReleaseVersion := "2.2.3" + tufServerUrl, rootJson := tufci.InitRemoteTufServer(t, testReleaseVersion) + s := setupStorage(t) + mockKnapsack := typesmocks.NewKnapsack(t) + mockKnapsack.On("RootDirectory").Return(testRootDir) + mockKnapsack.On("AutoupdateErrorsStore").Return(s) + mockKnapsack.On("TufServerURL").Return(tufServerUrl) + mockKnapsack.On("UpdateDirectory").Return("") + mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockKnapsack.On("LocalDevelopmentPath").Return("").Maybe() + mockQuerier := newMockQuerier(t) + mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + + // Start out on beta channel, then swap to nightly + mockKnapsack.On("UpdateChannel").Return("beta").Once() + mockKnapsack.On("UpdateChannel").Return("nightly") + + // Set up autoupdater + autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil })) + require.NoError(t, err, "could not initialize new TUF autoupdater") + require.Equal(t, "beta", autoupdater.updateChannel) + + // Update the metadata client with our test root JSON + require.NoError(t, autoupdater.metadataClient.Init(rootJson), "could not initialize metadata client with test root JSON") + + // Get metadata for each release + _, err = autoupdater.metadataClient.Update() + require.NoError(t, err, "could not update metadata client to fetch target metadata") + + // Expect that we attempt to update the library + mockLibraryManager := NewMocklibrarian(t) + autoupdater.libraryManager = mockLibraryManager + currentOsqueryVersion := "1.1.1" + mockQuerier.On("Query", mock.Anything).Return([]map[string]string{{"version": currentOsqueryVersion}}, nil) + mockLibraryManager.On("Available", binaryOsqueryd, fmt.Sprintf("%s-%s.tar.gz", binaryOsqueryd, testReleaseVersion)).Return(false) + mockLibraryManager.On("AddToLibrary", binaryOsqueryd, mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockLibraryManager.On("Available", binaryLauncher, fmt.Sprintf("%s-%s.tar.gz", binaryLauncher, testReleaseVersion)).Return(false) + mockLibraryManager.On("AddToLibrary", binaryLauncher, mock.Anything, mock.Anything, mock.Anything).Return(nil) + + // Notify that flags changed + autoupdater.FlagsChanged(keys.UpdateChannel) + + // Assert expectation that we added the expected `testReleaseVersion` to the updates library + mockLibraryManager.AssertExpectations(t) + + // Confirm we pulled all config items as expected + mockKnapsack.AssertExpectations(t) + + // Confirm we're on the expected update channel + require.Equal(t, "nightly", autoupdater.updateChannel) +} + func Test_currentRunningVersion_launcher_errorWhenVersionIsNotSet(t *testing.T) { t.Parallel() @@ -711,6 +780,8 @@ func Test_storeError(t *testing.T) { mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() mockQuerier := newMockQuerier(t) autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier) diff --git a/ee/tuf/library_lookup.go b/ee/tuf/library_lookup.go index b361ff59c..d70403a2e 100644 --- a/ee/tuf/library_lookup.go +++ b/ee/tuf/library_lookup.go @@ -10,6 +10,8 @@ import ( "strings" "github.com/Masterminds/semver" + "github.com/kolide/launcher/ee/agent/flags/keys" + "github.com/kolide/launcher/ee/agent/startupsettings" "github.com/kolide/launcher/pkg/autoupdate" "github.com/kolide/launcher/pkg/launcher" "github.com/kolide/launcher/pkg/traces" @@ -32,6 +34,9 @@ type autoupdateConfig struct { // CheckOutLatestWithoutConfig returns information about the latest downloaded executable for our binary, // searching for launcher configuration values in its config file. func CheckOutLatestWithoutConfig(binary autoupdatableBinary, slogger *slog.Logger) (*BinaryUpdateInfo, error) { + ctx, span := traces.StartSpan(context.Background()) + defer span.End() + slogger = slogger.With("component", "tuf_library_lookup") cfg, err := getAutoupdateConfig(os.Args[1:]) if err != nil { @@ -43,7 +48,39 @@ func CheckOutLatestWithoutConfig(binary autoupdatableBinary, slogger *slog.Logge return &BinaryUpdateInfo{Path: cfg.localDevelopmentPath}, nil } - return CheckOutLatest(context.Background(), binary, cfg.rootDirectory, cfg.updateDirectory, cfg.channel, slogger) + // Get update channel from startup settings + updateChannel, err := getUpdateChannelFromStartupSettings(ctx, cfg.rootDirectory) + if err != nil { + slogger.Log(ctx, slog.LevelWarn, + "could not get update channel from startup settings, falling back to config value instead", + "config_update_channel", cfg.channel, + "err", err, + ) + updateChannel = cfg.channel + } + + return CheckOutLatest(ctx, binary, cfg.rootDirectory, cfg.updateDirectory, updateChannel, slogger) +} + +// getUpdateChannelFromStartupSettings queries the startup settings database to fetch the desired +// update channel. This accounts for e.g. the control server sending down a particular value for +// the update channel, overriding the config file. +func getUpdateChannelFromStartupSettings(ctx context.Context, rootDirectory string) (string, error) { + ctx, span := traces.StartSpan(ctx) + defer span.End() + + r, err := startupsettings.OpenReader(ctx, rootDirectory) + if err != nil { + return "", fmt.Errorf("opening startupsettings reader: %w", err) + } + defer r.Close() + + updateChannel, err := r.Get(keys.UpdateChannel.String()) + if err != nil { + return "", fmt.Errorf("getting update channel from startupsettings: %w", err) + } + + return updateChannel, nil } // getAutoupdateConfig pulls the configuration values necessary to work with the autoupdate library diff --git a/ee/tuf/library_lookup_test.go b/ee/tuf/library_lookup_test.go index 39ced74a0..4274ae887 100644 --- a/ee/tuf/library_lookup_test.go +++ b/ee/tuf/library_lookup_test.go @@ -7,11 +7,43 @@ import ( "path/filepath" "testing" + "github.com/kolide/launcher/ee/agent/flags/keys" + agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite" tufci "github.com/kolide/launcher/ee/tuf/ci" "github.com/kolide/launcher/pkg/log/multislogger" "github.com/stretchr/testify/require" ) +func Test_getUpdateChannelFromStartupSettings(t *testing.T) { + t.Parallel() + + expectedChannel := "beta" + + // Set up an override for the channel in the startupsettings db + rootDir := t.TempDir() + store, err := agentsqlite.OpenRW(context.TODO(), rootDir, agentsqlite.StartupSettingsStore) + require.NoError(t, err, "setting up db connection") + require.NoError(t, store.Set([]byte(keys.UpdateChannel.String()), []byte(expectedChannel)), "setting key") + require.NoError(t, store.Close(), "closing test db") + + actualChannel, err := getUpdateChannelFromStartupSettings(context.TODO(), rootDir) + require.NoError(t, err, "did not expect error getting update channel from startup settings") + require.Equal(t, expectedChannel, actualChannel, "did not get expected channel") +} + +func Test_getUpdateChannelFromStartupSettings_NotFound(t *testing.T) { + t.Parallel() + + // Create a startupsettings db but don't set anything in it + rootDir := t.TempDir() + store, err := agentsqlite.OpenRW(context.TODO(), rootDir, agentsqlite.StartupSettingsStore) + require.NoError(t, err, "setting up db connection") + require.NoError(t, store.Close(), "closing test db") + + _, err = getUpdateChannelFromStartupSettings(context.TODO(), rootDir) + require.Error(t, err, "should not have been able to get update channel when it is not set") +} + func TestCheckOutLatest_withTufRepository(t *testing.T) { t.Parallel() @@ -45,7 +77,7 @@ func TestCheckOutLatest_withTufRepository(t *testing.T) { require.NoError(t, os.Chmod(tooRecentPath, 0755)) // Check it - latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.New().Logger) + latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.NewNopLogger()) require.NoError(t, err, "unexpected error on checking out latest") require.Equal(t, executablePath, latest.Path) require.Equal(t, executableVersion, latest.Version) @@ -72,7 +104,7 @@ func TestCheckOutLatest_withoutTufRepository(t *testing.T) { require.NoError(t, err, "did not make test binary") // Check it - latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.New().Logger) + latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.NewNopLogger()) require.NoError(t, err, "unexpected error on checking out latest") require.Equal(t, executablePath, latest.Path) require.Equal(t, executableVersion, latest.Version)