Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[teleport-update] status subcommand #49308

Merged
merged 5 commits into from
Nov 21, 2024
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
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider trace.ConvertSystemErr for filesystem operations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like that would convert the os/fs errors to trace-specific types that this package isn't looking for? Or is the primary intent to avoid stack traces for these errors?

I haven't found any guidelines on when trace should be used or avoided. I try to avoid everything besides trace.Errorf and trace.Wrap (unless I'm writing code that is called by something that expects a specific trace error). That said, happy to follow repo conventions.

}
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
Loading