diff --git a/lib/autoupdate/agent/setup.go b/lib/autoupdate/agent/setup.go index b51e378208715..c2575e0cc1e5f 100644 --- a/lib/autoupdate/agent/setup.go +++ b/lib/autoupdate/agent/setup.go @@ -27,6 +27,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "text/template" "github.com/google/renameio/v2" @@ -37,13 +38,14 @@ import ( // Base paths for constructing namespaced directories. const ( - teleportOptDir = "/opt/teleport" - versionsDirName = "versions" - systemdAdminDir = "/etc/systemd/system" - systemdPIDFile = "/run/teleport.pid" - defaultNamespace = "default" - systemNamespace = "system" - lockFileName = "update.lock" + teleportOptDir = "/opt/teleport" + systemdAdminDir = "/etc/systemd/system" + systemdPIDFile = "/run/teleport.pid" + needrestartConfDir = "/etc/needrestart/conf.d" + versionsDirName = "versions" + lockFileName = "update.lock" + defaultNamespace = "default" + systemNamespace = "system" ) const ( @@ -68,9 +70,28 @@ RandomizedDelaySec=1m [Install] WantedBy={{.TeleportService}} +` + teleportDropInTemplate = `# teleport-update +# DO NOT EDIT THIS FILE +[Service] +Environment=TELEPORT_UPDATE_CONFIG_FILE={{.UpdaterConfigFile}} +` + // This configuration sets the default value for needrestart-trigger automatic restarts for teleport.service to disabled. + // Users may still choose to enable needrestart for teleport.service when installing packaging interactively (or via dpkg config), + // but doing so will result in a hard restart that disconnects the agent whenever any dependent libraries are updated. + // Other network services, like openvpn, follow this pattern. + // It is possible to configure needrestart to trigger a soft restart (via restart.d script), but given that Teleport subprocesses + // can use a wide variety of installed binaries (when executed by the user), this could trigger many unexpected reloads. + needrestartConfTemplate = `$nrconf{override_rc}{qr(^{{replace .TeleportService "." "\\."}})} = 0; ` ) +type confParams struct { + TeleportService string + UpdaterCommand string + UpdaterConfigFile string +} + // Namespace represents a namespace within various system paths for a isolated installation of Teleport. type Namespace struct { log *slog.Logger @@ -98,6 +119,10 @@ type Namespace struct { updaterServiceFile string // updaterTimerFile is the systemd timer path for the updater updaterTimerFile string + // dropInFile is the Teleport systemd drop-in path extending Teleport + dropInFile string + // needrestartConfFile is the path to needrestart configuration for Teleport + needrestartConfFile string } var alphanum = regexp.MustCompile("^[a-zA-Z0-9-]*$") @@ -120,19 +145,21 @@ func NewNamespace(log *slog.Logger, name, dataDir, linkDir string) (*Namespace, linkDir = DefaultLinkDir } return &Namespace{ - log: log, - name: name, - dataDir: dataDir, - linkDir: linkDir, - versionsDir: filepath.Join(namespaceDir(name), versionsDirName), - serviceFile: filepath.Join("/", serviceDir, serviceName), - configFile: defaults.ConfigFilePath, - pidFile: systemdPIDFile, - updaterLockFile: filepath.Join(namespaceDir(name), lockFileName), - updaterConfigFile: filepath.Join(namespaceDir(name), updateConfigName), - updaterBinFile: filepath.Join(linkDir, BinaryName), - updaterServiceFile: filepath.Join(systemdAdminDir, BinaryName+".service"), - updaterTimerFile: filepath.Join(systemdAdminDir, BinaryName+".timer"), + log: log, + name: name, + dataDir: dataDir, + linkDir: linkDir, + versionsDir: filepath.Join(namespaceDir(name), versionsDirName), + serviceFile: filepath.Join("/", serviceDir, serviceName), + configFile: defaults.ConfigFilePath, + pidFile: systemdPIDFile, + updaterLockFile: filepath.Join(namespaceDir(name), lockFileName), + updaterConfigFile: filepath.Join(namespaceDir(name), updateConfigName), + updaterBinFile: filepath.Join(linkDir, BinaryName), + updaterServiceFile: filepath.Join(systemdAdminDir, BinaryName+".service"), + updaterTimerFile: filepath.Join(systemdAdminDir, BinaryName+".timer"), + dropInFile: filepath.Join(systemdAdminDir, "teleport.service.d", BinaryName+".conf"), + needrestartConfFile: filepath.Join(needrestartConfDir, BinaryName+".conf"), }, nil } @@ -144,19 +171,21 @@ func NewNamespace(log *slog.Logger, name, dataDir, linkDir string) (*Namespace, linkDir = filepath.Join(namespaceDir(name), "bin") } return &Namespace{ - log: log, - name: name, - dataDir: dataDir, - linkDir: linkDir, - versionsDir: filepath.Join(namespaceDir(name), versionsDirName), - serviceFile: filepath.Join(systemdAdminDir, prefix+".service"), - configFile: filepath.Join(filepath.Dir(defaults.ConfigFilePath), prefix+".yaml"), - pidFile: filepath.Join(filepath.Dir(systemdPIDFile), prefix+".pid"), - updaterLockFile: filepath.Join(namespaceDir(name), lockFileName), - updaterConfigFile: filepath.Join(namespaceDir(name), updateConfigName), - updaterBinFile: filepath.Join(linkDir, BinaryName), - updaterServiceFile: filepath.Join(systemdAdminDir, BinaryName+"_"+name+".service"), - updaterTimerFile: filepath.Join(systemdAdminDir, BinaryName+"_"+name+".timer"), + log: log, + name: name, + dataDir: dataDir, + linkDir: linkDir, + versionsDir: filepath.Join(namespaceDir(name), versionsDirName), + serviceFile: filepath.Join(systemdAdminDir, prefix+".service"), + configFile: filepath.Join(filepath.Dir(defaults.ConfigFilePath), prefix+".yaml"), + pidFile: filepath.Join(filepath.Dir(systemdPIDFile), prefix+".pid"), + updaterLockFile: filepath.Join(namespaceDir(name), lockFileName), + updaterConfigFile: filepath.Join(namespaceDir(name), updateConfigName), + updaterBinFile: filepath.Join(linkDir, BinaryName), + updaterServiceFile: filepath.Join(systemdAdminDir, BinaryName+"_"+name+".service"), + updaterTimerFile: filepath.Join(systemdAdminDir, BinaryName+"_"+name+".timer"), + dropInFile: filepath.Join(systemdAdminDir, "teleport.service.d", BinaryName+"_"+name+".conf"), + needrestartConfFile: filepath.Join(needrestartConfDir, BinaryName+"_"+name+".conf"), }, nil } @@ -178,7 +207,7 @@ func (ns *Namespace) Init() (lockFile string, err error) { // Setup installs service and timer files for the teleport-update binary. // Afterwords, Setup reloads systemd and enables the timer with --now. func (ns *Namespace) Setup(ctx context.Context) error { - err := ns.writeConfigFiles() + err := ns.writeConfigFiles(ctx) if err != nil { return trace.Wrap(err, "failed to write teleport-update systemd config files") } @@ -204,11 +233,15 @@ func (ns *Namespace) Teardown(ctx context.Context) error { if err := svc.Disable(ctx); err != nil { return trace.Wrap(err, "failed to disable teleport-update systemd timer") } - if err := os.Remove(ns.updaterServiceFile); err != nil && !errors.Is(err, fs.ErrNotExist) { - return trace.Wrap(err, "failed to remove teleport-update systemd service") - } - if err := os.Remove(ns.updaterTimerFile); err != nil && !errors.Is(err, fs.ErrNotExist) { - return trace.Wrap(err, "failed to remove teleport-update systemd timer") + for _, p := range []string{ + ns.updaterServiceFile, + ns.updaterTimerFile, + ns.dropInFile, + ns.needrestartConfFile, + } { + if err := os.Remove(p); err != nil && !errors.Is(err, fs.ErrNotExist) { + return trace.Wrap(err, "failed to remove %s", filepath.Base(p)) + } } if err := svc.Sync(ctx); err != nil { return trace.Wrap(err, "failed to sync systemd config") @@ -219,27 +252,44 @@ func (ns *Namespace) Teardown(ctx context.Context) error { return nil } -func (ns *Namespace) writeConfigFiles() error { +func (ns *Namespace) writeConfigFiles(ctx context.Context) error { var args string if ns.name != "" { args = " --install-suffix=" + ns.name } - err := writeTemplate( - ns.updaterServiceFile, updateServiceTemplate, - struct{ UpdaterCommand string }{ - ns.updaterBinFile + args + " update", - }, - ) + teleportService := filepath.Base(ns.serviceFile) + params := confParams{ + TeleportService: teleportService, + UpdaterCommand: ns.updaterBinFile + args + " update", + UpdaterConfigFile: ns.updaterConfigFile, + } + err := writeTemplate(ns.updaterServiceFile, updateServiceTemplate, params) if err != nil { return trace.Wrap(err) } - err = writeTemplate( - ns.updaterTimerFile, updateTimerTemplate, - struct{ TeleportService string }{filepath.Base(ns.serviceFile)}, - ) + err = writeTemplate(ns.updaterTimerFile, updateTimerTemplate, params) + if err != nil { + return trace.Wrap(err) + } + err = writeTemplate(ns.dropInFile, teleportDropInTemplate, params) if err != nil { return trace.Wrap(err) } + // Needrestart config is non-critical for updater functionality. + _, err = os.Stat(filepath.Dir(ns.needrestartConfFile)) + if os.IsNotExist(err) { + return nil // needrestart is not present + } + if err != nil { + ns.log.ErrorContext(ctx, "Unable to disable needrestart.", errorKey, err) + return nil + } + ns.log.InfoContext(ctx, "Disabling needrestart.", unitKey, teleportService) + err = writeTemplate(ns.needrestartConfFile, needrestartConfTemplate, params) + if err != nil { + ns.log.ErrorContext(ctx, "Unable to disable needrestart.", errorKey, err) + return nil + } return nil } @@ -258,7 +308,11 @@ func writeTemplate(path, t string, values any) error { } defer f.Cleanup() - tmpl, err := template.New(file).Parse(t) + tmpl, err := template.New(file).Funcs(template.FuncMap{ + "replace": func(s, old, new string) string { + return strings.ReplaceAll(s, old, new) + }, + }).Parse(t) if err != nil { return trace.Wrap(err) } diff --git a/lib/autoupdate/agent/setup_test.go b/lib/autoupdate/agent/setup_test.go index 8892acb85e06c..97f224c985206 100644 --- a/lib/autoupdate/agent/setup_test.go +++ b/lib/autoupdate/agent/setup_test.go @@ -20,6 +20,7 @@ package agent import ( "bytes" + "context" "log/slog" "os" "path/filepath" @@ -42,17 +43,19 @@ func TestNewNamespace(t *testing.T) { { name: "no namespace", ns: &Namespace{ - dataDir: "/var/lib/teleport", - linkDir: "/usr/local/bin", - versionsDir: "/opt/teleport/default/versions", - serviceFile: "/lib/systemd/system/teleport.service", - configFile: "/etc/teleport.yaml", - pidFile: "/run/teleport.pid", - updaterLockFile: "/opt/teleport/default/update.lock", - updaterConfigFile: "/opt/teleport/default/update.yaml", - updaterBinFile: "/usr/local/bin/teleport-update", - updaterServiceFile: "/etc/systemd/system/teleport-update.service", - updaterTimerFile: "/etc/systemd/system/teleport-update.timer", + dataDir: "/var/lib/teleport", + linkDir: "/usr/local/bin", + versionsDir: "/opt/teleport/default/versions", + serviceFile: "/lib/systemd/system/teleport.service", + configFile: "/etc/teleport.yaml", + pidFile: "/run/teleport.pid", + updaterLockFile: "/opt/teleport/default/update.lock", + updaterConfigFile: "/opt/teleport/default/update.yaml", + updaterBinFile: "/usr/local/bin/teleport-update", + updaterServiceFile: "/etc/systemd/system/teleport-update.service", + updaterTimerFile: "/etc/systemd/system/teleport-update.timer", + dropInFile: "/etc/systemd/system/teleport.service.d/teleport-update.conf", + needrestartConfFile: "/etc/needrestart/conf.d/teleport-update.conf", }, }, { @@ -60,35 +63,39 @@ func TestNewNamespace(t *testing.T) { linkDir: "/link", dataDir: "/data", ns: &Namespace{ - dataDir: "/data", - linkDir: "/link", - versionsDir: "/opt/teleport/default/versions", - serviceFile: "/lib/systemd/system/teleport.service", - configFile: "/etc/teleport.yaml", - pidFile: "/run/teleport.pid", - updaterLockFile: "/opt/teleport/default/update.lock", - updaterConfigFile: "/opt/teleport/default/update.yaml", - updaterBinFile: "/link/teleport-update", - updaterServiceFile: "/etc/systemd/system/teleport-update.service", - updaterTimerFile: "/etc/systemd/system/teleport-update.timer", + dataDir: "/data", + linkDir: "/link", + versionsDir: "/opt/teleport/default/versions", + serviceFile: "/lib/systemd/system/teleport.service", + configFile: "/etc/teleport.yaml", + pidFile: "/run/teleport.pid", + updaterLockFile: "/opt/teleport/default/update.lock", + updaterConfigFile: "/opt/teleport/default/update.yaml", + updaterBinFile: "/link/teleport-update", + updaterServiceFile: "/etc/systemd/system/teleport-update.service", + updaterTimerFile: "/etc/systemd/system/teleport-update.timer", + dropInFile: "/etc/systemd/system/teleport.service.d/teleport-update.conf", + needrestartConfFile: "/etc/needrestart/conf.d/teleport-update.conf", }, }, { name: "test namespace", namespace: "test", ns: &Namespace{ - name: "test", - dataDir: "/var/lib/teleport_test", - linkDir: "/opt/teleport/test/bin", - versionsDir: "/opt/teleport/test/versions", - serviceFile: "/etc/systemd/system/teleport_test.service", - configFile: "/etc/teleport_test.yaml", - pidFile: "/run/teleport_test.pid", - updaterLockFile: "/opt/teleport/test/update.lock", - updaterConfigFile: "/opt/teleport/test/update.yaml", - updaterBinFile: "/opt/teleport/test/bin/teleport-update", - updaterServiceFile: "/etc/systemd/system/teleport-update_test.service", - updaterTimerFile: "/etc/systemd/system/teleport-update_test.timer", + name: "test", + dataDir: "/var/lib/teleport_test", + linkDir: "/opt/teleport/test/bin", + versionsDir: "/opt/teleport/test/versions", + serviceFile: "/etc/systemd/system/teleport_test.service", + configFile: "/etc/teleport_test.yaml", + pidFile: "/run/teleport_test.pid", + updaterLockFile: "/opt/teleport/test/update.lock", + updaterConfigFile: "/opt/teleport/test/update.yaml", + updaterBinFile: "/opt/teleport/test/bin/teleport-update", + updaterServiceFile: "/etc/systemd/system/teleport-update_test.service", + updaterTimerFile: "/etc/systemd/system/teleport-update_test.timer", + dropInFile: "/etc/systemd/system/teleport.service.d/teleport-update_test.conf", + needrestartConfFile: "/etc/needrestart/conf.d/teleport-update_test.conf", }, }, { @@ -97,18 +104,20 @@ func TestNewNamespace(t *testing.T) { linkDir: "/link", dataDir: "/data", ns: &Namespace{ - name: "test", - dataDir: "/data", - linkDir: "/link", - versionsDir: "/opt/teleport/test/versions", - serviceFile: "/etc/systemd/system/teleport_test.service", - configFile: "/etc/teleport_test.yaml", - pidFile: "/run/teleport_test.pid", - updaterLockFile: "/opt/teleport/test/update.lock", - updaterConfigFile: "/opt/teleport/test/update.yaml", - updaterBinFile: "/link/teleport-update", - updaterServiceFile: "/etc/systemd/system/teleport-update_test.service", - updaterTimerFile: "/etc/systemd/system/teleport-update_test.timer", + name: "test", + dataDir: "/data", + linkDir: "/link", + versionsDir: "/opt/teleport/test/versions", + serviceFile: "/etc/systemd/system/teleport_test.service", + configFile: "/etc/teleport_test.yaml", + pidFile: "/run/teleport_test.pid", + updaterLockFile: "/opt/teleport/test/update.lock", + updaterConfigFile: "/opt/teleport/test/update.yaml", + updaterBinFile: "/link/teleport-update", + updaterServiceFile: "/etc/systemd/system/teleport-update_test.service", + updaterTimerFile: "/etc/systemd/system/teleport-update_test.timer", + dropInFile: "/etc/systemd/system/teleport.service.d/teleport-update_test.conf", + needrestartConfFile: "/etc/needrestart/conf.d/teleport-update_test.conf", }, }, { @@ -157,28 +166,33 @@ func TestWriteConfigFiles(t *testing.T) { require.NoError(t, err) ns.updaterServiceFile = filepath.Join(linkDir, serviceDir, filepath.Base(ns.updaterServiceFile)) ns.updaterTimerFile = filepath.Join(linkDir, serviceDir, filepath.Base(ns.updaterTimerFile)) - err = ns.writeConfigFiles() + ns.dropInFile = filepath.Join(linkDir, serviceDir, filepath.Base(filepath.Dir(ns.dropInFile)), filepath.Base(ns.dropInFile)) + ns.needrestartConfFile = filepath.Join(linkDir, filepath.Base(ns.dropInFile)) + ctx := context.Background() + err = ns.writeConfigFiles(ctx) require.NoError(t, err) - data, err := os.ReadFile(ns.updaterServiceFile) - require.NoError(t, err) - data = replaceValues(data, map[string]string{ - DefaultLinkDir: linkDir, - }) - if golden.ShouldSet() { - golden.SetNamed(t, "service", data) - } - require.Equal(t, string(golden.GetNamed(t, "service")), string(data)) - - data, err = os.ReadFile(ns.updaterTimerFile) - require.NoError(t, err) - data = replaceValues(data, map[string]string{ - DefaultLinkDir: linkDir, - }) - if golden.ShouldSet() { - golden.SetNamed(t, "timer", data) + for _, tt := range []struct { + name string + path string + }{ + {name: "service", path: ns.updaterServiceFile}, + {name: "timer", path: ns.updaterTimerFile}, + {name: "dropin", path: ns.dropInFile}, + {name: "needrestart", path: ns.needrestartConfFile}, + } { + t.Run(tt.name, func(t *testing.T) { + data, err := os.ReadFile(tt.path) + require.NoError(t, err) + data = replaceValues(data, map[string]string{ + DefaultLinkDir: linkDir, + }) + if golden.ShouldSet() { + golden.Set(t, data) + } + require.Equal(t, string(golden.Get(t)), string(data)) + }) } - require.Equal(t, string(golden.GetNamed(t, "timer")), string(data)) }) } } diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/dropin.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/dropin.golden new file mode 100644 index 0000000000000..105c0060f1eb7 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/dropin.golden @@ -0,0 +1,4 @@ +# teleport-update +# DO NOT EDIT THIS FILE +[Service] +Environment=TELEPORT_UPDATE_CONFIG_FILE=/opt/teleport/default/update.yaml diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/needrestart.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/needrestart.golden new file mode 100644 index 0000000000000..b5d6a74435cb2 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/needrestart.golden @@ -0,0 +1 @@ +$nrconf{override_rc}{qr(^teleport\.service)} = 0; diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/dropin.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/dropin.golden new file mode 100644 index 0000000000000..a14fd0e5f028d --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/dropin.golden @@ -0,0 +1,4 @@ +# teleport-update +# DO NOT EDIT THIS FILE +[Service] +Environment=TELEPORT_UPDATE_CONFIG_FILE=/opt/teleport/test/update.yaml diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/needrestart.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/needrestart.golden new file mode 100644 index 0000000000000..ad6bd606a74cb --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/needrestart.golden @@ -0,0 +1 @@ +$nrconf{override_rc}{qr(^teleport_test\.service)} = 0;