Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control server can set update channel for autoupdater #1628

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion ee/tuf/autoupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import (
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"sync"
"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"
Expand Down Expand Up @@ -86,6 +88,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
Expand Down Expand Up @@ -115,6 +118,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,
Expand Down Expand Up @@ -142,6 +146,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
}

Expand Down Expand Up @@ -312,6 +319,36 @@ 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) {
if !slices.Contains(flagKeys, keys.UpdateChannel) {
return
}
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved

// No change
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() {
Expand Down Expand Up @@ -462,7 +499,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)
}
Expand Down
71 changes: 71 additions & 0 deletions ee/tuf/autoupdate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }))
Expand Down Expand Up @@ -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 }))
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down
39 changes: 38 additions & 1 deletion ee/tuf/library_lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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
Expand Down
36 changes: 34 additions & 2 deletions ee/tuf/library_lookup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading