diff --git a/lib/autoupdate/agent/config.go b/lib/autoupdate/agent/config.go index 334d1089ab7f4..a13b475f59720 100644 --- a/lib/autoupdate/agent/config.go +++ b/lib/autoupdate/agent/config.go @@ -19,98 +19,187 @@ package agent import ( - "context" - "log/slog" + "errors" + "io/fs" "os" - "path/filepath" - "text/template" + "strings" + "time" "github.com/google/renameio/v2" "github.com/gravitational/trace" + "gopkg.in/yaml.v3" ) const ( - updateServiceTemplate = `# teleport-update -[Unit] -Description=Teleport auto-update service - -[Service] -Type=oneshot -ExecStart={{.LinkDir}}/bin/teleport-update update -` - updateTimerTemplate = `# teleport-update -[Unit] -Description=Teleport auto-update timer unit - -[Timer] -OnActiveSec=1m -OnUnitActiveSec=5m -RandomizedDelaySec=1m - -[Install] -WantedBy=teleport.service -` + // updateConfigName specifies the name of the file inside versionsDirName containing configuration for the teleport update. + updateConfigName = "update.yaml" + + // UpdateConfig metadata + updateConfigVersion = "v1" + updateConfigKind = "update_config" ) -// Setup installs service and timer files for the teleport-update binary. -// Afterwords, Setup reloads systemd and enables the timer with --now. -func Setup(ctx context.Context, log *slog.Logger, linkDir, dataDir string) error { - err := writeConfigFiles(linkDir, dataDir) +// UpdateConfig describes the update.yaml file schema. +type UpdateConfig struct { + // Version of the configuration file + Version string `yaml:"version"` + // Kind of configuration file (always "update_config") + Kind string `yaml:"kind"` + // Spec contains user-specified configuration. + Spec UpdateSpec `yaml:"spec"` + // Status contains state configuration. + Status UpdateStatus `yaml:"status"` +} + +// UpdateSpec describes the spec field in update.yaml. +type UpdateSpec struct { + // Proxy address + Proxy string `yaml:"proxy"` + // Group specifies the update group identifier for the agent. + Group string `yaml:"group,omitempty"` + // URLTemplate for the Teleport tgz download URL. + URLTemplate string `yaml:"url_template,omitempty"` + // Enabled controls whether auto-updates are enabled. + Enabled bool `yaml:"enabled"` + // Pinned controls whether the active_version is pinned. + Pinned bool `yaml:"pinned"` +} + +// UpdateStatus describes the status field in update.yaml. +type UpdateStatus struct { + // ActiveVersion is the currently active Teleport version. + ActiveVersion string `yaml:"active_version"` + // BackupVersion is the last working version of Teleport. + BackupVersion string `yaml:"backup_version"` + // SkipVersion is the last reverted version of Teleport. + SkipVersion string `yaml:"skip_version,omitempty"` +} + +// readConfig reads UpdateConfig from a file. +func readConfig(path string) (*UpdateConfig, error) { + f, err := os.Open(path) + if errors.Is(err, fs.ErrNotExist) { + return &UpdateConfig{ + Version: updateConfigVersion, + Kind: updateConfigKind, + }, nil + } if err != nil { - return trace.Errorf("failed to write teleport-update systemd config files: %w", err) + return nil, trace.Errorf("failed to open: %w", err) } - svc := &SystemdService{ - ServiceName: "teleport-update.timer", - Log: log, + defer f.Close() + var cfg UpdateConfig + if err := yaml.NewDecoder(f).Decode(&cfg); err != nil { + return nil, trace.Errorf("failed to parse: %w", err) } - if err := svc.Sync(ctx); err != nil { - return trace.Errorf("failed to sync systemd config: %w", err) + if k := cfg.Kind; k != updateConfigKind { + return nil, trace.Errorf("invalid kind %q", k) } - if err := svc.Enable(ctx, true); err != nil { - return trace.Errorf("failed to enable teleport-update systemd timer: %w", err) + if v := cfg.Version; v != updateConfigVersion { + return nil, trace.Errorf("invalid version %q", v) } - return nil + return &cfg, nil } -func writeConfigFiles(linkDir, dataDir string) error { - servicePath := filepath.Join(linkDir, serviceDir, updateServiceName) - err := writeTemplate(servicePath, updateServiceTemplate, linkDir, dataDir) +// writeConfig writes UpdateConfig to a file atomically, ensuring the file cannot be corrupted. +func writeConfig(filename string, cfg *UpdateConfig) error { + opts := []renameio.Option{ + renameio.WithPermissions(configFileMode), + renameio.WithExistingPermissions(), + } + t, err := renameio.NewPendingFile(filename, opts...) if err != nil { return trace.Wrap(err) } - timerPath := filepath.Join(linkDir, serviceDir, updateTimerName) - err = writeTemplate(timerPath, updateTimerTemplate, linkDir, dataDir) + defer t.Cleanup() + err = yaml.NewEncoder(t).Encode(cfg) if err != nil { return trace.Wrap(err) } - return nil + return trace.Wrap(t.CloseAtomicallyReplace()) } -func writeTemplate(path, t, linkDir, dataDir string) error { - dir, file := filepath.Split(path) - if err := os.MkdirAll(dir, systemDirMode); err != nil { - return trace.Wrap(err) +func validateConfigSpec(spec *UpdateSpec, override OverrideConfig) error { + if override.Proxy != "" { + spec.Proxy = override.Proxy } - opts := []renameio.Option{ - renameio.WithPermissions(configFileMode), - renameio.WithExistingPermissions(), + if override.Group != "" { + spec.Group = override.Group } - f, err := renameio.NewPendingFile(path, opts...) - if err != nil { - return trace.Wrap(err) + switch override.URLTemplate { + case "": + case "default": + spec.URLTemplate = "" + default: + spec.URLTemplate = override.URLTemplate + } + if spec.URLTemplate != "" && + !strings.HasPrefix(strings.ToLower(spec.URLTemplate), "https://") { + return trace.Errorf("Teleport download URL must use TLS (https://)") } - defer f.Cleanup() + if override.Enabled { + spec.Enabled = true + } + if override.Pinned { + spec.Pinned = true + } + return nil +} - tmpl, err := template.New(file).Parse(t) - if err != nil { - return trace.Wrap(err) +// Status of the agent auto-updates system. +type Status struct { + UpdateSpec `yaml:",inline"` + UpdateStatus `yaml:",inline"` + FindResp `yaml:",inline"` +} + +// FindResp summarizes the auto-update status response from cluster. +type FindResp struct { + // Version of Teleport to install + TargetVersion string `yaml:"target_version"` + // Flags describing the edition of Teleport + Flags InstallFlags `yaml:"flags"` + // InWindow is true when the install should happen now. + InWindow bool `yaml:"in_window"` + // Jitter duration before an automated install + Jitter time.Duration `yaml:"jitter"` +} + +// InstallFlags sets flags for the Teleport installation +type InstallFlags int + +const ( + // FlagEnterprise installs enterprise Teleport + FlagEnterprise InstallFlags = 1 << iota + // FlagFIPS installs FIPS Teleport + FlagFIPS +) + +func (i InstallFlags) MarshalYAML() (any, error) { + return i.Strings(), nil +} + +func (i InstallFlags) Strings() []string { + var out []string + for _, flag := range []InstallFlags{ + FlagEnterprise, + FlagFIPS, + } { + if i&flag != 0 { + out = append(out, flag.String()) + } } - err = tmpl.Execute(f, struct { - LinkDir string - DataDir string - }{linkDir, dataDir}) - if err != nil { - return trace.Wrap(err) + return out +} + +func (i InstallFlags) String() string { + switch i { + case 0: + return "" + case FlagEnterprise: + return "Enterprise" + case FlagFIPS: + return "FIPS" } - return trace.Wrap(f.CloseAtomicallyReplace()) + return "Unknown" } diff --git a/lib/autoupdate/agent/setup.go b/lib/autoupdate/agent/setup.go new file mode 100644 index 0000000000000..334d1089ab7f4 --- /dev/null +++ b/lib/autoupdate/agent/setup.go @@ -0,0 +1,116 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package agent + +import ( + "context" + "log/slog" + "os" + "path/filepath" + "text/template" + + "github.com/google/renameio/v2" + "github.com/gravitational/trace" +) + +const ( + updateServiceTemplate = `# teleport-update +[Unit] +Description=Teleport auto-update service + +[Service] +Type=oneshot +ExecStart={{.LinkDir}}/bin/teleport-update update +` + updateTimerTemplate = `# teleport-update +[Unit] +Description=Teleport auto-update timer unit + +[Timer] +OnActiveSec=1m +OnUnitActiveSec=5m +RandomizedDelaySec=1m + +[Install] +WantedBy=teleport.service +` +) + +// Setup installs service and timer files for the teleport-update binary. +// Afterwords, Setup reloads systemd and enables the timer with --now. +func Setup(ctx context.Context, log *slog.Logger, linkDir, dataDir string) error { + err := writeConfigFiles(linkDir, dataDir) + if err != nil { + return trace.Errorf("failed to write teleport-update systemd config files: %w", err) + } + svc := &SystemdService{ + ServiceName: "teleport-update.timer", + Log: log, + } + if err := svc.Sync(ctx); err != nil { + return trace.Errorf("failed to sync systemd config: %w", err) + } + if err := svc.Enable(ctx, true); err != nil { + return trace.Errorf("failed to enable teleport-update systemd timer: %w", err) + } + return nil +} + +func writeConfigFiles(linkDir, dataDir string) error { + servicePath := filepath.Join(linkDir, serviceDir, updateServiceName) + err := writeTemplate(servicePath, updateServiceTemplate, linkDir, dataDir) + if err != nil { + return trace.Wrap(err) + } + timerPath := filepath.Join(linkDir, serviceDir, updateTimerName) + err = writeTemplate(timerPath, updateTimerTemplate, linkDir, dataDir) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +func writeTemplate(path, t, linkDir, dataDir string) error { + dir, file := filepath.Split(path) + if err := os.MkdirAll(dir, systemDirMode); err != nil { + return trace.Wrap(err) + } + opts := []renameio.Option{ + renameio.WithPermissions(configFileMode), + renameio.WithExistingPermissions(), + } + f, err := renameio.NewPendingFile(path, opts...) + if err != nil { + return trace.Wrap(err) + } + defer f.Cleanup() + + tmpl, err := template.New(file).Parse(t) + if err != nil { + return trace.Wrap(err) + } + err = tmpl.Execute(f, struct { + LinkDir string + DataDir string + }{linkDir, dataDir}) + if err != nil { + return trace.Wrap(err) + } + return trace.Wrap(f.CloseAtomicallyReplace()) +} diff --git a/lib/autoupdate/agent/config_test.go b/lib/autoupdate/agent/setup_test.go similarity index 100% rename from lib/autoupdate/agent/config_test.go rename to lib/autoupdate/agent/setup_test.go diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Disable/already_disabled.golden b/lib/autoupdate/agent/testdata/TestUpdater_Disable/already_disabled.golden index 3f31750f4fe2e..b05a76e619cdc 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Disable/already_disabled.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Disable/already_disabled.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: "" - group: "" - url_template: "" enabled: false pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Disable/enabled.golden b/lib/autoupdate/agent/testdata/TestUpdater_Disable/enabled.golden index 3f31750f4fe2e..b05a76e619cdc 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Disable/enabled.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Disable/enabled.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: "" - group: "" - url_template: "" enabled: false pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/FIPS_and_Enterprise_flags.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/FIPS_and_Enterprise_flags.golden index 01e93f92914bb..9c2b8ef209bbd 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/FIPS_and_Enterprise_flags.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/FIPS_and_Enterprise_flags.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" - url_template: "" enabled: false pinned: false status: active_version: 16.3.0 backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/backup_version_kept_for_validation.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/backup_version_kept_for_validation.golden index 9d7a8ddc539c2..715d8d3aae94d 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/backup_version_kept_for_validation.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/backup_version_kept_for_validation.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" - url_template: "" enabled: false pinned: false status: active_version: 16.3.0 backup_version: backup-version - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/backup_version_removed_on_install.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/backup_version_removed_on_install.golden index 55aa3076c66cc..ec6bf5a101df8 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/backup_version_removed_on_install.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/backup_version_removed_on_install.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" - url_template: "" enabled: false pinned: false status: active_version: 16.3.0 backup_version: old-version - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/config_does_not_exist.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/config_does_not_exist.golden index 01e93f92914bb..9c2b8ef209bbd 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/config_does_not_exist.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/config_does_not_exist.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" - url_template: "" enabled: false pinned: false status: active_version: 16.3.0 backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/config_from_file.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/config_from_file.golden index 95cf51146d769..d63e09b2fa155 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/config_from_file.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/config_from_file.golden @@ -9,4 +9,3 @@ spec: status: active_version: 16.3.0 backup_version: old-version - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/config_from_user.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/config_from_user.golden index 02157c7f6751a..52b33b0111955 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/config_from_user.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/config_from_user.golden @@ -9,4 +9,3 @@ spec: status: active_version: new-version backup_version: old-version - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/defaults.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/defaults.golden index 55aa3076c66cc..ec6bf5a101df8 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/defaults.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/defaults.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" - url_template: "" enabled: false pinned: false status: active_version: 16.3.0 backup_version: old-version - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/insecure_URL.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/insecure_URL.golden index fcfc5f9ee2085..4de09b7eeb217 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/insecure_URL.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/insecure_URL.golden @@ -2,11 +2,9 @@ version: v1 kind: update_config spec: proxy: "" - group: "" url_template: http://example.com enabled: false pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/install_error.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/install_error.golden index 3f31750f4fe2e..b05a76e619cdc 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/install_error.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/install_error.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: "" - group: "" - url_template: "" enabled: false pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/invalid_metadata.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/invalid_metadata.golden index 6da0a8480b84e..91cb5de367475 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/invalid_metadata.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/invalid_metadata.golden @@ -2,11 +2,8 @@ version: "" kind: "" spec: proxy: "" - group: "" - url_template: "" enabled: false pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/override_skip.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/override_skip.golden index 55aa3076c66cc..ec6bf5a101df8 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/override_skip.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/override_skip.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" - url_template: "" enabled: false pinned: false status: active_version: 16.3.0 backup_version: old-version - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Install/version_already_installed.golden b/lib/autoupdate/agent/testdata/TestUpdater_Install/version_already_installed.golden index 01e93f92914bb..9c2b8ef209bbd 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Install/version_already_installed.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Install/version_already_installed.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" - url_template: "" enabled: false pinned: false status: active_version: 16.3.0 backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Unpin/not_pinned.golden b/lib/autoupdate/agent/testdata/TestUpdater_Unpin/not_pinned.golden index 3f31750f4fe2e..b05a76e619cdc 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Unpin/not_pinned.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Unpin/not_pinned.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: "" - group: "" - url_template: "" enabled: false pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Unpin/pinned.golden b/lib/autoupdate/agent/testdata/TestUpdater_Unpin/pinned.golden index 3f31750f4fe2e..b05a76e619cdc 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Unpin/pinned.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Unpin/pinned.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: "" - group: "" - url_template: "" enabled: false pinned: false status: active_version: "" backup_version: "" - skip_version: "" 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 index 7a1a7032fb885..0f0663cdaffc8 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/FIPS_and_Enterprise_flags.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/FIPS_and_Enterprise_flags.golden @@ -2,11 +2,9 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: false status: active_version: 16.3.0 backup_version: old-version - skip_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 index 757a156bbfa96..c56612e230890 100644 --- 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 @@ -2,11 +2,9 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: false status: active_version: 16.3.0 backup_version: backup-version - skip_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 index 7a1a7032fb885..0f0663cdaffc8 100644 --- 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 @@ -2,11 +2,9 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: false status: active_version: 16.3.0 backup_version: old-version - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/insecure_URL.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/insecure_URL.golden index c98efffd903f6..1078bb7378840 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/insecure_URL.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/insecure_URL.golden @@ -2,11 +2,9 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: http://example.com enabled: true pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/install_error.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/install_error.golden index fd57902d7d177..85c50cbb4b1e0 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/install_error.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/install_error.golden @@ -2,11 +2,8 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" - url_template: "" enabled: true pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/invalid_metadata.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/invalid_metadata.golden index a9f316edbbe7c..99be1fccf2fd7 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/invalid_metadata.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/invalid_metadata.golden @@ -2,11 +2,8 @@ version: "" kind: "" spec: proxy: localhost - group: "" - url_template: "" enabled: false pinned: false status: active_version: "" backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/pinned_version.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/pinned_version.golden index 2d14ba4eb6504..ec80fb388511c 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/pinned_version.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/pinned_version.golden @@ -2,11 +2,9 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: true status: active_version: old-version backup_version: backup-version - skip_version: "" diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/reload_fails.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/reload_fails.golden index bbf9d429ef96e..8451c7aae551b 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/reload_fails.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/reload_fails.golden @@ -2,7 +2,6 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: false diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/setup_fails.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/setup_fails.golden index bbf9d429ef96e..8451c7aae551b 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/setup_fails.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/setup_fails.golden @@ -2,7 +2,6 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: false diff --git a/lib/autoupdate/agent/testdata/TestUpdater_Update/skip_version.golden b/lib/autoupdate/agent/testdata/TestUpdater_Update/skip_version.golden index bbf9d429ef96e..8451c7aae551b 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/skip_version.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/skip_version.golden @@ -2,7 +2,6 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: false 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 index 3f50f3d6cc3fe..41bab44393b8d 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_during_window.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_disabled_during_window.golden @@ -9,4 +9,3 @@ spec: status: active_version: old-version backup_version: "" - skip_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 index 3f50f3d6cc3fe..41bab44393b8d 100644 --- 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 @@ -9,4 +9,3 @@ spec: status: active_version: old-version backup_version: "" - skip_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 index 95cf51146d769..d63e09b2fa155 100644 --- a/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_during_window.golden +++ b/lib/autoupdate/agent/testdata/TestUpdater_Update/updates_enabled_during_window.golden @@ -9,4 +9,3 @@ spec: status: active_version: 16.3.0 backup_version: old-version - skip_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 index e63a0dc9363d5..3d0ead7bbfe4d 100644 --- 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 @@ -9,4 +9,3 @@ spec: status: active_version: old-version backup_version: "" - skip_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 index 5c33b18377c84..09cfd0264e561 100644 --- 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 @@ -2,11 +2,9 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: false status: active_version: 16.3.0 backup_version: "" - skip_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 index 5c33b18377c84..09cfd0264e561 100644 --- 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 @@ -2,11 +2,9 @@ version: v1 kind: update_config spec: proxy: localhost - group: "" url_template: https://example.com enabled: true pinned: false status: active_version: 16.3.0 backup_version: "" - skip_version: "" diff --git a/lib/autoupdate/agent/updater.go b/lib/autoupdate/agent/updater.go index 4c968a0dd0f91..73cd193124dfe 100644 --- a/lib/autoupdate/agent/updater.go +++ b/lib/autoupdate/agent/updater.go @@ -23,19 +23,15 @@ import ( "crypto/tls" "crypto/x509" "errors" - "io/fs" "log/slog" "net/http" "os" "os/exec" "path/filepath" "runtime" - "strings" "time" - "github.com/google/renameio/v2" "github.com/gravitational/trace" - "gopkg.in/yaml.v3" "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/constants" @@ -72,51 +68,6 @@ const ( errorKey = "error" ) -const ( - // updateConfigName specifies the name of the file inside versionsDirName containing configuration for the teleport update. - updateConfigName = "update.yaml" - - // UpdateConfig metadata - updateConfigVersion = "v1" - updateConfigKind = "update_config" -) - -// UpdateConfig describes the update.yaml file schema. -type UpdateConfig struct { - // Version of the configuration file - Version string `yaml:"version"` - // Kind of configuration file (always "update_config") - Kind string `yaml:"kind"` - // Spec contains user-specified configuration. - Spec UpdateSpec `yaml:"spec"` - // Status contains state configuration. - Status UpdateStatus `yaml:"status"` -} - -// UpdateSpec describes the spec field in update.yaml. -type UpdateSpec struct { - // Proxy address - Proxy string `yaml:"proxy"` - // Group specifies the update group identifier for the agent. - Group string `yaml:"group"` - // URLTemplate for the Teleport tgz download URL. - URLTemplate string `yaml:"url_template"` - // Enabled controls whether auto-updates are enabled. - Enabled bool `yaml:"enabled"` - // Pinned controls whether the active_version is pinned. - Pinned bool `yaml:"pinned"` -} - -// UpdateStatus describes the status field in update.yaml. -type UpdateStatus struct { - // ActiveVersion is the currently active Teleport version. - ActiveVersion string `yaml:"active_version"` - // BackupVersion is the last working version of Teleport. - BackupVersion string `yaml:"backup_version"` - // SkipVersion is the last reverted version of Teleport. - SkipVersion string `yaml:"skip_version"` -} - // NewLocalUpdater returns a new Updater that auto-updates local // installations of the Teleport agent. // The AutoUpdater uses an HTTP client with sane defaults for downloads, and @@ -305,16 +256,7 @@ type Process interface { Sync(ctx context.Context) error } -// InstallFlags sets flags for the Teleport installation -type InstallFlags int - -// TODO(sclevine): add flags for need_restart and selinux config -const ( - // FlagEnterprise installs enterprise Teleport - FlagEnterprise InstallFlags = 1 << iota - // FlagFIPS installs FIPS Teleport - FlagFIPS -) +// TODO(sclevine): add support for need_restart and selinux config // OverrideConfig contains overrides for individual update operations. // If validated, these overrides may be persisted to disk. @@ -349,8 +291,8 @@ func (u *Updater) Install(ctx context.Context, override OverrideConfig) error { if err != nil { return trace.Wrap(err) } - targetVersion := resp.version - flags := resp.flags + targetVersion := resp.TargetVersion + flags := resp.Flags flags |= override.ForceFlags if override.ForceVersion != "" { targetVersion = override.ForceVersion @@ -382,6 +324,29 @@ func (u *Updater) Install(ctx context.Context, override OverrideConfig) error { 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 + // Read configuration from update.yaml. + cfg, err := readConfig(u.ConfigPath) + if err != nil { + return out, trace.Errorf("failed to read %s: %w", updateConfigName, err) + } + if err := validateConfigSpec(&cfg.Spec, OverrideConfig{}); err != nil { + return out, trace.Wrap(err) + } + out.UpdateSpec = cfg.Spec + out.UpdateStatus = cfg.Status + + // Lookup target version from the proxy. + resp, err := u.find(ctx, cfg) + if err != nil { + return out, trace.Wrap(err) + } + out.FindResp = resp + return out, nil +} + // Disable disables agent auto-updates. // This function is idempotent. func (u *Updater) Disable(ctx context.Context) error { @@ -443,7 +408,7 @@ func (u *Updater) Update(ctx context.Context) error { if err != nil { return trace.Wrap(err) } - targetVersion := resp.version + targetVersion := resp.TargetVersion if cfg.Spec.Pinned { switch targetVersion { @@ -455,7 +420,7 @@ func (u *Updater) Update(ctx context.Context) error { return nil } - if !resp.active { + if !resp.InWindow { switch targetVersion { case "": u.Log.WarnContext(ctx, "Cannot determine target agent version. Waiting for both version and update window.") @@ -482,9 +447,9 @@ func (u *Updater) Update(ctx context.Context) error { default: u.Log.InfoContext(ctx, "Update available. Initiating update.", targetVersionKey, targetVersion, activeVersionKey, activeVersion) } - time.Sleep(resp.jitter) + time.Sleep(resp.Jitter) - updateErr := u.update(ctx, cfg, targetVersion, resp.flags) + updateErr := u.update(ctx, cfg, targetVersion, resp.Flags) writeErr := writeConfig(u.ConfigPath, cfg) if writeErr != nil { writeErr = trace.Errorf("failed to write %s: %w", updateConfigName, writeErr) @@ -494,13 +459,13 @@ func (u *Updater) Update(ctx context.Context) error { return trace.NewAggregate(updateErr, writeErr) } -func (u *Updater) find(ctx context.Context, cfg *UpdateConfig) (findResp, error) { +func (u *Updater) find(ctx context.Context, cfg *UpdateConfig) (FindResp, error) { if cfg.Spec.Proxy == "" { - return findResp{}, trace.Errorf("Teleport proxy URL must be specified with --proxy or present in %s", updateConfigName) + return FindResp{}, trace.Errorf("Teleport proxy URL must be specified with --proxy or present in %s", updateConfigName) } addr, err := libutils.ParseAddr(cfg.Spec.Proxy) if err != nil { - return findResp{}, trace.Errorf("failed to parse proxy server address: %w", err) + return FindResp{}, trace.Errorf("failed to parse proxy server address: %w", err) } resp, err := webclient.Find(&webclient.Config{ Context: ctx, @@ -511,7 +476,7 @@ func (u *Updater) find(ctx context.Context, cfg *UpdateConfig) (findResp, error) Pool: u.Pool, }) if err != nil { - return findResp{}, trace.Errorf("failed to request version from proxy: %w", err) + return FindResp{}, trace.Errorf("failed to request version from proxy: %w", err) } var flags InstallFlags switch resp.Edition { @@ -525,21 +490,14 @@ func (u *Updater) find(ctx context.Context, cfg *UpdateConfig) (findResp, error) flags |= FlagFIPS } jitterSec := resp.AutoUpdate.AgentUpdateJitterSeconds - return findResp{ - version: resp.AutoUpdate.AgentVersion, - flags: flags, - active: resp.AutoUpdate.AgentAutoUpdate, - jitter: time.Duration(jitterSec) * time.Second, + return FindResp{ + TargetVersion: resp.AutoUpdate.AgentVersion, + Flags: flags, + InWindow: resp.AutoUpdate.AgentAutoUpdate, + Jitter: time.Duration(jitterSec) * time.Second, }, nil } -type findResp struct { - version string - flags InstallFlags - active bool - jitter time.Duration -} - func (u *Updater) update(ctx context.Context, cfg *UpdateConfig, targetVersion string, flags InstallFlags) error { activeVersion := cfg.Status.ActiveVersion switch v := cfg.Status.BackupVersion; v { @@ -652,77 +610,6 @@ func (u *Updater) update(ctx context.Context, cfg *UpdateConfig, targetVersion s return nil } -// readConfig reads UpdateConfig from a file. -func readConfig(path string) (*UpdateConfig, error) { - f, err := os.Open(path) - if errors.Is(err, fs.ErrNotExist) { - return &UpdateConfig{ - Version: updateConfigVersion, - Kind: updateConfigKind, - }, nil - } - if err != nil { - return nil, trace.Errorf("failed to open: %w", err) - } - defer f.Close() - var cfg UpdateConfig - if err := yaml.NewDecoder(f).Decode(&cfg); err != nil { - return nil, trace.Errorf("failed to parse: %w", err) - } - if k := cfg.Kind; k != updateConfigKind { - return nil, trace.Errorf("invalid kind %q", k) - } - if v := cfg.Version; v != updateConfigVersion { - return nil, trace.Errorf("invalid version %q", v) - } - return &cfg, nil -} - -// writeConfig writes UpdateConfig to a file atomically, ensuring the file cannot be corrupted. -func writeConfig(filename string, cfg *UpdateConfig) error { - opts := []renameio.Option{ - renameio.WithPermissions(configFileMode), - renameio.WithExistingPermissions(), - } - t, err := renameio.NewPendingFile(filename, opts...) - if err != nil { - return trace.Wrap(err) - } - defer t.Cleanup() - err = yaml.NewEncoder(t).Encode(cfg) - if err != nil { - return trace.Wrap(err) - } - return trace.Wrap(t.CloseAtomicallyReplace()) -} - -func validateConfigSpec(spec *UpdateSpec, override OverrideConfig) error { - if override.Proxy != "" { - spec.Proxy = override.Proxy - } - if override.Group != "" { - spec.Group = override.Group - } - switch override.URLTemplate { - case "": - case "default": - spec.URLTemplate = "" - default: - spec.URLTemplate = override.URLTemplate - } - if spec.URLTemplate != "" && - !strings.HasPrefix(strings.ToLower(spec.URLTemplate), "https://") { - return trace.Errorf("Teleport download URL must use TLS (https://)") - } - if override.Enabled { - spec.Enabled = true - } - if override.Pinned { - spec.Pinned = true - } - return nil -} - // LinkPackage creates links from the system (package) installation of Teleport, if they are needed. // LinkPackage returns nil and warns if an auto-updates version is already linked, but auto-updates is disabled. // LinkPackage returns an error only if an unknown version of Teleport is present (e.g., manually copied files). diff --git a/tool/teleport-update/main.go b/tool/teleport-update/main.go index e8d52bb87e42b..9e19393f12d0c 100644 --- a/tool/teleport-update/main.go +++ b/tool/teleport-update/main.go @@ -29,6 +29,7 @@ import ( "syscall" "github.com/gravitational/trace" + "gopkg.in/yaml.v3" "github.com/gravitational/teleport" autoupdate "github.com/gravitational/teleport/lib/autoupdate/agent" @@ -144,6 +145,8 @@ func Run(args []string) error { setupCmd := app.Command("setup", "Write configuration files that run the update subcommand on a timer."). Hidden() + statusCmd := app.Command("status", "Show Teleport agent auto-update status.") + libutils.UpdateAppUsageTemplate(app, args) command, err := app.Parse(args) if err != nil { @@ -175,6 +178,8 @@ func Run(args []string) error { err = cmdUnlink(ctx, &ccfg) case setupCmd.FullCommand(): err = cmdSetup(ctx, &ccfg) + case statusCmd.FullCommand(): + err = cmdStatus(ctx, &ccfg) case versionCmd.FullCommand(): modules.GetModules().PrintVersion() default: @@ -212,7 +217,7 @@ func cmdDisable(ctx context.Context, ccfg *cliConfig) error { Log: plog, }) if err != nil { - return trace.Errorf("failed to setup updater: %w", err) + return trace.Errorf("failed to initialize updater: %w", err) } unlock, err := libutils.FSWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) if err != nil { @@ -266,7 +271,7 @@ func cmdInstall(ctx context.Context, ccfg *cliConfig) error { Log: plog, }) if err != nil { - return trace.Errorf("failed to setup updater: %w", err) + return trace.Errorf("failed to initialize updater: %w", err) } // Ensure enable can't run concurrently. @@ -295,7 +300,7 @@ func cmdUpdate(ctx context.Context, ccfg *cliConfig) error { Log: plog, }) if err != nil { - return trace.Errorf("failed to setup updater: %w", err) + return trace.Errorf("failed to initialize updater: %w", err) } // Ensure update can't run concurrently. unlock, err := libutils.FSWriteLock(filepath.Join(ccfg.DataDir, lockFileName)) @@ -324,7 +329,7 @@ func cmdLink(ctx context.Context, ccfg *cliConfig) error { Log: plog, }) if err != nil { - return trace.Errorf("failed to setup updater: %w", err) + return trace.Errorf("failed to initialize updater: %w", err) } // Skip operation and warn if the updater is currently running. @@ -393,3 +398,23 @@ func cmdSetup(ctx context.Context, ccfg *cliConfig) error { } return nil } + +// cmdStatus displays auto-update status. +func cmdStatus(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) + } + status, err := updater.Status(ctx) + if err != nil { + return trace.Errorf("failed to get status: %w", err) + } + enc := yaml.NewEncoder(os.Stdout) + return trace.Wrap(enc.Encode(status)) +}