Skip to content

Commit

Permalink
[teleport-update] status subcommand (#49308)
Browse files Browse the repository at this point in the history
* status

* cleanup

* comments

* cleanup output by removing optional fields

* rebase fix
  • Loading branch information
sclevine authored Nov 21, 2024
1 parent 75c2cef commit e6fb164
Show file tree
Hide file tree
Showing 37 changed files with 337 additions and 290 deletions.
217 changes: 153 additions & 64 deletions lib/autoupdate/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
116 changes: 116 additions & 0 deletions lib/autoupdate/agent/setup.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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())
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Loading

0 comments on commit e6fb164

Please sign in to comment.