From 36008dd114a9b7238727aebfbd951ad4ae9d14b1 Mon Sep 17 00:00:00 2001 From: Stephen Levine Date: Fri, 22 Nov 2024 16:58:02 -0500 Subject: [PATCH] [teleport-update] Uninstall subcommand (#49341) * Uninstall * tests * comment * Short-circuit link package on pinned * log * move error * Update lib/autoupdate/agent/process.go Co-authored-by: Hugo Shaka * Update lib/autoupdate/agent/process.go Co-authored-by: Hugo Shaka * Update lib/autoupdate/agent/process.go Co-authored-by: Hugo Shaka * Update lib/autoupdate/agent/process.go Co-authored-by: Hugo Shaka * fix --------- Co-authored-by: Hugo Shaka --- lib/autoupdate/agent/installer.go | 4 +- lib/autoupdate/agent/process.go | 43 ++- lib/autoupdate/agent/setup.go | 28 ++ .../no_need_to_reload.golden | 9 + .../TestUpdater_Install/no_systemd.golden | 9 + lib/autoupdate/agent/updater.go | 131 +++++++- lib/autoupdate/agent/updater_test.go | 286 +++++++++++++++++- tool/teleport-update/main.go | 33 ++ 8 files changed, 533 insertions(+), 10 deletions(-) create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Install/no_need_to_reload.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Install/no_systemd.golden diff --git a/lib/autoupdate/agent/installer.go b/lib/autoupdate/agent/installer.go index a723b02aa3379..8b8cf77a32026 100644 --- a/lib/autoupdate/agent/installer.go +++ b/lib/autoupdate/agent/installer.go @@ -575,7 +575,7 @@ func (li *LocalInstaller) forceLinks(ctx context.Context, binDir, svcDir string) linked++ } if linked == 0 { - return revert, trace.Errorf("no binaries available to link") + return revert, trace.Wrap(ErrNoBinaries) } // create systemd service file @@ -784,7 +784,7 @@ func (li *LocalInstaller) tryLinks(ctx context.Context, binDir, svcDir string) e } // bail if no binaries can be linked if linked == 0 { - return trace.Errorf("no binaries available to link") + return trace.Wrap(ErrNoBinaries) } // link binaries that are missing links diff --git a/lib/autoupdate/agent/process.go b/lib/autoupdate/agent/process.go index ed210cae99ed0..77455b24eb1dd 100644 --- a/lib/autoupdate/agent/process.go +++ b/lib/autoupdate/agent/process.go @@ -111,7 +111,11 @@ func (s SystemdService) Reload(ctx context.Context) error { } if initPID != 0 { s.Log.InfoContext(ctx, "Monitoring PID file to detect crashes.", unitKey, s.ServiceName) - return trace.Wrap(s.monitor(ctx, initPID)) + err := s.monitor(ctx, initPID) + if errors.Is(err, context.DeadlineExceeded) { + return trace.Errorf("timed out while waiting for process to start") + } + return trace.Wrap(err) } return nil } @@ -265,7 +269,7 @@ func (s SystemdService) Enable(ctx context.Context, now bool) error { if now { args = append(args, "--now") } - code := s.systemctl(ctx, slog.LevelError, args...) + code := s.systemctl(ctx, slog.LevelInfo, args...) if code != 0 { return trace.Errorf("unable to enable systemd service") } @@ -273,6 +277,41 @@ func (s SystemdService) Enable(ctx context.Context, now bool) error { return nil } +// Disable the systemd service. +func (s SystemdService) Disable(ctx context.Context) error { + if err := s.checkSystem(ctx); err != nil { + return trace.Wrap(err) + } + code := s.systemctl(ctx, slog.LevelInfo, "disable", s.ServiceName) + if code != 0 { + return trace.Errorf("unable to disable systemd service") + } + s.Log.InfoContext(ctx, "Systemd service disabled.", unitKey, s.ServiceName) + return nil +} + +// IsEnabled returns true if the service is enabled, or if it's disabled but still active. +func (s SystemdService) IsEnabled(ctx context.Context) (bool, error) { + if err := s.checkSystem(ctx); err != nil { + return false, trace.Wrap(err) + } + code := s.systemctl(ctx, slog.LevelDebug, "is-enabled", "--quiet", s.ServiceName) + switch { + case code < 0: + return false, trace.Errorf("unable to determine if systemd service %q is enabled", s.ServiceName) + case code == 0: + return true, nil + } + code = s.systemctl(ctx, slog.LevelDebug, "is-active", "--quiet", s.ServiceName) + switch { + case code < 0: + return false, trace.Errorf("unable to determine if systemd service %q is active", s.ServiceName) + case code == 0: + return true, nil + } + return false, nil +} + // checkSystem returns an error if the system is not compatible with this process manager. func (s SystemdService) checkSystem(ctx context.Context) error { _, err := os.Stat("/run/systemd/system") diff --git a/lib/autoupdate/agent/setup.go b/lib/autoupdate/agent/setup.go index 334d1089ab7f4..a8bf12a0afe0a 100644 --- a/lib/autoupdate/agent/setup.go +++ b/lib/autoupdate/agent/setup.go @@ -20,6 +20,8 @@ package agent import ( "context" + "errors" + "io/fs" "log/slog" "os" "path/filepath" @@ -72,6 +74,32 @@ func Setup(ctx context.Context, log *slog.Logger, linkDir, dataDir string) error return nil } +// Teardown removes all traces of the auto-updater, including its configuration. +func Teardown(ctx context.Context, log *slog.Logger, linkDir, dataDir string) error { + svc := &SystemdService{ + ServiceName: "teleport-update.timer", + Log: log, + } + if err := svc.Disable(ctx); err != nil { + return trace.Errorf("failed to disable teleport-update systemd timer: %w", err) + } + servicePath := filepath.Join(linkDir, serviceDir, updateServiceName) + if err := os.Remove(servicePath); err != nil && !errors.Is(err, fs.ErrNotExist) { + return trace.Errorf("failed to remove teleport-update systemd service: %w", err) + } + timerPath := filepath.Join(linkDir, serviceDir, updateTimerName) + if err := os.Remove(timerPath); err != nil && !errors.Is(err, fs.ErrNotExist) { + return trace.Errorf("failed to remove teleport-update systemd timer: %w", err) + } + if err := svc.Sync(ctx); err != nil { + return trace.Errorf("failed to sync systemd config: %w", err) + } + if err := os.RemoveAll(filepath.Join(dataDir, VersionsDirName)); err != nil { + return trace.Errorf("failed to remove versions directory: %w", err) + } + return nil +} + func writeConfigFiles(linkDir, dataDir string) error { servicePath := filepath.Join(linkDir, serviceDir, updateServiceName) err := writeTemplate(servicePath, updateServiceTemplate, linkDir, dataDir) diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/no_need_to_reload.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/no_need_to_reload.golden new file mode 100644 index 0000000000000..9c2b8ef209bbd --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/no_need_to_reload.golden @@ -0,0 +1,9 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + enabled: false + pinned: false +status: + active_version: 16.3.0 + backup_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/no_systemd.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/no_systemd.golden new file mode 100644 index 0000000000000..9c2b8ef209bbd --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/no_systemd.golden @@ -0,0 +1,9 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + enabled: false + pinned: false +status: + active_version: 16.3.0 + backup_version: "" diff --git a/lib/autoupdate/agent/updater.go b/lib/autoupdate/agent/updater.go index 73cd193124dfe..5dc8f8a0ce082 100644 --- a/lib/autoupdate/agent/updater.go +++ b/lib/autoupdate/agent/updater.go @@ -150,6 +150,9 @@ func NewLocalUpdater(cfg LocalUpdaterConfig) (*Updater, error) { Revert: func(ctx context.Context) error { return trace.Wrap(Setup(ctx, cfg.Log, cfg.LinkDir, cfg.DataDir)) }, + Teardown: func(ctx context.Context) error { + return trace.Wrap(Teardown(ctx, cfg.Log, cfg.LinkDir, cfg.DataDir)) + }, }, nil } @@ -191,6 +194,8 @@ type Updater struct { Setup func(ctx context.Context) error // Revert installs the Teleport updater service using the running installation. Revert func(ctx context.Context) error + // Teardown removes all traces of the updater and all managed installations. + Teardown func(ctx context.Context) error } // Installer provides an API for installing Teleport agents. @@ -214,8 +219,11 @@ type Installer interface { // Unlike LinkSystem, TryLinkSystem will fail if existing links to other locations are present. // TryLinkSystem must be idempotent. TryLinkSystem(ctx context.Context) error + // Unlink unlinks the specified version of Teleport from the linking locations. + // Unlink must be idempotent. + Unlink(ctx context.Context, version string) error // UnlinkSystem unlinks the system (package) installation of Teleport from the linking locations. - // TryLinkSystem must be idempotent. + // UnlinkSystem must be idempotent. UnlinkSystem(ctx context.Context) error // List the installed versions of Teleport. List(ctx context.Context) (versions []string, err error) @@ -232,6 +240,8 @@ var ( ErrNotNeeded = errors.New("not needed") // ErrNotSupported is returned when the operation is not supported on the platform. ErrNotSupported = errors.New("not supported on this platform") + // ErrNoBinaries is returned when no binaries are available to be linked. + ErrNoBinaries = errors.New("no binaries available to link") ) const ( @@ -254,6 +264,10 @@ type Process interface { // If the type implementing Process does not support the system process manager, // Sync must return ErrNotSupported. Sync(ctx context.Context) error + // IsEnabled must return true if the Process is running or is configured to run. + // If the type implementing Process does not support the system process manager, + // Sync must return ErrNotSupported. + IsEnabled(ctx context.Context) (bool, error) } // TODO(sclevine): add support for need_restart and selinux config @@ -324,6 +338,111 @@ func (u *Updater) Install(ctx context.Context, override OverrideConfig) error { return nil } +// Remove removes everything created by the updater. +// Before attempting this, Remove attempts to gracefully recover the system-packaged version of Teleport (if present). +// This function is idempotent. +func (u *Updater) Remove(ctx context.Context) error { + cfg, err := readConfig(u.ConfigPath) + if err != nil { + return trace.Errorf("failed to read %s: %w", updateConfigName, err) + } + if err := validateConfigSpec(&cfg.Spec, OverrideConfig{}); err != nil { + return trace.Wrap(err) + } + activeVersion := cfg.Status.ActiveVersion + if activeVersion == "" { + u.Log.InfoContext(ctx, "No installation of Teleport managed by the updater. Removing updater configuration.") + if err := u.Teardown(ctx); err != nil { + return trace.Wrap(err) + } + u.Log.InfoContext(ctx, "Automatic update configuration for Teleport successfully uninstalled.") + return nil + } + + revert, err := u.Installer.LinkSystem(ctx) + if errors.Is(err, ErrNoBinaries) { + u.Log.InfoContext(ctx, "Updater-managed installation of Teleport detected. Attempting to unlink and remove.") + ok, err := u.Process.IsEnabled(ctx) + if err != nil && !errors.Is(err, ErrNotSupported) { + return trace.Wrap(err) + } + if ok { + return trace.Errorf("refusing to remove active installation of Teleport, please disable Teleport first") + } + if err := u.Installer.Unlink(ctx, activeVersion); err != nil { + return trace.Wrap(err) + } + u.Log.InfoContext(ctx, "Teleport uninstalled.", "version", activeVersion) + if err := u.Teardown(ctx); err != nil { + return trace.Wrap(err) + } + u.Log.InfoContext(ctx, "Automatic update configuration for Teleport successfully uninstalled.") + return nil + } + if err != nil { + return trace.Errorf("failed to link: %w", err) + } + + u.Log.InfoContext(ctx, "Updater-managed installation of Teleport detected. Restoring packaged version of Teleport before removing.") + + revertConfig := func(ctx context.Context) bool { + if ok := revert(ctx); !ok { + u.Log.ErrorContext(ctx, "Failed to revert Teleport symlinks. Installation likely broken.") + return false + } + if err := u.Process.Sync(ctx); err != nil { + u.Log.ErrorContext(ctx, "Failed to revert systemd configuration after failed restart.", errorKey, err) + return false + } + return true + } + + // Sync systemd. + + err = u.Process.Sync(ctx) + if errors.Is(err, ErrNotSupported) { + u.Log.WarnContext(ctx, "Not syncing systemd configuration because systemd is not running.") + } else if errors.Is(err, context.Canceled) { + return trace.Errorf("sync canceled") + } else if err != nil { + // If sync fails, we may have left the host in a bad state, so we revert linking and re-Sync. + u.Log.ErrorContext(ctx, "Reverting symlinks due to invalid configuration.") + if ok := revertConfig(ctx); ok { + u.Log.WarnContext(ctx, "Teleport updater encountered a configuration error and successfully reverted the installation.") + } + return trace.Errorf("failed to validate configuration for system package version of Teleport: %w", err) + } + + // Restart Teleport. + + u.Log.InfoContext(ctx, "Teleport package successfully restored.") + err = u.Process.Reload(ctx) + if errors.Is(err, context.Canceled) { + return trace.Errorf("reload canceled") + } + if err != nil && + !errors.Is(err, ErrNotNeeded) && // no output if restart not needed + !errors.Is(err, ErrNotSupported) { // already logged above for Sync + + // If reloading Teleport at the new version fails, revert and reload. + u.Log.ErrorContext(ctx, "Reverting symlinks due to failed restart.") + if ok := revertConfig(ctx); ok { + if err := u.Process.Reload(ctx); err != nil && !errors.Is(err, ErrNotNeeded) { + u.Log.ErrorContext(ctx, "Failed to reload Teleport after reverting. Installation likely broken.", errorKey, err) + } else { + u.Log.WarnContext(ctx, "Teleport updater detected an error with the new installation and successfully reverted it.") + } + } + return trace.Errorf("failed to start system package version of Teleport: %w", err) + } + u.Log.InfoContext(ctx, "Auto-updating Teleport removed and replaced by Teleport packaged.", "version", activeVersion) + if err := u.Teardown(ctx); err != nil { + return trace.Wrap(err) + } + u.Log.InfoContext(ctx, "Auto-update configuration for Teleport successfully uninstalled.") + return nil +} + // Status returns all available local and remote fields related to agent auto-updates. func (u *Updater) Status(ctx context.Context) (Status, error) { var out Status @@ -585,7 +704,7 @@ func (u *Updater) update(ctx context.Context, cfg *UpdateConfig, targetVersion s if err := u.Process.Reload(ctx); err != nil && !errors.Is(err, ErrNotNeeded) { u.Log.ErrorContext(ctx, "Failed to reload Teleport after reverting. Installation likely broken.", errorKey, err) } else { - u.Log.WarnContext(ctx, "Teleport updater encountered a configuration error and successfully reverted the installation.") + u.Log.WarnContext(ctx, "Teleport updater detected an error with the new installation and successfully reverted it.") } } return trace.Errorf("failed to start new version %q of Teleport: %w", targetVersion, err) @@ -624,14 +743,18 @@ func (u *Updater) LinkPackage(ctx context.Context) error { } activeVersion := cfg.Status.ActiveVersion if cfg.Spec.Enabled { - u.Log.InfoContext(ctx, "Automatic updates enabled. Skipping system package link.", activeVersionKey, activeVersion) + u.Log.InfoContext(ctx, "Automatic updates is enabled. Skipping system package link.", activeVersionKey, activeVersion) + return nil + } + if cfg.Spec.Pinned { + u.Log.InfoContext(ctx, "Automatic update version is pinned. Skipping system package link.", activeVersionKey, activeVersion) return nil } // If an active version is set, but auto-updates is disabled, try to link the system installation in case the config is stale. // If any links are present, this will return ErrLinked and not create any system links. // This state is important to log as a warning, if err := u.Installer.TryLinkSystem(ctx); errors.Is(err, ErrLinked) { - u.Log.WarnContext(ctx, "Automatic updates disabled, but a non-package version of Teleport is linked.", activeVersionKey, activeVersion) + u.Log.WarnContext(ctx, "Automatic updates is disabled, but a non-package version of Teleport is linked.", activeVersionKey, activeVersion) return nil } else if err != nil { return trace.Errorf("failed to link system package installation: %w", err) diff --git a/lib/autoupdate/agent/updater_test.go b/lib/autoupdate/agent/updater_test.go index b67c808c6bbac..761381905cdd2 100644 --- a/lib/autoupdate/agent/updater_test.go +++ b/lib/autoupdate/agent/updater_test.go @@ -668,6 +668,19 @@ func TestUpdater_LinkPackage(t *testing.T) { tryLinkSystemCalls: 0, syncCalls: 0, }, + { + name: "pinned", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + Pinned: true, + }, + }, + + tryLinkSystemCalls: 0, + syncCalls: 0, + }, { name: "updates disabled", cfg: &UpdateConfig{ @@ -765,6 +778,244 @@ func TestUpdater_LinkPackage(t *testing.T) { } } +func TestUpdater_Remove(t *testing.T) { + t.Parallel() + + const version = "active-version" + + tests := []struct { + name string + cfg *UpdateConfig // nil -> file not present + linkSystemErr error + isEnabledErr error + syncErr error + reloadErr error + processEnabled bool + + unlinkedVersion string + teardownCalls int + syncCalls int + revertFuncCalls int + linkSystemCalls int + reloadCalls int + errMatch string + }{ + { + name: "no config", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: "", + }, + }, + teardownCalls: 1, + }, + { + name: "no active version", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + }, + teardownCalls: 1, + }, + { + name: "no system links, process enabled", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: version, + }, + }, + linkSystemErr: ErrNoBinaries, + linkSystemCalls: 1, + processEnabled: true, + errMatch: "refusing to remove", + }, + { + name: "no system links, process disabled", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: version, + }, + }, + linkSystemErr: ErrNoBinaries, + linkSystemCalls: 1, + unlinkedVersion: version, + teardownCalls: 1, + }, + { + name: "no system links, process disabled, no systemd", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: version, + }, + }, + linkSystemErr: ErrNoBinaries, + linkSystemCalls: 1, + isEnabledErr: ErrNotSupported, + unlinkedVersion: version, + teardownCalls: 1, + }, + { + name: "active version", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: version, + }, + }, + linkSystemCalls: 1, + syncCalls: 1, + reloadCalls: 1, + teardownCalls: 1, + }, + { + name: "active version, no systemd", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: version, + }, + }, + linkSystemCalls: 1, + syncCalls: 1, + reloadCalls: 1, + teardownCalls: 1, + syncErr: ErrNotSupported, + reloadErr: ErrNotSupported, + }, + { + name: "active version, no reload", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: version, + }, + }, + linkSystemCalls: 1, + syncCalls: 1, + reloadCalls: 1, + teardownCalls: 1, + reloadErr: ErrNotNeeded, + }, + { + name: "active version, sync error", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: version, + }, + }, + linkSystemCalls: 1, + syncCalls: 2, + revertFuncCalls: 1, + syncErr: errors.New("sync error"), + errMatch: "configuration", + }, + { + name: "active version, reload error", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Status: UpdateStatus{ + ActiveVersion: version, + }, + }, + linkSystemCalls: 1, + syncCalls: 2, + reloadCalls: 2, + revertFuncCalls: 1, + reloadErr: errors.New("reload error"), + errMatch: "start", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, VersionsDirName, "update.yaml") + + updater, err := NewLocalUpdater(LocalUpdaterConfig{ + InsecureSkipVerify: true, + DataDir: dir, + }) + require.NoError(t, err) + + // Create config file only if provided in test case + if tt.cfg != nil { + b, err := yaml.Marshal(tt.cfg) + require.NoError(t, err) + err = os.WriteFile(cfgPath, b, 0600) + require.NoError(t, err) + } + + var ( + linkSystemCalls int + revertFuncCalls int + syncCalls int + reloadCalls int + teardownCalls int + unlinkedVersion string + ) + updater.Installer = &testInstaller{ + FuncLinkSystem: func(_ context.Context) (revert func(context.Context) bool, err error) { + linkSystemCalls++ + return func(_ context.Context) bool { + revertFuncCalls++ + return true + }, tt.linkSystemErr + }, + FuncUnlink: func(_ context.Context, version string) error { + unlinkedVersion = version + return nil + }, + } + updater.Process = &testProcess{ + FuncSync: func(_ context.Context) error { + syncCalls++ + return tt.syncErr + }, + FuncReload: func(_ context.Context) error { + reloadCalls++ + return tt.reloadErr + }, + FuncIsEnabled: func(_ context.Context) (bool, error) { + return tt.processEnabled, tt.isEnabledErr + }, + } + updater.Teardown = func(_ context.Context) error { + teardownCalls++ + return nil + } + + ctx := context.Background() + err = updater.Remove(ctx) + if tt.errMatch != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errMatch) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.syncCalls, syncCalls) + require.Equal(t, tt.reloadCalls, reloadCalls) + require.Equal(t, tt.linkSystemCalls, linkSystemCalls) + require.Equal(t, tt.revertFuncCalls, revertFuncCalls) + require.Equal(t, tt.unlinkedVersion, unlinkedVersion) + require.Equal(t, tt.teardownCalls, teardownCalls) + }) + } +} + func TestUpdater_Install(t *testing.T) { t.Parallel() @@ -995,6 +1246,27 @@ func TestUpdater_Install(t *testing.T) { setupCalls: 1, errMatch: "reload error", }, + { + name: "no systemd", + reloadErr: ErrNotSupported, + setupErr: ErrNotSupported, + + installedVersion: "16.3.0", + installedTemplate: cdnURITemplate, + linkedVersion: "16.3.0", + reloadCalls: 1, + setupCalls: 1, + }, + { + name: "no need to reload", + reloadErr: ErrNotNeeded, + + installedVersion: "16.3.0", + installedTemplate: cdnURITemplate, + linkedVersion: "16.3.0", + reloadCalls: 1, + setupCalls: 1, + }, } for _, tt := range tests { @@ -1135,6 +1407,7 @@ type testInstaller struct { FuncLinkSystem func(ctx context.Context) (revert func(context.Context) bool, err error) FuncTryLink func(ctx context.Context, version string) error FuncTryLinkSystem func(ctx context.Context) error + FuncUnlink func(ctx context.Context, version string) error FuncUnlinkSystem func(ctx context.Context) error FuncList func(ctx context.Context) (versions []string, err error) } @@ -1163,6 +1436,10 @@ func (ti *testInstaller) TryLinkSystem(ctx context.Context) error { return ti.FuncTryLinkSystem(ctx) } +func (ti *testInstaller) Unlink(ctx context.Context, version string) error { + return ti.FuncUnlink(ctx, version) +} + func (ti *testInstaller) UnlinkSystem(ctx context.Context) error { return ti.FuncUnlinkSystem(ctx) } @@ -1172,8 +1449,9 @@ func (ti *testInstaller) List(ctx context.Context) (versions []string, err error } type testProcess struct { - FuncReload func(ctx context.Context) error - FuncSync func(ctx context.Context) error + FuncReload func(ctx context.Context) error + FuncSync func(ctx context.Context) error + FuncIsEnabled func(ctx context.Context) (bool, error) } func (tp *testProcess) Reload(ctx context.Context) error { @@ -1183,3 +1461,7 @@ func (tp *testProcess) Reload(ctx context.Context) error { func (tp *testProcess) Sync(ctx context.Context) error { return tp.FuncSync(ctx) } + +func (tp *testProcess) IsEnabled(ctx context.Context) (bool, error) { + return tp.FuncIsEnabled(ctx) +} diff --git a/tool/teleport-update/main.go b/tool/teleport-update/main.go index 9e19393f12d0c..e6702ae9f35c2 100644 --- a/tool/teleport-update/main.go +++ b/tool/teleport-update/main.go @@ -147,6 +147,8 @@ func Run(args []string) error { statusCmd := app.Command("status", "Show Teleport agent auto-update status.") + uninstallCmd := app.Command("uninstall", "Uninstall the updater-managed installation of Teleport. If the Teleport package is installed, it is restored as the primary installation.") + libutils.UpdateAppUsageTemplate(app, args) command, err := app.Parse(args) if err != nil { @@ -180,6 +182,8 @@ func Run(args []string) error { err = cmdSetup(ctx, &ccfg) case statusCmd.FullCommand(): err = cmdStatus(ctx, &ccfg) + case uninstallCmd.FullCommand(): + err = cmdUninstall(ctx, &ccfg) case versionCmd.FullCommand(): modules.GetModules().PrintVersion() default: @@ -418,3 +422,32 @@ func cmdStatus(ctx context.Context, ccfg *cliConfig) error { enc := yaml.NewEncoder(os.Stdout) return trace.Wrap(enc.Encode(status)) } + +// cmdUninstall removes the updater-managed install of Teleport and gracefully reverts back to the Teleport package. +func cmdUninstall(ctx context.Context, ccfg *cliConfig) error { + updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{ + DataDir: ccfg.DataDir, + LinkDir: ccfg.LinkDir, + SystemDir: autoupdate.DefaultSystemDir, + SelfSetup: ccfg.SelfSetup, + Log: plog, + }) + if err != nil { + return trace.Errorf("failed to initialize updater: %w", err) + } + // Ensure update can't run concurrently. + unlock, err := libutils.FSWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) + if err != nil { + return trace.Errorf("failed to grab concurrent execution lock: %w", err) + } + defer func() { + if err := unlock(); err != nil { + plog.DebugContext(ctx, "Failed to close lock file", "error", err) + } + }() + + if err := updater.Remove(ctx); err != nil { + return trace.Wrap(err) + } + return nil +}