Skip to content

Commit fba8393

Browse files
Control server can set update channel for autoupdater (#1628)
1 parent d8744bc commit fba8393

File tree

4 files changed

+176
-4
lines changed

4 files changed

+176
-4
lines changed

ee/tuf/autoupdate.go

+33-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"time"
2222

2323
"github.com/kolide/kit/version"
24+
"github.com/kolide/launcher/ee/agent/flags/keys"
2425
"github.com/kolide/launcher/ee/agent/types"
2526
"github.com/kolide/launcher/pkg/traces"
2627
client "github.com/theupdateframework/go-tuf/client"
@@ -86,6 +87,7 @@ type TufAutoupdater struct {
8687
osquerierRetryInterval time.Duration
8788
knapsack types.Knapsack
8889
store types.KVStore // stores autoupdater errors for kolide_tuf_autoupdater_errors table
90+
updateChannel string
8991
updateLock *sync.Mutex
9092
interrupt chan struct{}
9193
interrupted bool
@@ -115,6 +117,7 @@ func NewTufAutoupdater(ctx context.Context, k types.Knapsack, metadataHttpClient
115117
interrupt: make(chan struct{}, 1),
116118
signalRestart: make(chan error, 1),
117119
store: k.AutoupdateErrorsStore(),
120+
updateChannel: k.UpdateChannel(),
118121
updateLock: &sync.Mutex{},
119122
osquerier: osquerier,
120123
osquerierRetryInterval: 30 * time.Second,
@@ -142,6 +145,9 @@ func NewTufAutoupdater(ctx context.Context, k types.Knapsack, metadataHttpClient
142145
return nil, fmt.Errorf("could not init update library manager: %w", err)
143146
}
144147

148+
// Subscribe to changes in update-related flags
149+
ta.knapsack.RegisterChangeObserver(ta, keys.UpdateChannel)
150+
145151
return ta, nil
146152
}
147153

@@ -312,6 +318,32 @@ func (ta *TufAutoupdater) Do(data io.Reader) error {
312318
return nil
313319
}
314320

321+
// FlagsChanged satisfies the FlagsChangeObserver interface, allowing the autoupdater
322+
// to respond to changes to autoupdate-related settings.
323+
func (ta *TufAutoupdater) FlagsChanged(flagKeys ...keys.FlagKey) {
324+
// No change -- this is the only setting we currently care about.
325+
if ta.updateChannel == ta.knapsack.UpdateChannel() {
326+
return
327+
}
328+
329+
// Update channel has changed -- update it, then check to see if we
330+
// need to switch versions
331+
ta.slogger.Log(context.TODO(), slog.LevelInfo,
332+
"control server sent down new update channel value",
333+
"new_channel", ta.knapsack.UpdateChannel(),
334+
"old_channel", ta.updateChannel,
335+
)
336+
ta.updateChannel = ta.knapsack.UpdateChannel()
337+
if err := ta.checkForUpdate(binaries); err != nil {
338+
ta.storeError(err)
339+
ta.slogger.Log(context.TODO(), slog.LevelError,
340+
"error checking for update after switching update channels",
341+
"new_channel", ta.updateChannel,
342+
"err", err,
343+
)
344+
}
345+
}
346+
315347
// tidyLibrary gets the current running version for each binary (so that the current version is not removed)
316348
// and then asks the update library manager to tidy the update library.
317349
func (ta *TufAutoupdater) tidyLibrary() {
@@ -462,7 +494,7 @@ func (ta *TufAutoupdater) checkForUpdate(binariesToCheck []autoupdatableBinary)
462494
// downloadUpdate will download a new release for the given binary, if available from TUF
463495
// and not already downloaded.
464496
func (ta *TufAutoupdater) downloadUpdate(binary autoupdatableBinary, targets data.TargetFiles) (string, error) {
465-
release, releaseMetadata, err := findRelease(context.Background(), binary, targets, ta.knapsack.UpdateChannel())
497+
release, releaseMetadata, err := findRelease(context.Background(), binary, targets, ta.updateChannel)
466498
if err != nil {
467499
return "", fmt.Errorf("could not find release: %w", err)
468500
}

ee/tuf/autoupdate_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"time"
1919

2020
"github.com/Masterminds/semver"
21+
"github.com/kolide/launcher/ee/agent/flags/keys"
2122
"github.com/kolide/launcher/ee/agent/storage"
2223
storageci "github.com/kolide/launcher/ee/agent/storage/ci"
2324
"github.com/kolide/launcher/ee/agent/types"
@@ -41,6 +42,8 @@ func TestNewTufAutoupdater(t *testing.T) {
4142
mockKnapsack.On("UpdateDirectory").Return("")
4243
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
4344
mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger())
45+
mockKnapsack.On("UpdateChannel").Return("nightly")
46+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
4447

4548
_, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, newMockQuerier(t))
4649
require.NoError(t, err, "could not initialize new TUF autoupdater")
@@ -79,6 +82,7 @@ func TestExecute_launcherUpdate(t *testing.T) {
7982
mockKnapsack.On("UpdateDirectory").Return("")
8083
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
8184
mockKnapsack.On("LocalDevelopmentPath").Return("")
85+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
8286
mockQuerier := newMockQuerier(t)
8387

8488
// Set logger so that we can capture output
@@ -169,6 +173,7 @@ func TestExecute_osquerydUpdate(t *testing.T) {
169173
mockKnapsack.On("TufServerURL").Return(tufServerUrl)
170174
mockKnapsack.On("UpdateDirectory").Return("")
171175
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
176+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
172177
mockQuerier := newMockQuerier(t)
173178

174179
// Set logger so that we can capture output
@@ -243,6 +248,7 @@ func TestExecute_downgrade(t *testing.T) {
243248
mockKnapsack.On("TufServerURL").Return(tufServerUrl)
244249
mockKnapsack.On("UpdateDirectory").Return("")
245250
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
251+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
246252
mockQuerier := newMockQuerier(t)
247253

248254
// Set logger so that we can capture output
@@ -328,6 +334,8 @@ func TestExecute_withInitialDelay(t *testing.T) {
328334
mockKnapsack.On("TufServerURL").Return(tufServerUrl)
329335
mockKnapsack.On("UpdateDirectory").Return("")
330336
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
337+
mockKnapsack.On("UpdateChannel").Return("nightly")
338+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
331339
mockQuerier := newMockQuerier(t)
332340

333341
// Set logger so that we can capture output
@@ -393,6 +401,8 @@ func TestInterrupt_Multiple(t *testing.T) {
393401
mockKnapsack.On("UpdateDirectory").Return("")
394402
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
395403
mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger())
404+
mockKnapsack.On("UpdateChannel").Return("nightly")
405+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
396406
mockQuerier := newMockQuerier(t)
397407

398408
// Set up autoupdater
@@ -519,6 +529,7 @@ func TestDo(t *testing.T) {
519529
mockKnapsack.On("LocalDevelopmentPath").Return("").Maybe()
520530
mockQuerier := newMockQuerier(t)
521531
mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger())
532+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
522533

523534
// Set up autoupdater
524535
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) {
585596
mockKnapsack.On("LocalDevelopmentPath").Return("")
586597
mockQuerier := newMockQuerier(t)
587598
mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger())
599+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
588600

589601
// Set up autoupdater
590602
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) {
640652
mockKnapsack.AssertExpectations(t)
641653
}
642654

655+
func TestFlagsChanged(t *testing.T) {
656+
t.Parallel()
657+
658+
testRootDir := t.TempDir()
659+
testReleaseVersion := "2.2.3"
660+
tufServerUrl, rootJson := tufci.InitRemoteTufServer(t, testReleaseVersion)
661+
s := setupStorage(t)
662+
mockKnapsack := typesmocks.NewKnapsack(t)
663+
mockKnapsack.On("RootDirectory").Return(testRootDir)
664+
mockKnapsack.On("AutoupdateErrorsStore").Return(s)
665+
mockKnapsack.On("TufServerURL").Return(tufServerUrl)
666+
mockKnapsack.On("UpdateDirectory").Return("")
667+
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
668+
mockKnapsack.On("LocalDevelopmentPath").Return("").Maybe()
669+
mockQuerier := newMockQuerier(t)
670+
mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger())
671+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
672+
673+
// Start out on beta channel, then swap to nightly
674+
mockKnapsack.On("UpdateChannel").Return("beta").Once()
675+
mockKnapsack.On("UpdateChannel").Return("nightly")
676+
677+
// Set up autoupdater
678+
autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil }))
679+
require.NoError(t, err, "could not initialize new TUF autoupdater")
680+
require.Equal(t, "beta", autoupdater.updateChannel)
681+
682+
// Update the metadata client with our test root JSON
683+
require.NoError(t, autoupdater.metadataClient.Init(rootJson), "could not initialize metadata client with test root JSON")
684+
685+
// Get metadata for each release
686+
_, err = autoupdater.metadataClient.Update()
687+
require.NoError(t, err, "could not update metadata client to fetch target metadata")
688+
689+
// Expect that we attempt to update the library
690+
mockLibraryManager := NewMocklibrarian(t)
691+
autoupdater.libraryManager = mockLibraryManager
692+
currentOsqueryVersion := "1.1.1"
693+
mockQuerier.On("Query", mock.Anything).Return([]map[string]string{{"version": currentOsqueryVersion}}, nil)
694+
mockLibraryManager.On("Available", binaryOsqueryd, fmt.Sprintf("%s-%s.tar.gz", binaryOsqueryd, testReleaseVersion)).Return(false)
695+
mockLibraryManager.On("AddToLibrary", binaryOsqueryd, mock.Anything, mock.Anything, mock.Anything).Return(nil)
696+
mockLibraryManager.On("Available", binaryLauncher, fmt.Sprintf("%s-%s.tar.gz", binaryLauncher, testReleaseVersion)).Return(false)
697+
mockLibraryManager.On("AddToLibrary", binaryLauncher, mock.Anything, mock.Anything, mock.Anything).Return(nil)
698+
699+
// Notify that flags changed
700+
autoupdater.FlagsChanged(keys.UpdateChannel)
701+
702+
// Assert expectation that we added the expected `testReleaseVersion` to the updates library
703+
mockLibraryManager.AssertExpectations(t)
704+
705+
// Confirm we pulled all config items as expected
706+
mockKnapsack.AssertExpectations(t)
707+
708+
// Confirm we're on the expected update channel
709+
require.Equal(t, "nightly", autoupdater.updateChannel)
710+
}
711+
643712
func Test_currentRunningVersion_launcher_errorWhenVersionIsNotSet(t *testing.T) {
644713
t.Parallel()
645714

@@ -711,6 +780,8 @@ func Test_storeError(t *testing.T) {
711780
mockKnapsack.On("UpdateDirectory").Return("")
712781
mockKnapsack.On("MirrorServerURL").Return("https://example.com")
713782
mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger())
783+
mockKnapsack.On("UpdateChannel").Return("nightly")
784+
mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return()
714785
mockQuerier := newMockQuerier(t)
715786

716787
autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier)

ee/tuf/library_lookup.go

+38-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"strings"
1111

1212
"github.com/Masterminds/semver"
13+
"github.com/kolide/launcher/ee/agent/flags/keys"
14+
"github.com/kolide/launcher/ee/agent/startupsettings"
1315
"github.com/kolide/launcher/pkg/autoupdate"
1416
"github.com/kolide/launcher/pkg/launcher"
1517
"github.com/kolide/launcher/pkg/traces"
@@ -32,6 +34,9 @@ type autoupdateConfig struct {
3234
// CheckOutLatestWithoutConfig returns information about the latest downloaded executable for our binary,
3335
// searching for launcher configuration values in its config file.
3436
func CheckOutLatestWithoutConfig(binary autoupdatableBinary, slogger *slog.Logger) (*BinaryUpdateInfo, error) {
37+
ctx, span := traces.StartSpan(context.Background())
38+
defer span.End()
39+
3540
slogger = slogger.With("component", "tuf_library_lookup")
3641
cfg, err := getAutoupdateConfig(os.Args[1:])
3742
if err != nil {
@@ -43,7 +48,39 @@ func CheckOutLatestWithoutConfig(binary autoupdatableBinary, slogger *slog.Logge
4348
return &BinaryUpdateInfo{Path: cfg.localDevelopmentPath}, nil
4449
}
4550

46-
return CheckOutLatest(context.Background(), binary, cfg.rootDirectory, cfg.updateDirectory, cfg.channel, slogger)
51+
// Get update channel from startup settings
52+
updateChannel, err := getUpdateChannelFromStartupSettings(ctx, cfg.rootDirectory)
53+
if err != nil {
54+
slogger.Log(ctx, slog.LevelWarn,
55+
"could not get update channel from startup settings, falling back to config value instead",
56+
"config_update_channel", cfg.channel,
57+
"err", err,
58+
)
59+
updateChannel = cfg.channel
60+
}
61+
62+
return CheckOutLatest(ctx, binary, cfg.rootDirectory, cfg.updateDirectory, updateChannel, slogger)
63+
}
64+
65+
// getUpdateChannelFromStartupSettings queries the startup settings database to fetch the desired
66+
// update channel. This accounts for e.g. the control server sending down a particular value for
67+
// the update channel, overriding the config file.
68+
func getUpdateChannelFromStartupSettings(ctx context.Context, rootDirectory string) (string, error) {
69+
ctx, span := traces.StartSpan(ctx)
70+
defer span.End()
71+
72+
r, err := startupsettings.OpenReader(ctx, rootDirectory)
73+
if err != nil {
74+
return "", fmt.Errorf("opening startupsettings reader: %w", err)
75+
}
76+
defer r.Close()
77+
78+
updateChannel, err := r.Get(keys.UpdateChannel.String())
79+
if err != nil {
80+
return "", fmt.Errorf("getting update channel from startupsettings: %w", err)
81+
}
82+
83+
return updateChannel, nil
4784
}
4885

4986
// getAutoupdateConfig pulls the configuration values necessary to work with the autoupdate library

ee/tuf/library_lookup_test.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,43 @@ import (
77
"path/filepath"
88
"testing"
99

10+
"github.com/kolide/launcher/ee/agent/flags/keys"
11+
agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite"
1012
tufci "github.com/kolide/launcher/ee/tuf/ci"
1113
"github.com/kolide/launcher/pkg/log/multislogger"
1214
"github.com/stretchr/testify/require"
1315
)
1416

17+
func Test_getUpdateChannelFromStartupSettings(t *testing.T) {
18+
t.Parallel()
19+
20+
expectedChannel := "beta"
21+
22+
// Set up an override for the channel in the startupsettings db
23+
rootDir := t.TempDir()
24+
store, err := agentsqlite.OpenRW(context.TODO(), rootDir, agentsqlite.StartupSettingsStore)
25+
require.NoError(t, err, "setting up db connection")
26+
require.NoError(t, store.Set([]byte(keys.UpdateChannel.String()), []byte(expectedChannel)), "setting key")
27+
require.NoError(t, store.Close(), "closing test db")
28+
29+
actualChannel, err := getUpdateChannelFromStartupSettings(context.TODO(), rootDir)
30+
require.NoError(t, err, "did not expect error getting update channel from startup settings")
31+
require.Equal(t, expectedChannel, actualChannel, "did not get expected channel")
32+
}
33+
34+
func Test_getUpdateChannelFromStartupSettings_NotFound(t *testing.T) {
35+
t.Parallel()
36+
37+
// Create a startupsettings db but don't set anything in it
38+
rootDir := t.TempDir()
39+
store, err := agentsqlite.OpenRW(context.TODO(), rootDir, agentsqlite.StartupSettingsStore)
40+
require.NoError(t, err, "setting up db connection")
41+
require.NoError(t, store.Close(), "closing test db")
42+
43+
_, err = getUpdateChannelFromStartupSettings(context.TODO(), rootDir)
44+
require.Error(t, err, "should not have been able to get update channel when it is not set")
45+
}
46+
1547
func TestCheckOutLatest_withTufRepository(t *testing.T) {
1648
t.Parallel()
1749

@@ -45,7 +77,7 @@ func TestCheckOutLatest_withTufRepository(t *testing.T) {
4577
require.NoError(t, os.Chmod(tooRecentPath, 0755))
4678

4779
// Check it
48-
latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.New().Logger)
80+
latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.NewNopLogger())
4981
require.NoError(t, err, "unexpected error on checking out latest")
5082
require.Equal(t, executablePath, latest.Path)
5183
require.Equal(t, executableVersion, latest.Version)
@@ -72,7 +104,7 @@ func TestCheckOutLatest_withoutTufRepository(t *testing.T) {
72104
require.NoError(t, err, "did not make test binary")
73105

74106
// Check it
75-
latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.New().Logger)
107+
latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.NewNopLogger())
76108
require.NoError(t, err, "unexpected error on checking out latest")
77109
require.Equal(t, executablePath, latest.Path)
78110
require.Equal(t, executableVersion, latest.Version)

0 commit comments

Comments
 (0)