From 45ad4706b5e4dd5d42689d2fb0281bcfc0b391cc Mon Sep 17 00:00:00 2001 From: hugoShaka Date: Wed, 11 Jun 2025 16:45:20 -0400 Subject: [PATCH 1/3] Add `teleport status --is-up-to-date` --- tool/teleport-update/main.go | 38 +++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/tool/teleport-update/main.go b/tool/teleport-update/main.go index 969718d2d2926..73a61ee9bac7a 100644 --- a/tool/teleport-update/main.go +++ b/tool/teleport-update/main.go @@ -56,6 +56,14 @@ const ( updateVersionEnvVar = "TELEPORT_UPDATE_VERSION" // updateLockTimeout is the duration commands will wait for update to complete before failing. updateLockTimeout = 10 * time.Minute + + // notUpToDateExitCode is returned by `teleport-update status --is-up-to-date` if Teleport is not up-to-date. + // We don't want to use the exit code 1 as this makes "failure" and "out-of-date" results undifferentiable. + // Bash reserves codes between 126 and 165: https://tldp.org/LDP/abs/html/exitcodes.html + // Systemd reserved code >= 200: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#Process%20Exit%20Codes + // Linux recommends codes 150-199 for application use. + // Hence, the first available recommended exit code is 166. + notUpToDateExitCode = 166 ) var plog = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentUpdater) @@ -87,6 +95,8 @@ type cliConfig struct { ForceUninstall bool // Insecure skips TLS certificate verification. Insecure bool + // StatusWithExitCode makes the status command return different exit codes depending on the update status. + StatusWithExitCode bool } func Run(args []string) int { @@ -172,6 +182,9 @@ func Run(args []string) int { Required().StringVar(&ccfg.Path) statusCmd := app.Command("status", "Show Teleport agent auto-update status.") + statusCmd.Flag("is-up-to-date", + fmt.Sprintf("Exits with code 0 if Teleport is up-to-date, and with code %d if 'teleport-update update' would attempt an update now.", notUpToDateExitCode), + ).BoolVar(&ccfg.StatusWithExitCode) uninstallCmd := app.Command("uninstall", "Uninstall the updater-managed installation of Teleport. If the Teleport package is installed, it is restored as the primary installation.") uninstallCmd.Flag("force", "Force complete uninstallation of Teleport, even if there is no packaged version of Teleport to revert to."). @@ -202,6 +215,8 @@ func Run(args []string) int { autoupdate.SetRequiredUmask(ctx, plog) } + var successExitCode int + switch command { case enableCmd.FullCommand(): ccfg.Enabled = true @@ -226,7 +241,7 @@ func Run(args []string) int { case versionCmd.FullCommand(): modules.GetModules().PrintVersion() case statusCmd.FullCommand(): - err = cmdStatus(ctx, &ccfg) + successExitCode, err = cmdStatus(ctx, &ccfg) if errors.Is(err, autoupdate.ErrNotInstalled) { plog.ErrorContext(ctx, "Teleport is not installed by teleport-update with this suffix.") return 1 @@ -239,7 +254,7 @@ func Run(args []string) int { plog.ErrorContext(ctx, "Command failed.", "error", err) return 1 } - return 0 + return successExitCode } func setupLogger(debug bool, format string) error { @@ -465,18 +480,27 @@ func cmdSetup(ctx context.Context, ccfg *cliConfig) error { return nil } -// cmdStatus displays auto-update status. -func cmdStatus(ctx context.Context, ccfg *cliConfig) error { +// cmdStatus displays auto-update status. The command also returns the desired +// error code (only valid if the error is nil). +func cmdStatus(ctx context.Context, ccfg *cliConfig) (int, error) { updater, err := statusConfig(ctx, ccfg) if err != nil { - return trace.Wrap(err, "failed to initialize updater") + return 0, trace.Wrap(err, "failed to initialize updater") } status, err := updater.Status(ctx) if err != nil { - return trace.Wrap(err, "failed to get status") + return 0, trace.Wrap(err, "failed to get status") } enc := yaml.NewEncoder(os.Stdout) - return trace.Wrap(enc.Encode(status)) + if err := enc.Encode(status); err != nil { + return 0, trace.Wrap(err) + } + + // Implement --is-up-to-date + if ccfg.StatusWithExitCode && status.InWindow && status.Active.Version != status.Target.Version { + return notUpToDateExitCode, nil + } + return 0, nil } // cmdUninstall removes the updater-managed install of Teleport and gracefully reverts back to the Teleport package. From bd36250d77db9c30a69ad52548610897aa871c51 Mon Sep 17 00:00:00 2001 From: hugoShaka Date: Fri, 13 Jun 2025 09:59:26 -0400 Subject: [PATCH 2/3] Add tests + update on edition mismatch --- tool/teleport-update/main.go | 11 ++- tool/teleport-update/main_test.go | 111 ++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/tool/teleport-update/main.go b/tool/teleport-update/main.go index 73a61ee9bac7a..3c13e6e71f34f 100644 --- a/tool/teleport-update/main.go +++ b/tool/teleport-update/main.go @@ -496,11 +496,16 @@ func cmdStatus(ctx context.Context, ccfg *cliConfig) (int, error) { return 0, trace.Wrap(err) } + return statusExitCode(ccfg, status), nil +} + +// statusExitCode returns the desired exit code for the status command. +func statusExitCode(ccfg *cliConfig, status autoupdate.Status) int { // Implement --is-up-to-date - if ccfg.StatusWithExitCode && status.InWindow && status.Active.Version != status.Target.Version { - return notUpToDateExitCode, nil + if ccfg.StatusWithExitCode && status.InWindow && status.Active.String() != status.Target.String() { + return notUpToDateExitCode } - return 0, nil + return 0 } // cmdUninstall removes the updater-managed install of Teleport and gracefully reverts back to the Teleport package. diff --git a/tool/teleport-update/main_test.go b/tool/teleport-update/main_test.go index 313b9bd3e43b0..aafe00784ce18 100644 --- a/tool/teleport-update/main_test.go +++ b/tool/teleport-update/main_test.go @@ -24,6 +24,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + autoupdatelib "github.com/gravitational/teleport/lib/autoupdate" + autoupdate "github.com/gravitational/teleport/lib/autoupdate/agent" ) const initTestSentinel = "init_test" @@ -46,3 +49,111 @@ func BenchmarkInit(b *testing.B) { assert.NoError(b, err) } } + +func TestStatusExitCode(t *testing.T) { + lowVersion := "1.2.3" + highVersion := "1.2.4" + tests := []struct { + name string + ccfg *cliConfig + status autoupdate.Status + expectedExitCode int + }{ + { + name: "no --is-up-to-date passed, should update", + ccfg: &cliConfig{StatusWithExitCode: false}, + status: autoupdate.Status{ + UpdateStatus: autoupdate.UpdateStatus{ + Active: autoupdate.Revision{ + Version: lowVersion, + }, + }, + FindResp: autoupdate.FindResp{ + Target: autoupdate.Revision{ + Version: highVersion, + }, + InWindow: true, + }, + }, + expectedExitCode: 0, + }, + { + name: "--is-up-to-date passed, different version in maintenance", + ccfg: &cliConfig{StatusWithExitCode: true}, + status: autoupdate.Status{ + UpdateStatus: autoupdate.UpdateStatus{ + Active: autoupdate.Revision{ + Version: lowVersion, + }, + }, + FindResp: autoupdate.FindResp{ + Target: autoupdate.Revision{ + Version: highVersion, + }, + InWindow: true, + }, + }, + expectedExitCode: notUpToDateExitCode, + }, + { + name: "--is-up-to-date passed, different version out of maintenance", + ccfg: &cliConfig{StatusWithExitCode: true}, + status: autoupdate.Status{ + UpdateStatus: autoupdate.UpdateStatus{ + Active: autoupdate.Revision{ + Version: lowVersion, + }, + }, + FindResp: autoupdate.FindResp{ + Target: autoupdate.Revision{ + Version: highVersion, + }, + InWindow: false, + }, + }, + expectedExitCode: 0, + }, + { + name: "--is-up-to-date passed, same version in maintenance", + ccfg: &cliConfig{StatusWithExitCode: true}, + status: autoupdate.Status{ + UpdateStatus: autoupdate.UpdateStatus{ + Active: autoupdate.Revision{ + Version: highVersion, + }, + }, + FindResp: autoupdate.FindResp{ + Target: autoupdate.Revision{ + Version: highVersion, + }, + InWindow: true, + }, + }, + expectedExitCode: 0, + }, + { + name: "--is-up-to-date passed, same version in maintenance, edition mismatch", + ccfg: &cliConfig{StatusWithExitCode: true}, + status: autoupdate.Status{ + UpdateStatus: autoupdate.UpdateStatus{ + Active: autoupdate.Revision{ + Version: highVersion, + }, + }, + FindResp: autoupdate.FindResp{ + Target: autoupdate.Revision{ + Version: highVersion, + Flags: autoupdatelib.FlagEnterprise, + }, + InWindow: true, + }, + }, + expectedExitCode: notUpToDateExitCode, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expectedExitCode, statusExitCode(tt.ccfg, tt.status)) + }) + } +} From 37e48bc3a57fbc1f05e117172f6731d189522c90 Mon Sep 17 00:00:00 2001 From: hugoShaka Date: Fri, 20 Jun 2025 10:23:30 -0400 Subject: [PATCH 3/3] Address Stephen's feedback --- tool/teleport-update/main.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tool/teleport-update/main.go b/tool/teleport-update/main.go index 3c13e6e71f34f..8e6890a5baf2a 100644 --- a/tool/teleport-update/main.go +++ b/tool/teleport-update/main.go @@ -58,12 +58,7 @@ const ( updateLockTimeout = 10 * time.Minute // notUpToDateExitCode is returned by `teleport-update status --is-up-to-date` if Teleport is not up-to-date. - // We don't want to use the exit code 1 as this makes "failure" and "out-of-date" results undifferentiable. - // Bash reserves codes between 126 and 165: https://tldp.org/LDP/abs/html/exitcodes.html - // Systemd reserved code >= 200: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#Process%20Exit%20Codes - // Linux recommends codes 150-199 for application use. - // Hence, the first available recommended exit code is 166. - notUpToDateExitCode = 166 + notUpToDateExitCode = 3 ) var plog = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentUpdater) @@ -182,8 +177,8 @@ func Run(args []string) int { Required().StringVar(&ccfg.Path) statusCmd := app.Command("status", "Show Teleport agent auto-update status.") - statusCmd.Flag("is-up-to-date", - fmt.Sprintf("Exits with code 0 if Teleport is up-to-date, and with code %d if 'teleport-update update' would attempt an update now.", notUpToDateExitCode), + statusCmd.Flag("err-if-should-update-now", + fmt.Sprintf("Exits with code %d if the agent should update now. Exit code 0 means that the agent should not update now, even if it might not run the target version.", notUpToDateExitCode), ).BoolVar(&ccfg.StatusWithExitCode) uninstallCmd := app.Command("uninstall", "Uninstall the updater-managed installation of Teleport. If the Teleport package is installed, it is restored as the primary installation.")