From f230311b725434859d65326f8fe03265e21e8537 Mon Sep 17 00:00:00 2001 From: Rebecca Mahany-Horton Date: Wed, 14 Feb 2024 11:57:22 -0500 Subject: [PATCH] Remove legacy autoupdater (#1600) --- .../internal/updater/mocks/updater.go | 60 --- .../internal/updater/updater-finalizer.go | 51 --- .../updater/updater-finalizer_windows.go | 42 -- cmd/launcher/internal/updater/updater.go | 173 -------- cmd/launcher/internal/updater/updater_test.go | 291 -------------- pkg/autoupdate/autoupdate.go | 374 +----------------- pkg/autoupdate/autoupdate_test.go | 211 ---------- pkg/autoupdate/errors.go | 22 -- pkg/autoupdate/errors_test.go | 21 - pkg/autoupdate/handler.go | 139 ------- pkg/autoupdate/mocklogger_test.go | 25 -- 11 files changed, 1 insertion(+), 1408 deletions(-) delete mode 100644 cmd/launcher/internal/updater/mocks/updater.go delete mode 100644 cmd/launcher/internal/updater/updater-finalizer.go delete mode 100644 cmd/launcher/internal/updater/updater-finalizer_windows.go delete mode 100644 cmd/launcher/internal/updater/updater.go delete mode 100644 cmd/launcher/internal/updater/updater_test.go delete mode 100644 pkg/autoupdate/errors.go delete mode 100644 pkg/autoupdate/errors_test.go delete mode 100644 pkg/autoupdate/handler.go delete mode 100644 pkg/autoupdate/mocklogger_test.go diff --git a/cmd/launcher/internal/updater/mocks/updater.go b/cmd/launcher/internal/updater/mocks/updater.go deleted file mode 100644 index 6e8743022..000000000 --- a/cmd/launcher/internal/updater/mocks/updater.go +++ /dev/null @@ -1,60 +0,0 @@ -// Code generated by mockery v2.21.1. DO NOT EDIT. - -package mocks - -import ( - tuf "github.com/kolide/updater/tuf" - mock "github.com/stretchr/testify/mock" -) - -// Updater is an autogenerated mock type for the updater type -type Updater struct { - mock.Mock -} - -// Run provides a mock function with given fields: opts -func (_m *Updater) Run(opts ...tuf.Option) (func(), error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - var r0 func() - var r1 error - if rf, ok := ret.Get(0).(func(...tuf.Option) (func(), error)); ok { - return rf(opts...) - } - if rf, ok := ret.Get(0).(func(...tuf.Option) func()); ok { - r0 = rf(opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(func()) - } - } - - if rf, ok := ret.Get(1).(func(...tuf.Option) error); ok { - r1 = rf(opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewUpdater interface { - mock.TestingT - Cleanup(func()) -} - -// NewUpdater creates a new instance of Updater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewUpdater(t mockConstructorTestingTNewUpdater) *Updater { - mock := &Updater{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/cmd/launcher/internal/updater/updater-finalizer.go b/cmd/launcher/internal/updater/updater-finalizer.go deleted file mode 100644 index cfbae8ec0..000000000 --- a/cmd/launcher/internal/updater/updater-finalizer.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build !windows -// +build !windows - -package updater - -import ( - "context" - "fmt" - "os" - "syscall" - - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" - "github.com/kolide/launcher/pkg/autoupdate" - "github.com/kolide/launcher/pkg/contexts/ctxlog" -) - -// UpdateFinalizer finalizes a launcher update. It assume the new -// binary has been copied into place, and calls exec, so we start a -// new running launcher in our place. -func UpdateFinalizer(logger log.Logger, shutdownOsquery func() error) func() error { - return func() error { - if err := shutdownOsquery(); err != nil { - level.Info(logger).Log("method", "updateFinalizer", "err", err) - level.Debug(logger).Log("method", "updateFinalizer", "err", err, "stack", fmt.Sprintf("%+v", err)) - } - // find the newest version of launcher on disk. - // FindNewestSelf uses context as a way to get a - // logger, so we need to create and pass one. - binaryPath, err := autoupdate.FindNewestSelf( - ctxlog.NewContext(context.TODO(), logger), - autoupdate.DeleteCorruptUpdates(), - autoupdate.DeleteOldUpdates(), - ) - - if err != nil { - level.Info(logger).Log("method", "updateFinalizer", "err", err) - return fmt.Errorf("finding newest: %w", err) - } - - // replace launcher - level.Info(logger).Log( - "msg", "Exec updated launcher", - "newPath", binaryPath, - ) - if err := syscall.Exec(binaryPath, os.Args, os.Environ()); err != nil { - return fmt.Errorf("exec updated launcher: %w", err) - } - return nil - } -} diff --git a/cmd/launcher/internal/updater/updater-finalizer_windows.go b/cmd/launcher/internal/updater/updater-finalizer_windows.go deleted file mode 100644 index 8ec0760ba..000000000 --- a/cmd/launcher/internal/updater/updater-finalizer_windows.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build windows -// +build windows - -package updater - -import ( - "context" - "fmt" - - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" - "github.com/kolide/launcher/pkg/autoupdate" - "github.com/kolide/launcher/pkg/contexts/ctxlog" -) - -// UpdateFinalizer finalizes a launcher update. As windows does not -// support an exec, we exit so the service manager will restart -// us. Exit(0) might be more correct, but that's harder to plumb -// through this stack. So, return an error here to trigger an exit -// higher in the stack. -func UpdateFinalizer(logger log.Logger, shutdownOsquery func() error) func() error { - return func() error { - if err := shutdownOsquery(); err != nil { - level.Info(logger).Log("msg", "calling shutdownOsquery", "method", "updateFinalizer", "err", err) - level.Debug(logger).Log("msg", "calling shutdownOsquery", "method", "updateFinalizer", "err", err, "stack", fmt.Sprintf("%+v", err)) - } - - // 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. - autoupdate.FindNewestSelf( - ctxlog.NewContext(context.TODO(), logger), - autoupdate.DeleteCorruptUpdates(), - autoupdate.DeleteOldUpdates(), - ) - - level.Info(logger).Log("msg", "Exiting launcher to allow a service manager to start the new one") - return autoupdate.NewLauncherRestartNeededErr("Exiting launcher to allow a service manager restart") - } -} diff --git a/cmd/launcher/internal/updater/updater.go b/cmd/launcher/internal/updater/updater.go deleted file mode 100644 index 8b2fe8dd3..000000000 --- a/cmd/launcher/internal/updater/updater.go +++ /dev/null @@ -1,173 +0,0 @@ -package updater - -import ( - "context" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" - "github.com/kolide/kit/actor" - "github.com/kolide/launcher/pkg/autoupdate" - "github.com/kolide/launcher/pkg/traces" - "github.com/kolide/updater/tuf" -) - -// UpdaterConfig is a struct of update related options. It's used to -// simplify the call to `createUpdater` from launcher's main blocks. -type UpdaterConfig struct { - Logger log.Logger - RootDirectory string // launcher's root dir. use for holding tuf staging and updates - AutoupdateInterval time.Duration - UpdateChannel autoupdate.UpdateChannel - InitialDelay time.Duration // start delay, to avoid whomping critical early data - NotaryURL string - MirrorURL string - NotaryPrefix string - HTTPClient *http.Client - SigChannel chan os.Signal -} - -// NewUpdater returns an Actor suitable for a pkg/rungroup group. It -// is a light wrapper around autoupdate.NewUpdater to simplify having -// multiple ones in launcher. -func NewUpdater( - ctx context.Context, - binaryPath string, - finalizer autoupdate.UpdateFinalizer, - config *UpdaterConfig, -) (*actor.Actor, error) { - ctx, span := traces.StartSpan(ctx) - defer span.End() - - if config.Logger == nil { - config.Logger = log.NewNopLogger() - } - - config.Logger = log.With(config.Logger, "updater", filepath.Base(binaryPath)) - - // create the updater - updater, err := autoupdate.NewUpdater( - binaryPath, - config.RootDirectory, - autoupdate.WithLogger(config.Logger), - autoupdate.WithHTTPClient(config.HTTPClient), - autoupdate.WithNotaryURL(config.NotaryURL), - autoupdate.WithMirrorURL(config.MirrorURL), - autoupdate.WithNotaryPrefix(config.NotaryPrefix), - autoupdate.WithFinalizer(finalizer), - autoupdate.WithUpdateChannel(config.UpdateChannel), - autoupdate.WithSigChannel(config.SigChannel), - ) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithCancel(ctx) - - updateCmd := &updaterCmd{ - updater: updater, - ctx: ctx, // nolint:containedctx - cancel: cancel, - stopChan: make(chan bool), - config: config, - runUpdaterRetryInterval: 30 * time.Minute, - } - - return &actor.Actor{ - Execute: updateCmd.execute, - Interrupt: updateCmd.interrupt, - }, nil -} - -// updater allows us to mock *autoupdate.Updater during testing -type updater interface { - Run(opts ...tuf.Option) (stop func(), err error) -} - -type updaterCmd struct { - updater updater - ctx context.Context // nolint:containedctx - cancel context.CancelFunc - stopChan chan bool - stopExecution func() - stopped bool - config *UpdaterConfig - runUpdaterRetryInterval time.Duration -} - -func (u *updaterCmd) execute() error { - // When launcher first starts, we'd like the - // server to start receiving data - // immediately. But, if updater is trying to - // run, this creates an awkward pause for restart. - // So, delay starting updates by an hour or two. - level.Debug(u.config.Logger).Log("msg", "updater entering initial delay", "delay", u.config.InitialDelay) - - select { - case <-u.stopChan: - level.Debug(u.config.Logger).Log("msg", "updater stopped requested during initial delay, breaking loop") - return nil - case <-time.After(u.config.InitialDelay): - level.Debug(u.config.Logger).Log("msg", "updater initial delay complete") - break - } - - // Failing to start the updater is not a fatal launcher - // error. If there's a problem, sleep and try - // again. Implementing this is a bit gnarly. In the event of a - // success, we get a nil error, and a stop function. But I don't - // see a simple way to ensure the updater is still running in - // the background. - for { - level.Debug(u.config.Logger).Log("msg", "updater starting") - - // run the updater and set the stop function so that the interrupt has access to it - stop, err := u.updater.Run(tuf.WithFrequency(u.config.AutoupdateInterval), tuf.WithLogger(u.config.Logger)) - u.stopExecution = stop - if err == nil { - break - } - - // err != nil, log it and loop again - level.Error(u.config.Logger).Log("msg", "error running updater", "err", err) - select { - case <-u.stopChan: - level.Debug(u.config.Logger).Log("msg", "updater stop requested, Breaking loop") - return nil - case <-time.After(u.runUpdaterRetryInterval): - break - } - } - - level.Debug(u.config.Logger).Log("msg", "updater waiting ... just sitting until done signal") - <-u.ctx.Done() - - return nil -} - -func (u *updaterCmd) interrupt(_ error) { - // Only perform shutdown tasks on first call to interrupt -- no need to repeat on potential extra calls. - if u.stopped { - return - } - u.stopped = true - - level.Info(u.config.Logger).Log("msg", "updater interrupted") - - // non-blocking channel send - select { - case u.stopChan <- true: - level.Info(u.config.Logger).Log("msg", "updater interrupt sent signal over stop channel") - default: - level.Info(u.config.Logger).Log("msg", "updater interrupt without sending signal over stop channel (no one to receive)") - } - - if u.stopExecution != nil { - u.stopExecution() - } - - u.cancel() -} diff --git a/cmd/launcher/internal/updater/updater_test.go b/cmd/launcher/internal/updater/updater_test.go deleted file mode 100644 index a7b874fed..000000000 --- a/cmd/launcher/internal/updater/updater_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package updater - -import ( - "context" - "errors" - "os" - "sync" - "testing" - "time" - - "github.com/go-kit/kit/log" - "github.com/kolide/launcher/cmd/launcher/internal/updater/mocks" - "github.com/kolide/updater/tuf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func Test_updaterCmd_execute(t *testing.T) { - t.Parallel() - - type fields struct { - // Mock generated with `mockery --name updater --exported` - updater *mocks.Updater - stopChan chan bool - config *UpdaterConfig - runUpdaterRetryInterval time.Duration - } - tests := []struct { - name string - fields fields - // in this test, the calls to run are the only thing we can really assert against - // leave this field empty and the test will fail if there is a call to run function made - // add 3 funcs here and the test will expect updater.Run() to be called 3 times - updaterRunReturns []func(opts ...tuf.Option) (stop func(), err error) - callStopChanAfter time.Duration - assertion assert.ErrorAssertionFunc - }{ - { - name: "success", - fields: fields{ - updater: &mocks.Updater{}, - config: &UpdaterConfig{ - Logger: log.NewNopLogger(), - }, - }, - updaterRunReturns: []func(opts ...tuf.Option) (stop func(), err error){ - func(opts ...tuf.Option) (stop func(), err error) { - return func() {}, nil - }, - }, - assertion: assert.NoError, - }, - { - name: "multiple_run_retries", - fields: fields{ - updater: &mocks.Updater{}, - config: &UpdaterConfig{ - Logger: log.NewNopLogger(), - }, - runUpdaterRetryInterval: time.Millisecond, - }, - updaterRunReturns: []func(opts ...tuf.Option) (stop func(), err error){ - func(opts ...tuf.Option) (stop func(), err error) { - return nil, errors.New("some error") - }, - func(opts ...tuf.Option) (stop func(), err error) { - return nil, errors.New("some error") - }, - func(opts ...tuf.Option) (stop func(), err error) { - return func() {}, nil - }, - }, - assertion: assert.NoError, - }, - { - name: "stop_during_initial_delay", - fields: fields{ - updater: &mocks.Updater{}, - stopChan: make(chan bool), - config: &UpdaterConfig{ - Logger: log.NewNopLogger(), - InitialDelay: 200 * time.Millisecond, - }, - }, - callStopChanAfter: time.Millisecond, - assertion: assert.NoError, - }, - { - name: "stop_during_retry_loop", - fields: fields{ - updater: &mocks.Updater{}, - stopChan: make(chan bool), - config: &UpdaterConfig{ - Logger: log.NewNopLogger(), - }, - runUpdaterRetryInterval: 1 * time.Second, - }, - updaterRunReturns: []func(opts ...tuf.Option) (stop func(), err error){ - func(opts ...tuf.Option) (stop func(), err error) { - return nil, errors.New("some error") - }, - }, - callStopChanAfter: 5 * time.Millisecond, - assertion: assert.NoError, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - ctx, cancelCtx := context.WithTimeout(context.Background(), 0) - defer cancelCtx() - - u := &updaterCmd{ - updater: tt.fields.updater, - ctx: ctx, - cancel: cancelCtx, - stopChan: tt.fields.stopChan, - config: tt.fields.config, - runUpdaterRetryInterval: tt.fields.runUpdaterRetryInterval, - } - - var wg sync.WaitGroup - if tt.callStopChanAfter > 0 { - wg.Add(1) - go func() { - time.Sleep(tt.callStopChanAfter) - tt.fields.stopChan <- true - wg.Done() - }() - } - - if tt.updaterRunReturns != nil { - for _, returnFunc := range tt.updaterRunReturns { - tt.fields.updater.On("Run", mock.AnythingOfType("tuf.Option"), mock.AnythingOfType("tuf.Option")).Return(returnFunc()).Once() - } - } - - tt.assertion(t, u.execute()) - tt.fields.updater.AssertExpectations(t) - - // test will time out if we don't get to send something on u.stopChan when expecting channel receive - wg.Wait() - }) - } -} - -func Test_updaterCmd_interrupt(t *testing.T) { - t.Parallel() - - type fields struct { - stopChan chan bool - config *UpdaterConfig - } - type args struct { - err error - } - tests := []struct { - name string - fields fields - args args - expectStopChannelReceive bool - expectedCallsToStop int - }{ - { - name: "default_interrupt", - fields: fields{ - stopChan: make(chan bool), - config: &UpdaterConfig{ - Logger: log.NewNopLogger(), - }, - }, - args: args{ - err: errors.New("some error"), - }, - expectedCallsToStop: 1, - }, - { - name: "channel_send_interrupt", - fields: fields{ - stopChan: make(chan bool), - config: &UpdaterConfig{ - Logger: log.NewNopLogger(), - }, - }, - args: args{ - err: errors.New("some error"), - }, - expectedCallsToStop: 1, - expectStopChannelReceive: true, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(context.Background()) - - u := &updaterCmd{ - stopChan: tt.fields.stopChan, - config: tt.fields.config, - ctx: ctx, - cancel: cancel, - } - - // using this wait group to ensure that something gets received on u.StopChan - // wonder if there is a more elegant way - var wg sync.WaitGroup - if tt.expectStopChannelReceive { - wg.Add(1) - go func() { - <-u.stopChan - wg.Done() - }() - time.Sleep(5 * time.Millisecond) - } - - stopCalledCount := 0 - stopFunc := func() { - stopCalledCount++ - } - u.stopExecution = stopFunc - - u.interrupt(tt.args.err) - assert.Equal(t, tt.expectedCallsToStop, stopCalledCount) - - // test will time out if we don't get something on u.stopChan when expecting channel receive - wg.Wait() - }) - } -} - -func Test_updaterCmd_interrupt_multiple(t *testing.T) { - t.Parallel() - - sigChannel := make(chan os.Signal, 1) - logger := log.NewNopLogger() - autoupdater := &mocks.Updater{} - autoupdater.On("Run", mock.Anything, mock.Anything).Return(func() {}, nil) - - ctx, cancel := context.WithCancel(context.Background()) - - u := &updaterCmd{ - updater: autoupdater, - ctx: ctx, - cancel: cancel, - stopChan: make(chan bool), - config: &UpdaterConfig{ - Logger: logger, - AutoupdateInterval: 1 * time.Second, - InitialDelay: 10 * time.Second, - SigChannel: sigChannel, - }, - runUpdaterRetryInterval: 30 * time.Minute, - } - - go u.execute() - time.Sleep(3 * time.Second) - u.interrupt(errors.New("test error")) - - // Confirm we can call Interrupt multiple times without blocking - interruptComplete := make(chan struct{}) - expectedInterrupts := 3 - for i := 0; i < expectedInterrupts; i += 1 { - go func() { - u.interrupt(nil) - interruptComplete <- struct{}{} - }() - } - - receivedInterrupts := 0 - for { - if receivedInterrupts >= expectedInterrupts { - break - } - - select { - case <-interruptComplete: - receivedInterrupts += 1 - continue - case <-time.After(5 * time.Second): - t.Errorf("could not call interrupt multiple times and return within 5 seconds -- received %d interrupts before timeout", receivedInterrupts) - t.FailNow() - } - } - - require.Equal(t, expectedInterrupts, receivedInterrupts) -} diff --git a/pkg/autoupdate/autoupdate.go b/pkg/autoupdate/autoupdate.go index 519b1683d..f04442304 100644 --- a/pkg/autoupdate/autoupdate.go +++ b/pkg/autoupdate/autoupdate.go @@ -1,64 +1,6 @@ -// Package autoupdate provides a TUF Updater for the launcher and -// related binaries. This is abstracted across two packages, as well -// as main, making for a rather complex tangle. -// -// As different binaries need different strategies for restarting, -// there are several moving parts to this: -// -// github.com/kolide/updater/tuf is kolide's client to The Update -// Framework (also called notary). This library is based around -// signed metadata. When the metadata changes, it will download the -// linked file. (This idiom is a bit confusing, and a bit -// limiting. It downloads on _metadata_ change, and not as a file -// comparison) -// -// tuf.NotificationHandler is responsible for moving the downloaded -// binary into the desired location. It defined by this package, -// and is passed to TUF as a function. It is also used by TUF as a -// ad-hoc logging mechanism. -// -// autoupdate.UpdateFinalizer is responsible for finalizing the -// update. Eg: restarting the service appropriately. As it is -// different per binary, it is defined by main, and passed in to -// autoupdate.NewUpdater. -// -// # Expected Usage -// -// For each binary that is being updated, main will create a rungroup -// actor.Actor, for the autouopdate.Updater. main is responsible for -// setting an appropriate finalizer. -// -// This actor is a wrapper around TUF. TUF will check at a specified -// interval for new metadata. If found, it will update the local -// metadata repo, and fetch a new binary. -// -// tuf will then call the updater's handler to move the resultant -// binary. And finally pass off to the finalizer. -// -// # Testing -// -// While some functions can be unit tested, integration is tightly -// coupled to TUF. One of the simplest ways to test this, is by -// attaching to the `nightly` channel, and causing frequent updates. -// -//nolint:typecheck // parts of this come from bindata, so lint fails +// Package autoupdate has largely been superseded by ee/tuf. package autoupdate -import ( - "encoding/json" - "fmt" - "net/http" - "os" - "path" - "path/filepath" - "strings" - - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" - "github.com/kolide/launcher/pkg/osquery" - "github.com/kolide/updater/tuf" -) - // UpdateChannel determines the TUF target for a Updater. // The Default UpdateChannel is Stable. type UpdateChannel string @@ -88,317 +30,3 @@ const ( DefaultNotary = "https://notary.kolide.co" DefaultNotaryPrefix = "kolide" ) - -// Updater is a TUF autoupdater. It expects a tar.gz archive with an -// executable binary, which will be placed into an update area and -// spawned via appropriate platform mechanisms. -type Updater struct { - binaryName string // What binary name on disk. This includes things like `.exe` - strippedBinaryName string // What is the binary name minus any extensions. - bootstrapFn func() error // function to create the local TUF metadata - finalizer UpdateFinalizer // function that will "finalize" the update, by restarting the binary - stagingPath string // Where should TUF stage the downloads - updatesDirectory string // directory to store updates in - target string // filename to download, passed to TUF. - updateChannel UpdateChannel // Update channel (stable, nightly, etc) - settings *tuf.Settings // tuf.Settings - sigChannel chan os.Signal // channel for shutdown signaling - client *http.Client - logger log.Logger -} - -// UpdateFinalizer is executed after the Updater updates a destination. -// The UpdateFinalizer is usually a function which will handle restarting the updated binary. -type UpdateFinalizer func() error - -// NewUpdater creates a unstarted updater for a specific binary -// updated from a TUF mirror. -func NewUpdater(binaryPath, rootDirectory string, opts ...UpdaterOption) (*Updater, error) { - // There's some chaos between windows and non-windows. In windows, - // the binaryName ends in .exe, in posix it does not. So, a simple - // TrimSuffix will handle stripping it. *However* this will break if - // we add the extension. The suffix is inconistent. package-builder - // has a lot of gnarly code around that. We may need to import it. - binaryName := filepath.Base(binaryPath) - - // this lets us run auto updater in vscode debug mode with dlv - // not really sure why the app can't handle __debug_bin as the executable name - // maybe look into it later if there is time - if binaryName == "__debug_bin" { - binaryName = "launcher" - } - - strippedBinaryName := strings.TrimSuffix(binaryName, ".exe") - tufRepoPath := filepath.Join(rootDirectory, fmt.Sprintf("%s-tuf", strippedBinaryName)) - - settings := tuf.Settings{ - LocalRepoPath: tufRepoPath, - NotaryURL: DefaultNotary, - GUN: path.Join(DefaultNotaryPrefix, strippedBinaryName), - MirrorURL: DefaultMirror, - } - - updater := Updater{ - settings: &settings, - updateChannel: Stable, - client: http.DefaultClient, - logger: log.NewNopLogger(), - finalizer: func() error { return nil }, - strippedBinaryName: strippedBinaryName, - binaryName: binaryName, - } - - // The staging directory is used as a temporary download - // location for TUF. The updatesDirectory is used as a place - // to hold newer binary versions. The updated binaries are - // executated from this directory. We store the update - // relatative to the binaryPath primarily so that command line - // executions can find it, without needing to know where the - // rootDirectory is. (it likely also helps uncommon noexec - // cases) - updater.stagingPath = filepath.Join(rootDirectory, fmt.Sprintf("%s-staging", binaryName)) - updater.updatesDirectory = filepath.Join(FindBaseDir(binaryPath), fmt.Sprintf("%s-updates", binaryName)) - - // create TUF from local assets, but allow overriding with a no-op in tests. - updater.bootstrapFn = updater.createLocalTufRepo - - for _, opt := range opts { - opt(&updater) - } - - if err := updater.setTargetPath(); err != nil { - return nil, fmt.Errorf("set updater target for destination %s: %w", binaryPath, err) - } - - if err := updater.bootstrapFn(); err != nil { - return nil, fmt.Errorf("creating local TUF repo: %w", err) - } - - level.Debug(updater.logger).Log( - "msg", "Created Updater", - "binaryName", updater.binaryName, - "stagingPath", updater.stagingPath, - "updatesDirectory", updater.updatesDirectory, - ) - - return &updater, nil -} - -// createLocalTufRepo bootstraps local TUF metadata from bindata -// assets. (TUF requires an initial starting repo) -func (u *Updater) createLocalTufRepo() error { - if err := os.MkdirAll(u.settings.LocalRepoPath, 0755); err != nil { - return fmt.Errorf("mkdir LocalRepoPath (%s): %w", u.settings.LocalRepoPath, err) - } - localRepo := filepath.Base(u.settings.LocalRepoPath) - assetPath := path.Join("pkg", "autoupdate", "assets", localRepo) - - if err := u.createTUFRepoDirectory(u.settings.LocalRepoPath, assetPath, AssetDir); err != nil { - return fmt.Errorf("createTUFRepoDirectory %s: %w", u.settings.LocalRepoPath, err) - } - return nil -} - -type assetDirFunc func(string) ([]string, error) - -// Creates TUF repo including delegate tree structure on local file system. -// assetDir is the bindata AssetDir function. -func (u *Updater) createTUFRepoDirectory(localPath string, currentAssetPath string, assetDir assetDirFunc) error { - paths, err := assetDir(currentAssetPath) - if err != nil { - return fmt.Errorf("assetDir: %w", err) - } - - for _, assetPath := range paths { - fullAssetPath := path.Join(currentAssetPath, assetPath) - fullLocalPath := filepath.Join(localPath, assetPath) - - // if fullAssetPath is a json file, we should copy it to localPath - if filepath.Ext(fullAssetPath) == ".json" { - // The local file should exist and be - // valid. The starting condition comes from - // our bundled assets, and it is subsequently - // updated by TUF. We have seen benign - // corruption occur, so we want to detect and - // repair that. - if ok := u.validLocalFile(fullLocalPath); ok { - continue - } - - asset, err := Asset(fullAssetPath) - if err != nil { - return fmt.Errorf("could not get asset: %w", err) - } - if err := os.WriteFile(fullLocalPath, asset, 0644); err != nil { - return fmt.Errorf("could not write file: %w", err) - } - continue - } - - // if fullAssetPath is not a JSON file, it's a directory. Create the - // directory in localPath and recurse into it - if err := os.MkdirAll(fullLocalPath, 0755); err != nil { - return fmt.Errorf("mkdir fullLocalPath (%s): %w", fullLocalPath, err) - } - if err := u.createTUFRepoDirectory(fullLocalPath, fullAssetPath, assetDir); err != nil { - return fmt.Errorf("could not recurse into createTUFRepoDirectory: %w", err) - } - } - return nil -} - -// validLocalFile Checks whether the local file is valid. This was -// originally a simple exists? check, but we've seen this become -// corrupt on disk for benign reasons. So, if it's obviously bad, log -// and allow it to be replaced with the one from assets. (Do not -// attempt to rollback inside the TUF repo, that breaks the -// abstraction of updater) -func (u *Updater) validLocalFile(fullLocalPath string) bool { - // Check for a missing file. This state is invalid, but we - // don't need to log about it. - if _, err := os.Stat(fullLocalPath); os.IsNotExist(err) { - // No file. While this is invalid, we don't need to log - return false - } - - logger := log.With(level.Info(u.logger), - "msg", "Replacing corrupt TUF file", - "file", fullLocalPath, - ) - - jsonFile, err := os.Open(fullLocalPath) - if err != nil { - logger.Log("err", err) - return false - } - defer jsonFile.Close() - - // Check json validity. We use a Decoder, and not Valid, so we - // can get the json error back. - var v interface{} - if err := json.NewDecoder(jsonFile).Decode(&v); err != nil { - logger.Log("err", err) - return false - } - - return true -} - -// UpdaterOption customizes the Updater. -type UpdaterOption func(*Updater) - -// WithHTTPClient client configures an http client for the updater. -// If unspecified, http.DefaultClient will be used. -func WithHTTPClient(client *http.Client) UpdaterOption { - return func(u *Updater) { - u.client = client - } -} - -// WithSigChannel configures the channel uses for shutdown signaling -func WithSigChannel(sc chan os.Signal) UpdaterOption { - return func(u *Updater) { - u.sigChannel = sc - } -} - -// WithUpdate configures the update channel. -// If unspecified, the Updater will use the Stable channel. -func WithUpdateChannel(channel UpdateChannel) UpdaterOption { - return func(u *Updater) { - u.updateChannel = channel - } -} - -// WithFinalizer configures an UpdateFinalizer for the updater. -func WithFinalizer(f UpdateFinalizer) UpdaterOption { - return func(u *Updater) { - u.finalizer = f - } -} - -// WithMirrorURL configures a MirrorURL in the TUF settings. -func WithMirrorURL(url string) UpdaterOption { - return func(u *Updater) { - u.settings.MirrorURL = url - } -} - -// WithLogger configures a logger. -func WithLogger(logger log.Logger) UpdaterOption { - return func(u *Updater) { - u.logger = log.With(logger, "caller", log.DefaultCaller) - } -} - -// WithNotaryURL configures a NotaryURL in the TUF settings. -func WithNotaryURL(url string) UpdaterOption { - return func(u *Updater) { - u.settings.NotaryURL = url - } -} - -// WithNotaryPrefix configures a prefix for the binaryTargets -func WithNotaryPrefix(prefix string) UpdaterOption { - return func(u *Updater) { - u.settings.GUN = path.Join(prefix, u.strippedBinaryName) - } -} - -// override the default bootstrap function for local TUF assets -// only used in tests. -func withoutBootstrap() UpdaterOption { - return func(u *Updater) { - u.bootstrapFn = func() error { return nil } - } -} - -// Run starts the updater, which will run until the stop function is called. -func (u *Updater) Run(opts ...tuf.Option) (stop func(), err error) { - updaterOpts := []tuf.Option{ - tuf.WithHTTPClient(u.client), - tuf.WithAutoUpdate(u.target, u.stagingPath, u.handler()), - } - for _, opt := range opts { - updaterOpts = append(updaterOpts, opt) - } - - level.Debug(u.logger).Log( - "msg", "Running Updater", - "targetName", u.target, - "strippedBinaryName", u.strippedBinaryName, - "LocalRepoPath", u.settings.LocalRepoPath, - "GUN", u.settings.GUN, - "stagingPath", u.stagingPath, - "updatesDirectory", u.updatesDirectory, - ) - - // tuf.NewClient spawns a go thread with a running worker in - // the background. We don't get much for runtime - // communication back from it. Some can come in via the - // UpdateFinalizer function, but it's mostly fire-and-forget - client, err := tuf.NewClient( - u.settings, - updaterOpts..., - ) - if err != nil { - return nil, fmt.Errorf("launching %s updater service: %w", filepath.Base(u.binaryName), err) - } - return client.Stop, nil -} - -// setTargetPath uses the platform and the binary name to set the -// updater's target to a notary path. Ex: darwin/osquery-stable.tar.gz -func (u *Updater) setTargetPath() error { - platform, err := osquery.DetectPlatform() - if err != nil { - return fmt.Errorf("detect platform: %w", err) - } - - // filename = -.tar.gz - filename := fmt.Sprintf("%s-%s", u.strippedBinaryName, u.updateChannel) - base := path.Join(string(platform), filename) - u.target = fmt.Sprintf("%s.tar.gz", base) - - return nil -} diff --git a/pkg/autoupdate/autoupdate_test.go b/pkg/autoupdate/autoupdate_test.go index 193eeb379..8ce45ebbf 100644 --- a/pkg/autoupdate/autoupdate_test.go +++ b/pkg/autoupdate/autoupdate_test.go @@ -1,222 +1,11 @@ -//nolint:typecheck // parts of this come from bindata, so lint fails package autoupdate import ( - "encoding/json" - "fmt" - "net/http" - "os" - "path/filepath" "testing" - "github.com/go-kit/kit/log" - "github.com/kolide/launcher/pkg/osquery" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestCreateTUFRepoDirectory(t *testing.T) { - t.Parallel() - - localTUFRepoPath := t.TempDir() - - u := &Updater{logger: log.NewNopLogger()} - require.NoError(t, u.createTUFRepoDirectory(localTUFRepoPath, "pkg/autoupdate/assets", AssetDir)) - - knownFilePaths := []string{ - "launcher-tuf/root.json", - "launcher-tuf/snapshot.json", - "launcher-tuf/targets.json", - "launcher-tuf/timestamp.json", - "launcher-tuf/targets/releases.json", - "osqueryd-tuf/root.json", - "osqueryd-tuf/snapshot.json", - "osqueryd-tuf/targets.json", - "osqueryd-tuf/timestamp.json", - "osqueryd-tuf/targets/releases.json", - } - - for _, knownFilePath := range knownFilePaths { - fullFilePath := filepath.Join(localTUFRepoPath, knownFilePath) - _, err := os.Stat(fullFilePath) - require.NoError(t, err, "stat file") - - jsonBytes, err := os.ReadFile(fullFilePath) - require.NoError(t, err, "read file") - - require.True(t, json.Valid(jsonBytes), "file is json") - } - - // Corrupt some local files - require.NoError(t, - os.Remove(filepath.Join(localTUFRepoPath, knownFilePaths[0])), - "remove a tuf file") - require.NoError(t, - os.WriteFile(filepath.Join(localTUFRepoPath, knownFilePaths[1]), nil, 0644), - "truncate a tuf file") - - // Attempt to re-create - require.NoError(t, u.createTUFRepoDirectory(localTUFRepoPath, "pkg/autoupdate/assets", AssetDir)) - - // And retest - for _, knownFilePath := range knownFilePaths { - fullFilePath := filepath.Join(localTUFRepoPath, knownFilePath) - _, err := os.Stat(fullFilePath) - require.NoError(t, err, "stat file") - - jsonBytes, err := os.ReadFile(fullFilePath) - require.NoError(t, err, "read file") - - require.True(t, json.Valid(jsonBytes), "file is json") - } - - require.NoError(t, os.RemoveAll(localTUFRepoPath)) -} - -func TestValidLocalFile(t *testing.T) { - t.Parallel() - var tests = []struct { - name string - content []byte - assertion require.BoolAssertionFunc - logCount int - }{ - { - name: "no file", - assertion: require.False, - }, - { - name: "empty", - content: []byte{}, - assertion: require.False, - logCount: 1, - }, - - { - name: "space", - content: []byte(" "), - assertion: require.False, - logCount: 1, - }, - { - name: "dangle brace", - content: []byte("{"), - assertion: require.False, - logCount: 1, - }, - { - name: "unquoted", - content: []byte("{a: 1}"), - assertion: require.False, - logCount: 1, - }, - { - name: "valid", - content: []byte("{}"), - assertion: require.True, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - testFile, err := os.CreateTemp("", "TestValidLocalFile") - require.NoError(t, err) - defer os.Remove(testFile.Name()) - - if tt.content == nil { - require.NoError(t, testFile.Close()) - require.NoError(t, os.Remove(testFile.Name())) - } else { - if len(tt.content) > 0 { - _, err := testFile.Write(tt.content) - require.NoError(t, err) - } - require.NoError(t, testFile.Close()) - } - - l := &mockLogger{} - u := &Updater{logger: l} - tt.assertion(t, u.validLocalFile(testFile.Name())) - require.Equal(t, tt.logCount, l.Count(), "log count") - }) - } - -} - -func TestNewUpdater(t *testing.T) { - t.Parallel() - var tests = []struct { - name string - opts []UpdaterOption - httpClient *http.Client - target string - localRepoPath string - notaryURL string - mirrorURL string - }{ - { - name: "default", - opts: nil, - httpClient: http.DefaultClient, - target: withPlatform(t, "%s/app-stable.tar.gz"), - localRepoPath: "/tmp/tuf/app-tuf", - notaryURL: DefaultNotary, - mirrorURL: DefaultMirror, - }, - { - name: "with-opts", - opts: []UpdaterOption{ - WithHTTPClient(nil), - WithUpdateChannel(Beta), - WithNotaryURL("https://notary"), - WithMirrorURL("https://mirror"), - }, - httpClient: nil, - target: withPlatform(t, "%s/app-beta.tar.gz"), - localRepoPath: "/tmp/tuf/app-tuf", - notaryURL: "https://notary", - mirrorURL: "https://mirror", - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - gun := fmt.Sprintf("kolide/app") - tt.opts = append(tt.opts, withoutBootstrap()) - u, err := NewUpdater("/tmp/app", "/tmp/tuf", tt.opts...) - require.NoError(t, err) - - require.Equal(t, tt.target, u.target) - - // check tuf.Settings derived from NewUpdater defaults. - require.Equal(t, gun, u.settings.GUN) - require.Equal(t, filepath.Clean(tt.localRepoPath), u.settings.LocalRepoPath) - require.Equal(t, tt.notaryURL, u.settings.NotaryURL) - require.Equal(t, tt.mirrorURL, u.settings.MirrorURL) - - // must have a non-nil finalizer - require.NotNil(t, u.finalizer) - - // Running finalizer shouldn't error - require.NoError(t, u.finalizer()) - }) - } -} - -func withPlatform(t *testing.T, format string) string { - platform, err := osquery.DetectPlatform() - if err != nil { - t.Fatal(err) - } - return fmt.Sprintf(format, platform) -} - func TestSanitizeUpdateChannel(t *testing.T) { t.Parallel() var tests = []struct { diff --git a/pkg/autoupdate/errors.go b/pkg/autoupdate/errors.go deleted file mode 100644 index a3030d3a8..000000000 --- a/pkg/autoupdate/errors.go +++ /dev/null @@ -1,22 +0,0 @@ -package autoupdate - -import "github.com/pkg/errors" - -type LauncherRestartNeeded struct { - msg string -} - -func NewLauncherRestartNeededErr(msg string) LauncherRestartNeeded { - return LauncherRestartNeeded{ - msg: msg, - } -} - -func (e LauncherRestartNeeded) Error() string { - return e.msg -} - -func IsLauncherRestartNeededErr(err error) bool { - _, ok := errors.Cause(err).(LauncherRestartNeeded) - return ok -} diff --git a/pkg/autoupdate/errors_test.go b/pkg/autoupdate/errors_test.go deleted file mode 100644 index 4ad03dd1b..000000000 --- a/pkg/autoupdate/errors_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package autoupdate - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestLauncherRestartNeededError(t *testing.T) { - t.Parallel() - - restartErr := NewLauncherRestartNeededErr("an error") - require.Error(t, restartErr) - require.True(t, IsLauncherRestartNeededErr(restartErr)) - - otherErr := errors.New("an error") - require.Error(t, otherErr) - require.False(t, IsLauncherRestartNeededErr(otherErr)) - -} diff --git a/pkg/autoupdate/handler.go b/pkg/autoupdate/handler.go deleted file mode 100644 index 033b76673..000000000 --- a/pkg/autoupdate/handler.go +++ /dev/null @@ -1,139 +0,0 @@ -package autoupdate - -import ( - "context" - "os" - "path/filepath" - "strconv" - "time" - - "github.com/go-kit/kit/log/level" - "github.com/kolide/kit/fsutil" - "github.com/kolide/updater/tuf" -) - -// handler is called by the tuf package in two cases. First, and -// confusingly, it's used as an error reporting channel for any kind -// of issue. In this case, it's called with an err set. -// -// Second, it's called when tuf detects a change with the remote metadata. -// The handler method will do the following: -// 1) untar the staged download -// 2) place binary into the updates/ directory -// 3) call the Updater's finalizer method, usually a restart function for the running binary. -func (u *Updater) handler() tuf.NotificationHandler { - return func(stagingPath string, err error) { - if err != nil { - level.Info(u.logger).Log( - "msg", "tuf updater returned", - "target", u.target, - "err", err) - return - } - - level.Debug(u.logger).Log( - "msg", "Starting to handle a staged TUF download", - "file", stagingPath, - "target", u.target, - ) - - // We store the updated file in a dated directory. The - // dated directory is a bit odd, but it's plastering - // over how tuf works. This way we ensure we're always - // running the mostly recently downloaded file. There - // are other patterns we should investigate if we - // change the way we denote stable in notary. - updateDir := filepath.Join(u.updatesDirectory, strconv.FormatInt(time.Now().Unix(), 10)) - - // Note that this is expecting the binary in the - // tarball to be named binaryName. There some some - // extension weirdness issues on windows vs posix. - outputBinary := filepath.Join(updateDir, u.binaryName) - - if err := os.MkdirAll(updateDir, 0755); err != nil { - level.Error(u.logger).Log( - "msg", "making updates directory", - "dir", updateDir, - "err", err) - return - } - - cleanupBrokenUpdate := func() { - if err := os.RemoveAll(updateDir); err != nil { - level.Error(u.logger).Log( - "msg", "failed to removed broken update directory", - "updateDir", updateDir, - "err", err, - ) - } - } - - // The UntarBundle(destination, source) paths are a - // little weird. Source is a tarball, obvious - // enough. But destination is a string that's passed - // through filepath.Dir. Which means it strips off the - // last component. - if err := fsutil.UntarBundle(outputBinary, stagingPath); err != nil { - level.Error(u.logger).Log( - "msg", "untar downloaded target", - "binary", outputBinary, - "err", err, - ) - cleanupBrokenUpdate() - return - } - - // Ensure it's executable - if err := os.Chmod(outputBinary, 0755); err != nil { - level.Error(u.logger).Log( - "msg", "setting +x permissions on binary", - "binary", outputBinary, - "err", err, - ) - cleanupBrokenUpdate() - return - } - - // Check that it all came through okay - if err := CheckExecutable(context.TODO(), outputBinary, "--version"); err != nil { - level.Error(u.logger).Log( - "msg", "Broken updated binary. Removing", - "target", u.target, - "outputBinary", outputBinary, - "err", err, - ) - cleanupBrokenUpdate() - return - } - - level.Info(u.logger).Log( - "msg", "Updated Binary ready to go", - "target", u.target, - "outputBinary", outputBinary, - ) - - if err := u.finalizer(); err != nil { - // Some kinds of updates require a full launcher restart. For - // example, windows doesn't have an exec. Instead launcher exits - // so the service manager restarts it. There may be others. - if IsLauncherRestartNeededErr(err) { - level.Info(u.logger).Log( - "msg", "signaling for a full restart", - "binary", outputBinary, - ) - u.sigChannel <- os.Interrupt - return - } - - level.Error(u.logger).Log( - "msg", "calling restart function for updated binary", - "binary", outputBinary, - "err", err) - // Reaching this point represents an unclear error. Trigger a restart - u.sigChannel <- os.Interrupt - return - } - - level.Debug(u.logger).Log("msg", "completed update for binary", "binary", outputBinary) - } -} diff --git a/pkg/autoupdate/mocklogger_test.go b/pkg/autoupdate/mocklogger_test.go deleted file mode 100644 index e52e2f50f..000000000 --- a/pkg/autoupdate/mocklogger_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package autoupdate - -import ( - "sync" -) - -type mockLogger struct { - count int - mx sync.Mutex -} - -func (l *mockLogger) Log(keyvals ...interface{}) error { - l.mx.Lock() - defer l.mx.Unlock() - - l.count = l.count + 1 - return nil -} - -func (l *mockLogger) Count() int { - l.mx.Lock() - defer l.mx.Unlock() - - return l.count -}