From 4fe77c2a105751b936628155698f7dfb721b2592 Mon Sep 17 00:00:00 2001 From: Stephen Levine Date: Thu, 31 Oct 2024 15:48:55 -0400 Subject: [PATCH] Add update subcommand --- lib/autoupdate/agent/installer.go | 1 + .../FIPS_and_Enterprise_flags.golden | 10 + .../backup_version_kept_when_no_change.golden | 10 + .../backup_version_removed_on_install.golden | 10 + .../updates_disabled_during_window.golden | 10 + .../updates_disabled_outside_of_window.golden | 10 + .../updates_enabled_during_window.golden | 10 + .../updates_enabled_outside_of_window.golden | 10 + ...version_already_installed_in_window.golden | 10 + ...already_installed_outside_of_window.golden | 10 + lib/autoupdate/agent/updater.go | 208 +++++++--- lib/autoupdate/agent/updater_test.go | 389 +++++++++++++++++- tool/teleport-update/main.go | 3 +- 13 files changed, 638 insertions(+), 53 deletions(-) create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/FIPS_and_Enterprise_flags.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/backup_version_kept_when_no_change.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/backup_version_removed_on_install.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_during_window.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_outside_of_window.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_during_window.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_outside_of_window.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/version_already_installed_in_window.golden create mode 100644 lib/autoupdate/agent/testdata/TestUpdater_Update/version_already_installed_outside_of_window.golden diff --git a/lib/autoupdate/agent/installer.go b/lib/autoupdate/agent/installer.go index ce3b7a63fc07d..0374433287010 100644 --- a/lib/autoupdate/agent/installer.go +++ b/lib/autoupdate/agent/installer.go @@ -499,6 +499,7 @@ func tryLink(oldname, newname string) (orig string, err error) { if orig == oldname { return "", nil } + // TODO(sclevine): verify oldname is valid binary err = renameio.Symlink(oldname, newname) if err != nil { return orig, trace.Wrap(err) diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/FIPS_and_Enterprise_flags.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/FIPS_and_Enterprise_flags.golden new file mode 100644 index 0000000000000..066926264d28e --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/FIPS_and_Enterprise_flags.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: "" + url_template: https://example.com + enabled: true +status: + active_version: 16.3.0 + backup_version: old-version diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/backup_version_kept_when_no_change.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/backup_version_kept_when_no_change.golden new file mode 100644 index 0000000000000..646397b4713d7 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/backup_version_kept_when_no_change.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: "" + url_template: https://example.com + enabled: true +status: + active_version: 16.3.0 + backup_version: backup-version diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/backup_version_removed_on_install.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/backup_version_removed_on_install.golden new file mode 100644 index 0000000000000..066926264d28e --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/backup_version_removed_on_install.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: "" + url_template: https://example.com + enabled: true +status: + active_version: 16.3.0 + backup_version: old-version diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_during_window.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_during_window.golden new file mode 100644 index 0000000000000..dc449aab8503e --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_during_window.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: group + url_template: https://example.com + enabled: false +status: + active_version: old-version + backup_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_outside_of_window.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_outside_of_window.golden new file mode 100644 index 0000000000000..dc449aab8503e --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_outside_of_window.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: group + url_template: https://example.com + enabled: false +status: + active_version: old-version + backup_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_during_window.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_during_window.golden new file mode 100644 index 0000000000000..61e41f76ca234 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_during_window.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: group + url_template: https://example.com + enabled: true +status: + active_version: 16.3.0 + backup_version: old-version diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_outside_of_window.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_outside_of_window.golden new file mode 100644 index 0000000000000..802c475ba90f4 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_outside_of_window.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: group + url_template: https://example.com + enabled: true +status: + active_version: old-version + backup_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/version_already_installed_in_window.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/version_already_installed_in_window.golden new file mode 100644 index 0000000000000..6e16d193a8eb0 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/version_already_installed_in_window.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: "" + url_template: https://example.com + enabled: true +status: + active_version: 16.3.0 + backup_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/version_already_installed_outside_of_window.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/version_already_installed_outside_of_window.golden new file mode 100644 index 0000000000000..6e16d193a8eb0 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/version_already_installed_outside_of_window.golden @@ -0,0 +1,10 @@ +version: v1 +kind: update_config +spec: + proxy: localhost + group: "" + url_template: https://example.com + enabled: true +status: + active_version: 16.3.0 + backup_version: "" diff --git a/lib/autoupdate/agent/updater.go b/lib/autoupdate/agent/updater.go index b82c3c6d419cb..35e81fbfe67b3 100644 --- a/lib/autoupdate/agent/updater.go +++ b/lib/autoupdate/agent/updater.go @@ -58,6 +58,14 @@ const ( updateConfigKind = "update_config" ) +// Log keys +const ( + targetVersionKey = "targetVersion" + activeVersionKey = "activeVersion" + backupVersionKey = "backupVersion" + errorKey = "error" +) + // UpdateConfig describes the update.yaml file schema. type UpdateConfig struct { // Version of the configuration file @@ -243,6 +251,8 @@ type OverrideConfig struct { URLTemplate string // ForceVersion to the specified version. ForceVersion string + // ForceFlags in installed Teleport. + ForceFlags InstallFlags } // Enable enables agent updates and attempts an initial update. @@ -258,6 +268,9 @@ func (u *Updater) Enable(ctx context.Context, override OverrideConfig) error { if err := validateConfigSpec(&cfg.Spec, override); err != nil { return trace.Wrap(err) } + if cfg.Spec.Proxy == "" { + return trace.Errorf("Teleport proxy URL must be specified with --proxy or present in %s", updateConfigName) + } // Lookup target version from the proxy. @@ -265,9 +278,9 @@ func (u *Updater) Enable(ctx context.Context, override OverrideConfig) error { if err != nil { return trace.Errorf("failed to parse proxy server address: %w", err) } - desiredVersion := override.ForceVersion - var flags InstallFlags - if desiredVersion == "" { + targetVersion := override.ForceVersion + flags := override.ForceFlags + if targetVersion == "" { resp, err := webclient.Find(&webclient.Config{ Context: ctx, ProxyAddr: addr.Addr, @@ -279,7 +292,7 @@ func (u *Updater) Enable(ctx context.Context, override OverrideConfig) error { if err != nil { return trace.Errorf("failed to request version from proxy: %w", err) } - desiredVersion = resp.AutoUpdate.AgentVersion + targetVersion = resp.AutoUpdate.AgentVersion if resp.Edition == "ent" { flags |= FlagEnterprise } @@ -288,20 +301,143 @@ func (u *Updater) Enable(ctx context.Context, override OverrideConfig) error { } } - if desiredVersion == "" { + if targetVersion == "" { return trace.Errorf("agent version not available from Teleport cluster") } - switch cfg.Status.BackupVersion { - case "", desiredVersion, cfg.Status.ActiveVersion: + + if err := u.update(ctx, cfg, targetVersion, flags); err != nil { + return trace.Wrap(err) + } + + // Always write the configuration file if enable succeeds. + + cfg.Spec.Enabled = true + if err := writeConfig(u.ConfigPath, cfg); err != nil { + return trace.Errorf("failed to write %s: %w", updateConfigName, err) + } + u.Log.InfoContext(ctx, "Configuration updated.") + return nil +} + +// Disable disables agent auto-updates. +// This function is idempotent. +func (u *Updater) Disable(ctx context.Context) error { + cfg, err := readConfig(u.ConfigPath) + if err != nil { + return trace.Errorf("failed to read %s: %w", updateConfigName, err) + } + if !cfg.Spec.Enabled { + u.Log.InfoContext(ctx, "Automatic updates already disabled.") + return nil + } + cfg.Spec.Enabled = false + if err := writeConfig(u.ConfigPath, cfg); err != nil { + return trace.Errorf("failed to write %s: %w", updateConfigName, err) + } + return nil +} + +// Update initiates an agent update. +// If the update succeeds, the new installed version is marked as active. +// Otherwise, the auto-updates configuration is not changed. +// Unlike Enable, Update will not validate or repair the current version. +// This function is idempotent. +func (u *Updater) Update(ctx context.Context) error { + // Read configuration from update.yaml and override any new values passed as flags. + 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 !cfg.Spec.Enabled { + u.Log.InfoContext(ctx, "Automatic updates disabled.", activeVersionKey, activeVersion) + return nil + } + if cfg.Spec.Proxy == "" { + return trace.Errorf("Teleport proxy URL must be present in %s", updateConfigName) + } + + // Lookup target version from the proxy. + + addr, err := libutils.ParseAddr(cfg.Spec.Proxy) + if err != nil { + return trace.Errorf("failed to parse proxy server address: %w", err) + } + resp, err := webclient.Find(&webclient.Config{ + Context: ctx, + ProxyAddr: addr.Addr, + Insecure: u.InsecureSkipVerify, + Timeout: 30 * time.Second, + UpdateGroup: cfg.Spec.Group, + Pool: u.Pool, + }) + if err != nil { + return trace.Errorf("failed to request version from proxy: %w", err) + } + targetVersion := resp.AutoUpdate.AgentVersion + var flags InstallFlags + if resp.Edition == "ent" { + flags |= FlagEnterprise + } + if resp.FIPS { + flags |= FlagFIPS + } + + if !resp.AutoUpdate.AgentAutoUpdate { + switch targetVersion { + case "": + u.Log.WarnContext(ctx, "Cannot determine target agent version. Waiting for both version and update window.") + case activeVersion: + u.Log.InfoContext(ctx, "Teleport is up-to-date. Update window is not active.", activeVersionKey, activeVersion) + default: + u.Log.InfoContext(ctx, "Update available, but update window is not active.", targetVersionKey, targetVersion, activeVersionKey, activeVersion) + } + return nil + } + + switch targetVersion { + case "": + u.Log.ErrorContext(ctx, "Update window is active, but target version is not available.", activeVersionKey, activeVersion) + return trace.Errorf("target version missing") + case activeVersion: + u.Log.InfoContext(ctx, "Teleport is up-to-date. Update window is active, but no action is needed.", activeVersionKey, activeVersion) + return nil + default: + u.Log.InfoContext(ctx, "Update available. Initiating update.", targetVersionKey, targetVersion, activeVersionKey, activeVersion) + } + + jitterSec := resp.AutoUpdate.AgentUpdateJitterSeconds + time.Sleep(time.Duration(jitterSec) * time.Second) + + if err := u.update(ctx, cfg, targetVersion, flags); err != nil { + return trace.Wrap(err) + } + + // Write the configuration file if update succeeds. + + if err := writeConfig(u.ConfigPath, cfg); err != nil { + return trace.Errorf("failed to write %s: %w", updateConfigName, err) + } + u.Log.InfoContext(ctx, "Configuration updated.") + return nil +} + +func (u *Updater) update(ctx context.Context, cfg *UpdateConfig, targetVersion string, flags InstallFlags) error { + activeVersion := cfg.Status.ActiveVersion + switch v := cfg.Status.BackupVersion; v { + case "", targetVersion, activeVersion: default: - if desiredVersion == cfg.Status.ActiveVersion { + if targetVersion == activeVersion { // Keep backup version if we are only verifying active version break } - err := u.Installer.Remove(ctx, cfg.Status.BackupVersion) + err := u.Installer.Remove(ctx, v) if err != nil { // this could happen if it was already removed due to a failed installation - u.Log.WarnContext(ctx, "Failed to remove backup version of Teleport before new install.", "error", err) + u.Log.WarnContext(ctx, "Failed to remove backup version of Teleport before new install.", errorKey, err, backupVersionKey, v) } } @@ -311,11 +447,11 @@ func (u *Updater) Enable(ctx context.Context, override OverrideConfig) error { if template == "" { template = cdnURITemplate } - err = u.Installer.Install(ctx, desiredVersion, template, flags) + err := u.Installer.Install(ctx, targetVersion, template, flags) if err != nil { return trace.Errorf("failed to install: %w", err) } - revert, err := u.Installer.Link(ctx, desiredVersion) + revert, err := u.Installer.Link(ctx, targetVersion) if err != nil { return trace.Errorf("failed to link: %w", err) } @@ -334,17 +470,17 @@ func (u *Updater) Enable(ctx context.Context, override OverrideConfig) error { if ok := revert(ctx); !ok { u.Log.ErrorContext(ctx, "Failed to revert Teleport symlinks. Installation likely broken.") } else if err := u.Process.Sync(ctx); err != nil { - u.Log.ErrorContext(ctx, "Failed to sync configuration after failed restart.", "error", err) + u.Log.ErrorContext(ctx, "Failed to sync configuration after failed restart.", errorKey, err) } u.Log.WarnContext(ctx, "Teleport updater encountered a configuration error and successfully reverted the installation.") - return trace.Errorf("failed to validate configuration for new version %q of Teleport: %w", desiredVersion, err) + return trace.Errorf("failed to validate configuration for new version %q of Teleport: %w", targetVersion, err) } // Restart Teleport if necessary. - if cfg.Status.ActiveVersion != desiredVersion { - u.Log.InfoContext(ctx, "Target version successfully installed.", "version", desiredVersion) + if cfg.Status.ActiveVersion != targetVersion { + u.Log.InfoContext(ctx, "Target version successfully installed.", targetVersionKey, targetVersion) if err := u.Process.Reload(ctx); err != nil && !errors.Is(err, ErrNotNeeded) { if errors.Is(err, context.Canceled) { return trace.Errorf("reload canceled") @@ -354,21 +490,21 @@ func (u *Updater) Enable(ctx context.Context, override OverrideConfig) error { if ok := revert(ctx); !ok { u.Log.ErrorContext(ctx, "Failed to revert Teleport symlinks to older version. Installation likely broken.") } else if err := u.Process.Sync(ctx); err != nil { - u.Log.ErrorContext(ctx, "Invalid configuration found after reverting Teleport to older version. Installation likely broken.", "error", err) + u.Log.ErrorContext(ctx, "Invalid configuration found after reverting Teleport to older version. Installation likely broken.", errorKey, err) } else if err := u.Process.Reload(ctx); err != nil && !errors.Is(err, ErrNotNeeded) { - u.Log.ErrorContext(ctx, "Failed to revert Teleport to older version. Installation likely broken.", "error", err) + u.Log.ErrorContext(ctx, "Failed to revert Teleport to older version. Installation likely broken.", errorKey, err) } u.Log.WarnContext(ctx, "Teleport updater encountered a configuration error and successfully reverted the installation.") - return trace.Errorf("failed to start new version %q of Teleport: %w", desiredVersion, err) + return trace.Errorf("failed to start new version %q of Teleport: %w", targetVersion, err) } cfg.Status.BackupVersion = cfg.Status.ActiveVersion - cfg.Status.ActiveVersion = desiredVersion + cfg.Status.ActiveVersion = targetVersion } else { - u.Log.InfoContext(ctx, "Target version successfully validated.", "version", desiredVersion) + u.Log.InfoContext(ctx, "Target version successfully validated.", targetVersionKey, targetVersion) } if v := cfg.Status.BackupVersion; v != "" { - u.Log.InfoContext(ctx, "Backup version set.", "version", v) + u.Log.InfoContext(ctx, "Backup version set.", "backupVersion", v) } // Check if manual cleanup might be needed. @@ -381,31 +517,6 @@ func (u *Updater) Enable(ctx context.Context, override OverrideConfig) error { u.Log.WarnContext(ctx, "More than 2 versions of Teleport installed. Version directory may need cleanup to save space.", "count", n) } - // Always write the configuration file if enable succeeds. - - cfg.Spec.Enabled = true - if err := writeConfig(u.ConfigPath, cfg); err != nil { - return trace.Errorf("failed to write %s: %w", updateConfigName, err) - } - u.Log.InfoContext(ctx, "Configuration updated.") - return nil -} - -// Disable disables agent auto-updates. -// This function is idempotent. -func (u *Updater) Disable(ctx context.Context) error { - cfg, err := readConfig(u.ConfigPath) - if err != nil { - return trace.Errorf("failed to read %s: %w", updateConfigName, err) - } - if !cfg.Spec.Enabled { - u.Log.InfoContext(ctx, "Automatic updates already disabled.") - return nil - } - cfg.Spec.Enabled = false - if err := writeConfig(u.ConfigPath, cfg); err != nil { - return trace.Errorf("failed to write %s: %w", updateConfigName, err) - } return nil } @@ -467,8 +578,5 @@ func validateConfigSpec(spec *UpdateSpec, override OverrideConfig) error { !strings.HasPrefix(strings.ToLower(spec.URLTemplate), "https://") { return trace.Errorf("Teleport download URL must use TLS (https://)") } - if spec.Proxy == "" { - return trace.Errorf("Teleport proxy URL must be specified with --proxy or present in %s", updateConfigName) - } return nil } diff --git a/lib/autoupdate/agent/updater_test.go b/lib/autoupdate/agent/updater_test.go index 8cefd3a59e3e7..16943d8a3e7e1 100644 --- a/lib/autoupdate/agent/updater_test.go +++ b/lib/autoupdate/agent/updater_test.go @@ -123,6 +123,393 @@ func TestUpdater_Disable(t *testing.T) { } } +func TestUpdater_Update(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cfg *UpdateConfig // nil -> file not present + flags InstallFlags + inWindow bool + installErr error + syncErr error + reloadErr error + + removedVersion string + installedVersion string + installedTemplate string + requestGroup string + syncCalls int + reloadCalls int + revertCalls int + errMatch string + }{ + { + name: "updates enabled during window", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + Group: "group", + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "old-version", + }, + }, + inWindow: true, + + installedVersion: "16.3.0", + installedTemplate: "https://example.com", + requestGroup: "group", + syncCalls: 1, + reloadCalls: 1, + }, + { + name: "updates disabled during window", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + Group: "group", + URLTemplate: "https://example.com", + Enabled: false, + }, + Status: UpdateStatus{ + ActiveVersion: "old-version", + }, + }, + inWindow: true, + }, + { + name: "updates enabled outside of window", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + Group: "group", + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "old-version", + }, + }, + requestGroup: "group", + }, + { + name: "updates disabled outside of window", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + Group: "group", + URLTemplate: "https://example.com", + Enabled: false, + }, + Status: UpdateStatus{ + ActiveVersion: "old-version", + }, + }, + }, + { + name: "insecure URL", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "http://example.com", + Enabled: true, + }, + }, + inWindow: true, + + errMatch: "URL must use TLS", + }, + { + name: "install error", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "https://example.com", + Enabled: true, + }, + }, + inWindow: true, + installErr: errors.New("install error"), + + errMatch: "install error", + }, + { + name: "version already installed in window", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "16.3.0", + }, + }, + inWindow: true, + }, + { + name: "version already installed outside of window", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "16.3.0", + }, + }, + }, + { + name: "backup version removed on install", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "old-version", + BackupVersion: "backup-version", + }, + }, + inWindow: true, + + installedVersion: "16.3.0", + installedTemplate: "https://example.com", + removedVersion: "backup-version", + syncCalls: 1, + reloadCalls: 1, + }, + { + name: "backup version kept when no change", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "16.3.0", + BackupVersion: "backup-version", + }, + }, + inWindow: true, + }, + { + name: "config does not exist", + }, + { + name: "FIPS and Enterprise flags", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "old-version", + BackupVersion: "backup-version", + }, + }, + inWindow: true, + flags: FlagEnterprise | FlagFIPS, + + installedVersion: "16.3.0", + installedTemplate: "https://example.com", + removedVersion: "backup-version", + syncCalls: 1, + reloadCalls: 1, + }, + { + name: "invalid metadata", + cfg: &UpdateConfig{}, + errMatch: "invalid", + }, + { + name: "sync fails", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "old-version", + BackupVersion: "backup-version", + }, + }, + inWindow: true, + syncErr: errors.New("sync error"), + + installedVersion: "16.3.0", + installedTemplate: "https://example.com", + removedVersion: "backup-version", + syncCalls: 2, + reloadCalls: 0, + revertCalls: 1, + errMatch: "sync error", + }, + { + name: "reload fails", + cfg: &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + Spec: UpdateSpec{ + URLTemplate: "https://example.com", + Enabled: true, + }, + Status: UpdateStatus{ + ActiveVersion: "old-version", + BackupVersion: "backup-version", + }, + }, + inWindow: true, + reloadErr: errors.New("reload error"), + + installedVersion: "16.3.0", + installedTemplate: "https://example.com", + removedVersion: "backup-version", + syncCalls: 2, + reloadCalls: 2, + revertCalls: 1, + errMatch: "reload error", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + var requestedGroup string + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestedGroup = r.URL.Query().Get("group") + config := webclient.PingResponse{ + AutoUpdate: webclient.AutoUpdateSettings{ + AgentVersion: "16.3.0", + AgentAutoUpdate: tt.inWindow, + }, + } + if tt.flags&FlagEnterprise != 0 { + config.Edition = "ent" + } + config.FIPS = tt.flags&FlagFIPS != 0 + err := json.NewEncoder(w).Encode(config) + require.NoError(t, err) + })) + t.Cleanup(server.Close) + + dir := t.TempDir() + cfgPath := filepath.Join(dir, "update.yaml") + + // Create config file only if provided in test case + if tt.cfg != nil { + tt.cfg.Spec.Proxy = strings.TrimPrefix(server.URL, "https://") + b, err := yaml.Marshal(tt.cfg) + require.NoError(t, err) + err = os.WriteFile(cfgPath, b, 0600) + require.NoError(t, err) + } + + updater, err := NewLocalUpdater(LocalUpdaterConfig{ + InsecureSkipVerify: true, + VersionsDir: dir, + }) + require.NoError(t, err) + + var ( + installedVersion string + installedTemplate string + linkedVersion string + removedVersion string + installedFlags InstallFlags + revertCalls int + ) + updater.Installer = &testInstaller{ + FuncInstall: func(_ context.Context, version, template string, flags InstallFlags) error { + installedVersion = version + installedTemplate = template + installedFlags = flags + return tt.installErr + }, + FuncLink: func(_ context.Context, version string) (revert func(context.Context) bool, err error) { + linkedVersion = version + return func(_ context.Context) bool { + revertCalls++ + return true + }, nil + }, + FuncList: func(_ context.Context) (versions []string, err error) { + return []string{"old"}, nil + }, + FuncRemove: func(_ context.Context, version string) error { + removedVersion = version + return nil + }, + } + var ( + syncCalls int + reloadCalls int + ) + updater.Process = &testProcess{ + FuncSync: func(_ context.Context) error { + syncCalls++ + return tt.syncErr + }, + FuncReload: func(_ context.Context) error { + reloadCalls++ + return tt.reloadErr + }, + } + + ctx := context.Background() + err = updater.Update(ctx) + if tt.errMatch != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errMatch) + return + } + require.NoError(t, err) + require.Equal(t, tt.installedVersion, installedVersion) + require.Equal(t, tt.installedTemplate, installedTemplate) + require.Equal(t, tt.installedVersion, linkedVersion) + require.Equal(t, tt.removedVersion, removedVersion) + require.Equal(t, tt.flags, installedFlags) + require.Equal(t, tt.requestGroup, requestedGroup) + require.Equal(t, tt.syncCalls, syncCalls) + require.Equal(t, tt.reloadCalls, reloadCalls) + require.Equal(t, tt.revertCalls, revertCalls) + + if tt.cfg == nil { + return + } + + data, err := os.ReadFile(cfgPath) + require.NoError(t, err) + data = blankTestAddr(data) + + if golden.ShouldSet() { + golden.Set(t, data) + } + require.Equal(t, string(golden.Get(t)), string(data)) + }) + } +} + func TestUpdater_Enable(t *testing.T) { t.Parallel() @@ -130,8 +517,8 @@ func TestUpdater_Enable(t *testing.T) { name string cfg *UpdateConfig // nil -> file not present userCfg OverrideConfig - installErr error flags InstallFlags + installErr error syncErr error reloadErr error diff --git a/tool/teleport-update/main.go b/tool/teleport-update/main.go index 2adce83a1877c..93515a9ccc688 100644 --- a/tool/teleport-update/main.go +++ b/tool/teleport-update/main.go @@ -115,12 +115,11 @@ func Run(args []string) error { Short('t').Envar(templateEnvVar).StringVar(&ccfg.URLTemplate) enableCmd.Flag("force-version", "Force the provided version instead of querying it from the Teleport cluster."). Short('f').Envar(updateVersionEnvVar).Hidden().StringVar(&ccfg.ForceVersion) + // TODO(sclevine): add force-fips and force-enterprise as hidden flags disableCmd := app.Command("disable", "Disable agent auto-updates.") updateCmd := app.Command("update", "Update agent to the latest version, if a new version is available.") - updateCmd.Flag("force-version", "Use the provided version instead of querying it from the Teleport cluster."). - Short('f').Envar(updateVersionEnvVar).Hidden().StringVar(&ccfg.ForceVersion) libutils.UpdateAppUsageTemplate(app, args) command, err := app.Parse(args)