Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions tool/teleport-update/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ 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.
notUpToDateExitCode = 3
)

var plog = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentUpdater)
Expand Down Expand Up @@ -87,6 +90,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 {
Expand Down Expand Up @@ -172,6 +177,9 @@ func Run(args []string) int {
Required().StringVar(&ccfg.Path)

statusCmd := app.Command("status", "Show Teleport agent auto-update status.")
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.")
uninstallCmd.Flag("force", "Force complete uninstallation of Teleport, even if there is no packaged version of Teleport to revert to.").
Expand Down Expand Up @@ -202,6 +210,8 @@ func Run(args []string) int {
autoupdate.SetRequiredUmask(ctx, plog)
}

var successExitCode int

switch command {
case enableCmd.FullCommand():
ccfg.Enabled = true
Expand All @@ -226,7 +236,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
Expand All @@ -239,7 +249,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 {
Expand Down Expand Up @@ -465,18 +475,32 @@ 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)
}

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.String() != status.Target.String() {
return notUpToDateExitCode
}
return 0
}

// cmdUninstall removes the updater-managed install of Teleport and gracefully reverts back to the Teleport package.
Expand Down
111 changes: 111 additions & 0 deletions tool/teleport-update/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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))
})
}
}
Loading