diff --git a/lib/autoupdate/agent/config.go b/lib/autoupdate/agent/config.go index 32c7b90a52dfa..34f8dca1a676a 100644 --- a/lib/autoupdate/agent/config.go +++ b/lib/autoupdate/agent/config.go @@ -164,6 +164,7 @@ func NewRevisionFromDir(dir string) (Revision, error) { } // Dir returns the directory path name of a Revision. +// These are unambiguous for semver and may be parsed with NewRevisionFromDir. func (r Revision) Dir() string { // Do not change the order of these statements. // Otherwise, installed versions will no longer match update.yaml. @@ -178,6 +179,7 @@ func (r Revision) Dir() string { } // String returns a human-readable description of a Teleport revision. +// These are semver-ambiguous and should not be parsed. func (r Revision) String() string { if flags := r.Flags.Strings(); len(flags) > 0 { return fmt.Sprintf("%s+%s", r.Version, strings.Join(flags, "+")) diff --git a/lib/autoupdate/agent/installer.go b/lib/autoupdate/agent/installer.go index ae25003e7f639..88820ba911adb 100644 --- a/lib/autoupdate/agent/installer.go +++ b/lib/autoupdate/agent/installer.go @@ -36,7 +36,6 @@ import ( "github.com/gravitational/trace" - "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/autoupdate" "github.com/gravitational/teleport/lib/utils" ) @@ -61,6 +60,31 @@ const ( serviceName = "teleport.service" ) +// ServiceFile represents a systemd service file for a Teleport binary. +// +// ExampleName and ExampleFunc are used to parse an example configuration +// file and copy it into the service directory. This mechanism of service +// file generation is only provided to support downgrading to older versions +// of the updater that do not install the service during the setup phase using +// logic from lib/config. +type ServiceFile struct { + // Path is the full path to the linked service file. + Path string + // Binary is the corresponding linked binary name. + Binary string + // ExampleName is the name of the example service file. + // Deprecated. + ExampleName string + // ExampleFunc can be used to create the service during linking from + // an archived example, instead of creating it during the setup phase. + // Deprecated. + ExampleFunc ExampleFunc +} + +// A ExampleFunc generates a systemd service by replacing an example file. +// Deprecated. +type ExampleFunc func(cfg []byte, path string, flags autoupdate.InstallFlags) []byte + // LocalInstaller manages the creation and removal of installations // of Teleport. // SetRequiredUmask must be called before any methods are executed. @@ -68,11 +92,11 @@ type LocalInstaller struct { // InstallDir contains each installation, named by version. InstallDir string // TargetServiceFile contains a copy of the linked installation's systemd service. - TargetServiceFile string + TargetServices []ServiceFile // SystemBinDir contains binaries for the system (packaged) install of Teleport. SystemBinDir string - // SystemServiceFile contains the systemd service file for the system (packaged) install of Teleport. - SystemServiceFile string + // SystemServiceDir contains the systemd service directory for the system (packaged) install of Teleport. + SystemServiceDir string // HTTP is an HTTP client for downloading Teleport. HTTP *http.Client // Log contains a logger. @@ -81,8 +105,6 @@ type LocalInstaller struct { ReservedFreeTmpDisk uint64 // ReservedFreeInstallDisk is the amount of disk that must remain free in the install directory. ReservedFreeInstallDisk uint64 - // TransformService transforms the systemd service during copying. - TransformService func(cfg []byte, pathDir string, flags autoupdate.InstallFlags) []byte // ValidateBinary returns true if a file is a linkable binary, or // false if a file should not be linked. ValidateBinary func(ctx context.Context, path string) (bool, error) @@ -416,7 +438,7 @@ func (li *LocalInstaller) Link(ctx context.Context, rev Revision, pathDir string } revert, err = li.forceLinks(ctx, filepath.Join(versionDir, "bin"), - filepath.Join(versionDir, serviceDir, serviceName), + filepath.Join(versionDir, serviceDir), pathDir, force, rev.Flags, ) if err != nil { @@ -432,7 +454,7 @@ func (li *LocalInstaller) Link(ctx context.Context, rev Revision, pathDir string func (li *LocalInstaller) LinkSystem(ctx context.Context) (revert func(context.Context) bool, err error) { // The system package service file is always removed without flags, so pass // no flags here to match the behavior. - revert, err = li.forceLinks(ctx, li.SystemBinDir, li.SystemServiceFile, defaultPathDir, false, 0) + revert, err = li.forceLinks(ctx, li.SystemBinDir, li.SystemServiceDir, defaultPathDir, false, 0) return revert, trace.Wrap(err) } @@ -446,7 +468,7 @@ func (li *LocalInstaller) TryLink(ctx context.Context, revision Revision, pathDi } return trace.Wrap(li.tryLinks(ctx, filepath.Join(versionDir, "bin"), - filepath.Join(versionDir, serviceDir, serviceName), + filepath.Join(versionDir, serviceDir), pathDir, revision.Flags, )) } @@ -457,7 +479,7 @@ func (li *LocalInstaller) TryLink(ctx context.Context, revision Revision, pathDi func (li *LocalInstaller) TryLinkSystem(ctx context.Context) error { // The system package service file is always removed without flags, so pass // no flags here to match the behavior. - return trace.Wrap(li.tryLinks(ctx, li.SystemBinDir, li.SystemServiceFile, defaultPathDir, 0)) + return trace.Wrap(li.tryLinks(ctx, li.SystemBinDir, li.SystemServiceDir, defaultPathDir, 0)) } // Unlink unlinks a version from pathDir and TargetServiceFile. @@ -467,19 +489,13 @@ func (li *LocalInstaller) Unlink(ctx context.Context, rev Revision, pathDir stri if err != nil { return trace.Wrap(err) } - return trace.Wrap(li.removeLinks(ctx, - filepath.Join(versionDir, "bin"), - filepath.Join(versionDir, serviceDir, serviceName), - pathDir, rev.Flags, - )) + return trace.Wrap(li.removeLinks(ctx, filepath.Join(versionDir, "bin"), pathDir)) } // UnlinkSystem unlinks the system (package) version from defaultPathDir and TargetServiceFile. // See Installer interface for additional specs. func (li *LocalInstaller) UnlinkSystem(ctx context.Context) error { - // The system package service file is always linked without flags, so pass - // no flags here to match the behavior. - return trace.Wrap(li.removeLinks(ctx, li.SystemBinDir, li.SystemServiceFile, defaultPathDir, 0)) + return trace.Wrap(li.removeLinks(ctx, li.SystemBinDir, defaultPathDir)) } // symlink from oldname to newname @@ -500,7 +516,7 @@ type smallFile struct { // If successful, forceLinks may also be reverted after it returns by calling revert. // The revert function returns true if reverting succeeds. // If force is true, non-link files will be overwritten. -func (li *LocalInstaller) forceLinks(ctx context.Context, srcBinDir, srcSvcFile, dstBinDir string, force bool, flags autoupdate.InstallFlags) (revert func(context.Context) bool, err error) { +func (li *LocalInstaller) forceLinks(ctx context.Context, srcBinDir, srcSvcDir, dstBinDir string, force bool, flags autoupdate.InstallFlags) (revert func(context.Context) bool, err error) { // setup revert function var ( revertLinks []symlink @@ -552,9 +568,11 @@ func (li *LocalInstaller) forceLinks(ctx context.Context, srcBinDir, srcSvcFile, if err != nil { return revert, trace.Wrap(err) } - err = os.MkdirAll(filepath.Dir(li.TargetServiceFile), systemDirMode) - if err != nil { - return revert, trace.Wrap(err) + for _, s := range li.TargetServices { + err = os.MkdirAll(filepath.Dir(s.Path), systemDirMode) + if err != nil { + return revert, trace.Wrap(err) + } } // create binary links @@ -588,27 +606,40 @@ func (li *LocalInstaller) forceLinks(ctx context.Context, srcBinDir, srcSvcFile, return revert, trace.Wrap(ErrNoBinaries) } - // create systemd service file + // create systemd service files - orig, err := li.forceCopyService(li.TargetServiceFile, srcSvcFile, maxServiceFileSize, dstBinDir, flags) - if err != nil && !errors.Is(err, os.ErrExist) { - return revert, trace.Wrap(err, "failed to copy service") - } - if orig != nil { - revertFiles = append(revertFiles, *orig) + for _, s := range li.TargetServices { + orig, err := copyService(s, srcSvcDir, dstBinDir, flags) + if err != nil && !errors.Is(err, os.ErrExist) { + return revert, trace.Wrap(err, "failed to copy service %s", filepath.Base(s.Path)) + } + if orig != nil { + revertFiles = append(revertFiles, *orig) + } } + return revert, nil } -// forceCopyService uses forceCopy to copy a systemd service file from src to dst. +// copyService copies a systemd service file from src to dst. // The contents of both src and dst must be smaller than n. -// See forceCopy for more details. -func (li *LocalInstaller) forceCopyService(dst, src string, n int64, dstBinDir string, flags autoupdate.InstallFlags) (orig *smallFile, err error) { - srcData, err := readFileAtMost(src, n) - if err != nil { - return nil, trace.Wrap(err) +// +// Copied data is processed by s.ExampleFunc. +// If s.ExampleFunc nil, no data is copied, but the original file contents are still returned. +// +// See prepCopy and forceCopy for more details. +func copyService(s ServiceFile, exampleDir string, dstBinDir string, flags autoupdate.InstallFlags) (orig *smallFile, err error) { + const n = maxServiceFileSize + if s.ExampleFunc != nil { + srcData, err := readFileAtMost(filepath.Join(exampleDir, s.ExampleName), n) + if err != nil { + return nil, trace.Wrap(err) + } + orig, err = forceCopy(s.Path, s.ExampleFunc(srcData, dstBinDir, flags), n) + return orig, trace.Wrap(err) } - return forceCopy(dst, li.TransformService(srcData, dstBinDir, flags), n) + orig, err = prepCopy(s.Path, n) + return orig, trace.Wrap(err) } // forceLink attempts to create a symlink, atomically replacing an existing link if already present. @@ -642,27 +673,38 @@ func forceLink(oldname, newname string, force bool) (orig string, err error) { // If an irregular file, too large file, or directory exists in dst already, forceCopy errors. // If the file is already present with the desired contents, forceCopy returns os.ErrExist. func forceCopy(dst string, srcData []byte, n int64) (orig *smallFile, err error) { + orig, err = prepCopy(dst, n) + if err != nil { + return orig, trace.Wrap(err) + } + if orig != nil && bytes.Equal(srcData, orig.data) { + return nil, trace.Wrap(os.ErrExist) + } + err = writeFileAtomicWithinDir(dst, srcData, configFileMode) + if err != nil { + return orig, trace.Wrap(err) + } + return orig, nil +} + +// prepCopy validates and returns a preserved original copy of a file with +// length <= n at the path specified by dst. +func prepCopy(dst string, n int64) (orig *smallFile, err error) { fi, err := os.Lstat(dst) - if err != nil && !errors.Is(err, os.ErrNotExist) { + if errors.Is(err, os.ErrNotExist) { + return orig, nil + } + if err != nil { return nil, trace.Wrap(err) } - if err == nil { - orig = &smallFile{ - name: dst, - mode: fi.Mode(), - } - if !orig.mode.IsRegular() { - return nil, trace.Errorf("refusing to replace irregular file at %s", dst) - } - orig.data, err = readFileAtMost(dst, n) - if err != nil { - return nil, trace.Wrap(err) - } - if bytes.Equal(srcData, orig.data) { - return nil, trace.Wrap(os.ErrExist) - } + orig = &smallFile{ + name: dst, + mode: fi.Mode(), } - err = writeFileAtomicWithinDir(dst, srcData, configFileMode) + if !orig.mode.IsRegular() { + return nil, trace.Errorf("refusing to replace irregular file at %s", dst) + } + orig.data, err = readFileAtMost(dst, n) if err != nil { return nil, trace.Wrap(err) } @@ -680,8 +722,7 @@ func readFileAtMost(name string, n int64) ([]byte, error) { return data, trace.Wrap(err) } -func (li *LocalInstaller) removeLinks(ctx context.Context, srcBinDir, srcSvcFile, dstBinDir string, flags autoupdate.InstallFlags) error { - removeService := false +func (li *LocalInstaller) removeLinks(ctx context.Context, srcBinDir, dstBinDir string) error { entries, err := os.ReadDir(srcBinDir) if err != nil { return trace.Wrap(err, "failed to find Teleport binary directory") @@ -710,34 +751,41 @@ func (li *LocalInstaller) removeLinks(ctx context.Context, srcBinDir, srcSvcFile li.Log.ErrorContext(ctx, "Unable to remove link.", "oldname", oldname, "newname", newname, errorKey, err) continue } - if filepath.Base(newname) == teleport.ComponentTeleport { - removeService = true + + for _, s := range li.TargetServices { + if filepath.Base(newname) != s.Binary { + continue + } + // binRev is either the version or "system" + binRev := filepath.Base(filepath.Dir(filepath.Dir(oldname))) + rev, err := NewRevisionFromDir(binRev) + if err != nil { + li.Log.DebugContext(ctx, "Service not present.", "path", s.Path) + continue + } + revMarker := genMarker(rev) + diskMarker, err := readFileLimit(s.Path, int64(len(revMarker))) + if errors.Is(err, os.ErrNotExist) { + li.Log.DebugContext(ctx, "Service not present.", "path", s.Path) + continue + } + if err != nil { + return trace.Wrap(err) + } + // Note that old versions of teleport-update will install services without the marker. + // Certain version combinations (before and after this commit) may leave services behind + // if they are not replaced by the new version of teleport-update. This should only impact + // explicit system package unlinking, which is rarely used. + if string(diskMarker) != revMarker { + li.Log.WarnContext(ctx, "Removed binary link, but skipping removal of custom service that does not match the binary.", + "service", filepath.Base(s.Path), "binary", filepath.Base(newname)) + continue + } + if err := os.Remove(s.Path); err != nil { + return trace.Wrap(err, "error removing copy of %s", filepath.Base(s.Path)) + } } } - // only remove service if teleport was removed - if !removeService { - li.Log.DebugContext(ctx, "Teleport binary not unlinked. Skipping removal of teleport.service.") - return nil - } - srcBytes, err := readFileAtMost(srcSvcFile, maxServiceFileSize) - if err != nil { - return trace.Wrap(err) - } - dstBytes, err := readFileAtMost(li.TargetServiceFile, maxServiceFileSize) - if errors.Is(err, os.ErrNotExist) { - li.Log.DebugContext(ctx, "Service not present.", "path", li.TargetServiceFile) - return nil - } - if err != nil { - return trace.Wrap(err) - } - if !bytes.Equal(li.TransformService(srcBytes, dstBinDir, flags), dstBytes) { - li.Log.WarnContext(ctx, "Removed teleport binary link, but skipping removal of custom teleport.service: the service file does not match the reference file for this version. The file might have been manually edited.") - return nil - } - if err := os.Remove(li.TargetServiceFile); err != nil { - return trace.Wrap(err, "error removing copy of %s", filepath.Base(li.TargetServiceFile)) - } return nil } @@ -745,7 +793,7 @@ func (li *LocalInstaller) removeLinks(ctx context.Context, srcBinDir, srcSvcFile // Existing links that point to files outside binDir or svcDir, as well as existing non-link files, will error. // tryLinks will not attempt to create any links if linking could result in an error. // However, concurrent changes to links may result in an error with partially-complete linking. -func (li *LocalInstaller) tryLinks(ctx context.Context, srcBinDir, srcSvcFile, dstBinDir string, flags autoupdate.InstallFlags) error { +func (li *LocalInstaller) tryLinks(ctx context.Context, srcBinDir, srcSvcDir, dstBinDir string, flags autoupdate.InstallFlags) error { // ensure source directory exists entries, err := os.ReadDir(srcBinDir) if errors.Is(err, os.ErrNotExist) { @@ -760,9 +808,11 @@ func (li *LocalInstaller) tryLinks(ctx context.Context, srcBinDir, srcSvcFile, d if err != nil { return trace.Wrap(err) } - err = os.MkdirAll(filepath.Dir(li.TargetServiceFile), systemDirMode) - if err != nil { - return trace.Wrap(err) + for _, s := range li.TargetServices { + err = os.MkdirAll(filepath.Dir(s.Path), systemDirMode) + if err != nil { + return trace.Wrap(err) + } } // validate that we can link all system binaries before attempting linking @@ -802,10 +852,11 @@ func (li *LocalInstaller) tryLinks(ctx context.Context, srcBinDir, srcSvcFile, d } } - // if any binaries are linked from srcBinDir, always link the service from svcDir - _, err = li.forceCopyService(li.TargetServiceFile, srcSvcFile, maxServiceFileSize, dstBinDir, flags) - if err != nil && !errors.Is(err, os.ErrExist) { - return trace.Wrap(err, "failed to copy service") + for _, s := range li.TargetServices { + _, err := copyService(s, srcSvcDir, dstBinDir, flags) + if err != nil && !errors.Is(err, os.ErrExist) { + return trace.Wrap(err, "failed to copy service %s", filepath.Base(s.Path)) + } } return nil diff --git a/lib/autoupdate/agent/installer_test.go b/lib/autoupdate/agent/installer_test.go index 80276d90e416d..afd977a92e09f 100644 --- a/lib/autoupdate/agent/installer_test.go +++ b/lib/autoupdate/agent/installer_test.go @@ -456,12 +456,17 @@ func TestLocalInstaller_Link(t *testing.T) { validator := Validator{Log: slog.Default()} installer := &LocalInstaller{ - InstallDir: versionsDir, - TargetServiceFile: filepath.Join(linkDir, serviceDir, serviceName), - Log: slog.Default(), - TransformService: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { - return []byte(fmt.Sprintf("[service=%s][path=%s][flags=%s]", string(b), pathDir, flags.Strings())) + InstallDir: versionsDir, + TargetServices: []ServiceFile{ + { + Path: filepath.Join(linkDir, serviceDir, serviceName), + ExampleName: serviceName, + ExampleFunc: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { + return fmt.Appendf(nil, "[service=%s][path=%s][flags=%s]", string(b), pathDir, flags.Strings()) + }, + }, }, + Log: slog.Default(), ValidateBinary: validator.IsExecutable, Template: autoupdate.DefaultCDNURITemplate, } @@ -711,12 +716,17 @@ func TestLocalInstaller_TryLink(t *testing.T) { validator := Validator{Log: slog.Default()} installer := &LocalInstaller{ - InstallDir: versionsDir, - TargetServiceFile: filepath.Join(linkDir, serviceDir, serviceName), - Log: slog.Default(), - TransformService: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { - return []byte(fmt.Sprintf("[service=%s][path=%s][flags=%s]", string(b), pathDir, flags.Strings())) + InstallDir: versionsDir, + TargetServices: []ServiceFile{ + { + Path: filepath.Join(linkDir, serviceDir, serviceName), + ExampleName: serviceName, + ExampleFunc: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { + return fmt.Appendf(nil, "[service=%s][path=%s][flags=%s]", string(b), pathDir, flags.Strings()) + }, + }, }, + Log: slog.Default(), ValidateBinary: validator.IsExecutable, } ctx := context.Background() @@ -848,12 +858,17 @@ func TestLocalInstaller_Remove(t *testing.T) { validator := Validator{Log: slog.Default()} installer := &LocalInstaller{ - InstallDir: versionsDir, - TargetServiceFile: filepath.Join(linkDir, serviceDir, serviceName), - Log: slog.Default(), - TransformService: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { - return []byte(fmt.Sprintf("[service=%s][path=%s][flags=%s]", string(b), pathDir, flags.Strings())) + InstallDir: versionsDir, + TargetServices: []ServiceFile{ + { + Path: filepath.Join(linkDir, serviceDir, serviceName), + ExampleName: serviceName, + ExampleFunc: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { + return fmt.Appendf(nil, "[service=%s][path=%s][flags=%s]", string(b), pathDir, flags.Strings()) + }, + }, }, + Log: slog.Default(), ValidateBinary: validator.IsExecutable, } ctx := context.Background() @@ -918,12 +933,18 @@ func TestLocalInstaller_IsLinked(t *testing.T) { validator := Validator{Log: slog.Default()} installer := &LocalInstaller{ - InstallDir: versionsDir, - TargetServiceFile: filepath.Join(linkDir, serviceDir, serviceName), - Log: slog.Default(), - TransformService: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { - return []byte(fmt.Sprintf("[service=%s][path=%s][flags=%s]", string(b), pathDir, flags.Strings())) + InstallDir: versionsDir, + TargetServices: []ServiceFile{ + { + Path: filepath.Join(linkDir, serviceDir, serviceName), + ExampleName: serviceName, + ExampleFunc: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { + return fmt.Appendf(nil, "[service=%s][path=%s][flags=%s]", string(b), pathDir, flags.Strings()) + + }, + }, }, + Log: slog.Default(), ValidateBinary: validator.IsExecutable, } ctx := context.Background() @@ -963,9 +984,8 @@ func TestLocalInstaller_Unlink(t *testing.T) { servicePath := filepath.Join(serviceDir, serviceName) tests := []struct { - name string - bins []string - svcOrig []byte + name string + bins []string links []symlink svcCopy []byte @@ -974,19 +994,17 @@ func TestLocalInstaller_Unlink(t *testing.T) { errMatch string }{ { - name: "normal", - bins: []string{"teleport", "tsh"}, - svcOrig: []byte("orig"), + name: "normal", + bins: []string{"teleport", "tsh"}, links: []symlink{ {oldname: "bin/teleport", newname: "bin/teleport"}, {oldname: "bin/tsh", newname: "bin/tsh"}, }, - svcCopy: []byte("[service=orig][path=bin][flags=[]]"), + svcCopy: []byte("# teleport-update " + version + "\n"), }, { - name: "different services", - bins: []string{"teleport", "tsh"}, - svcOrig: []byte("orig"), + name: "different services", + bins: []string{"teleport", "tsh"}, links: []symlink{ {oldname: "bin/teleport", newname: "bin/teleport"}, {oldname: "bin/tsh", newname: "bin/tsh"}, @@ -995,64 +1013,48 @@ func TestLocalInstaller_Unlink(t *testing.T) { remaining: []string{servicePath}, }, { - name: "missing target service", - bins: []string{"teleport", "tsh"}, - svcOrig: []byte("orig"), - links: []symlink{ - {oldname: "bin/teleport", newname: "bin/teleport"}, - {oldname: "bin/tsh", newname: "bin/tsh"}, - }, - }, - { - name: "missing source service", + name: "missing target service", bins: []string{"teleport", "tsh"}, links: []symlink{ {oldname: "bin/teleport", newname: "bin/teleport"}, {oldname: "bin/tsh", newname: "bin/tsh"}, }, - svcCopy: []byte("custom"), - remaining: []string{servicePath}, - errMatch: "no such", }, { - name: "missing teleport link", - bins: []string{"teleport", "tsh"}, - svcOrig: []byte("orig"), + name: "missing teleport link", + bins: []string{"teleport", "tsh"}, links: []symlink{ {oldname: "bin/tsh", newname: "bin/tsh"}, }, - svcCopy: []byte("[service=orig][path=bin][flags=[]]"), + svcCopy: []byte("# teleport-update " + version + "\n"), remaining: []string{servicePath}, }, { - name: "missing other link", - bins: []string{"teleport", "tsh"}, - svcOrig: []byte("orig"), + name: "missing other link", + bins: []string{"teleport", "tsh"}, links: []symlink{ {oldname: "bin/teleport", newname: "bin/teleport"}, }, - svcCopy: []byte("[service=orig][path=bin][flags=[]]"), + svcCopy: []byte("# teleport-update " + version + "\n"), }, { - name: "wrong teleport link", - bins: []string{"teleport", "tsh"}, - svcOrig: []byte("orig"), + name: "wrong teleport link", + bins: []string{"teleport", "tsh"}, links: []symlink{ {oldname: "other", newname: "bin/teleport"}, {oldname: "bin/tsh", newname: "bin/tsh"}, }, - svcCopy: []byte("[service=orig][path=bin][flags=[]]"), + svcCopy: []byte("# teleport-update " + version + "\n"), remaining: []string{servicePath, "bin/teleport"}, }, { - name: "wrong other link", - bins: []string{"teleport", "tsh"}, - svcOrig: []byte("orig"), + name: "wrong other link", + bins: []string{"teleport", "tsh"}, links: []symlink{ {oldname: "bin/teleport", newname: "bin/teleport"}, {oldname: "wrong", newname: "bin/tsh"}, }, - svcCopy: []byte("[service=orig][path=bin][flags=[]]"), + svcCopy: []byte("# teleport-update " + version + "\n"), remaining: []string{"bin/tsh"}, }, } @@ -1073,13 +1075,6 @@ func TestLocalInstaller_Unlink(t *testing.T) { mode: os.ModePerm, }) } - if tt.svcOrig != nil { - files = append(files, smallFile{ - name: filepath.Join(versionDir, servicePath), - data: tt.svcOrig, - mode: os.ModePerm, - }) - } if tt.svcCopy != nil { files = append(files, smallFile{ name: filepath.Join(linkDir, servicePath), @@ -1104,12 +1099,14 @@ func TestLocalInstaller_Unlink(t *testing.T) { } installer := &LocalInstaller{ - InstallDir: versionsDir, - TargetServiceFile: filepath.Join(linkDir, serviceDir, serviceName), - Log: slog.Default(), - TransformService: func(b []byte, pathDir string, flags autoupdate.InstallFlags) []byte { - return []byte(fmt.Sprintf("[service=%s][path=%s][flags=%s]", string(b), filepath.Base(pathDir), flags.Strings())) + InstallDir: versionsDir, + TargetServices: []ServiceFile{ + { + Path: filepath.Join(linkDir, serviceDir, serviceName), + Binary: "teleport", + }, }, + Log: slog.Default(), } ctx := context.Background() err = installer.Unlink(ctx, NewRevision(version, 0), filepath.Join(linkDir, "bin")) diff --git a/lib/autoupdate/agent/setup.go b/lib/autoupdate/agent/setup.go index 732471aba9be1..044271ed26c78 100644 --- a/lib/autoupdate/agent/setup.go +++ b/lib/autoupdate/agent/setup.go @@ -22,6 +22,7 @@ import ( "bytes" "context" "errors" + "fmt" "io" "io/fs" "log/slog" @@ -35,6 +36,7 @@ import ( "gopkg.in/yaml.v3" "github.com/gravitational/teleport/lib/autoupdate" + "github.com/gravitational/teleport/lib/config/systemd" "github.com/gravitational/teleport/lib/defaults" libutils "github.com/gravitational/teleport/lib/utils" ) @@ -59,9 +61,22 @@ const ( deprecatedServiceName = "teleport-upgrade.service" ) +// genHeader generates a systemd config file header that starts +// with the serviceMarker. +func genHeader(rev Revision) string { + return genMarker(rev) + + "# DO NOT EDIT THIS FILE\n" +} + +// genMarker generates a systemd config file marker that is the +// first part of the header for systemd service files. +// Each revision of Teleport has a unique marker. +func genMarker(rev Revision) string { + return "# teleport-update " + rev.Dir() + "\n" +} + const ( - updateServiceTemplate = `# teleport-update -# DO NOT EDIT THIS FILE + updateServiceTemplate = ` [Unit] Description=Teleport auto-update service @@ -69,8 +84,7 @@ Description=Teleport auto-update service Type=oneshot ExecStart={{.UpdaterBinary}} --install-suffix={{.InstallSuffix}} "--install-dir={{escape .InstallDir}}" update ` - updateTimerTemplate = `# teleport-update -# DO NOT EDIT THIS FILE + updateTimerTemplate = ` [Unit] Description=Teleport auto-update timer unit @@ -82,15 +96,13 @@ RandomizedDelaySec=1m [Install] WantedBy={{.TeleportService}} ` - teleportDropInTemplate = `# teleport-update -# DO NOT EDIT THIS FILE + teleportDropInTemplate = ` [Service] Environment="TELEPORT_UPDATE_CONFIG_FILE={{escape .UpdaterConfigFile}}" Environment="TELEPORT_UPDATE_INSTALL_DIR={{escape .InstallDir}}" ` - deprecatedDropInTemplate = `# teleport-update -# DO NOT EDIT THIS FILE + deprecatedDropInTemplate = ` [Service] ExecStart= ExecStart=-/bin/echo "The teleport-upgrade script has been disabled by teleport-update. Please remove the teleport-ent-updater package." @@ -224,13 +236,13 @@ 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, path string) error { +func (ns *Namespace) Setup(ctx context.Context, path string, rev Revision) error { if ok, err := hasSystemD(); err == nil && !ok { ns.log.WarnContext(ctx, "Systemd is not running, skipping updater installation.") return nil } - err := ns.writeConfigFiles(ctx, path) + err := ns.writeConfigFiles(ctx, path, rev) if err != nil { return trace.Wrap(err, "failed to write teleport-update systemd config files") } @@ -277,6 +289,7 @@ func (ns *Namespace) Setup(ctx context.Context, path string) error { } // Teardown removes all traces of the auto-updater, including its configuration. +// Teardown does not verify that the removed files were created by teleport-update. func (ns *Namespace) Teardown(ctx context.Context) error { if ok, err := hasSystemD(); err == nil && !ok { ns.log.WarnContext(ctx, "Systemd is not running, skipping updater removal.") @@ -344,7 +357,7 @@ func (ns *Namespace) Teardown(ctx context.Context) error { return nil } -func (ns *Namespace) writeConfigFiles(ctx context.Context, path string) error { +func (ns *Namespace) writeConfigFiles(ctx context.Context, path string, rev Revision) error { teleportService := filepath.Base(ns.serviceFile) params := confParams{ TeleportService: teleportService, @@ -366,7 +379,7 @@ func (ns *Namespace) writeConfigFiles(ctx context.Context, path string) error { if v.path == "" { continue } - err := writeSystemTemplate(v.path, v.tmpl, params) + err := writeSystemTemplate(v.path, genHeader(rev), v.tmpl, params) if err != nil { return trace.Wrap(err) } @@ -381,7 +394,7 @@ func (ns *Namespace) writeConfigFiles(ctx context.Context, path string) error { return nil } ns.log.InfoContext(ctx, "Disabling needrestart.", unitKey, teleportService) - err = writeSystemTemplate(ns.needrestartConfFile, needrestartConfTemplate, params) + err = writeSystemTemplate(ns.needrestartConfFile, "", needrestartConfTemplate, params) if err != nil { ns.log.ErrorContext(ctx, "Unable to disable needrestart.", errorKey, err) return nil @@ -391,13 +404,19 @@ func (ns *Namespace) writeConfigFiles(ctx context.Context, path string) error { // writeSystemTemplate atomically writes a template to a system file, creating any needed directories. // Temporarily files are stored in the target path to ensure the file has needed SELinux contexts. -func writeSystemTemplate(path, t string, values any) error { +func writeSystemTemplate(path, header, t string, values any) error { dir, file := filepath.Split(path) if err := os.MkdirAll(dir, systemDirMode); err != nil { return trace.Wrap(err) } return trace.Wrap(writeAtomicWithinDir(path, configFileMode, func(w io.Writer) error { + if header != "" { + _, err := fmt.Fprint(w, header) + if err != nil { + return trace.Wrap(err) + } + } tmpl, err := template.New(file).Funcs(template.FuncMap{ "replace": func(s, old, new string) string { return strings.ReplaceAll(s, old, new) @@ -421,11 +440,35 @@ func writeSystemTemplate(path, t string, values any) error { })) } -// ReplaceTeleportService replaces the default paths in the Teleport service config with namespaced paths. -func (ns *Namespace) ReplaceTeleportService(cfg []byte, pathDir string, flags autoupdate.InstallFlags) []byte { +// WriteTeleportService writes the Teleport systemd service for the version of Teleport +// that matches the version of Teleport compiled into the executing code. +func (ns *Namespace) WriteTeleportService(_ context.Context, pathDir string, rev Revision) error { if pathDir == "" { pathDir = ns.defaultPathDir } + return trace.Wrap(writeAtomicWithinDir(ns.serviceFile, configFileMode, func(w io.Writer) error { + _, err := fmt.Fprint(w, genHeader(rev)+"\n") + if err != nil { + return trace.Wrap(err) + } + return trace.Wrap(systemd.WriteUnitFile(systemd.Flags{ + EnvironmentFile: systemd.DefaultEnvironmentFile, + PIDFile: ns.pidFile, + FileDescriptorLimit: systemd.DefaultFileDescriptorLimit, + TeleportInstallationFile: filepath.Join(pathDir, "teleport"), + TeleportConfigPath: ns.configFile, + FIPS: rev.Flags&autoupdate.FlagFIPS != 0, + }, w)) + })) +} + +// ReplaceTeleportService replaces the default paths in the Teleport service config with namespaced paths. +// This function is still used for backwards-compatibility, but string-replaced systemd services +// are always overridden in more recent versions of Teleport. +func (ns *Namespace) ReplaceTeleportService(cfg []byte, path string, flags autoupdate.InstallFlags) []byte { + if path == "" { + path = ns.defaultPathDir + } var startFlags []string if flags&autoupdate.FlagFIPS != 0 { startFlags = append(startFlags, "--fips") @@ -435,7 +478,7 @@ func (ns *Namespace) ReplaceTeleportService(cfg []byte, pathDir string, flags au }{ { old: "/usr/local/bin/", - new: pathDir + "/", + new: path + "/", }, { old: "/etc/teleport.yaml", diff --git a/lib/autoupdate/agent/setup_test.go b/lib/autoupdate/agent/setup_test.go index e077d37dd91e7..42311e56fefbd 100644 --- a/lib/autoupdate/agent/setup_test.go +++ b/lib/autoupdate/agent/setup_test.go @@ -170,7 +170,7 @@ func TestWriteConfigFiles(t *testing.T) { ns.teleportDropInFile = rebasePath(filepath.Join(linkDir, serviceDir, filepath.Base(filepath.Dir(ns.teleportDropInFile))), ns.teleportDropInFile) ns.deprecatedDropInFile = rebasePath(filepath.Join(linkDir, serviceDir, filepath.Base(filepath.Dir(ns.deprecatedDropInFile))), ns.deprecatedDropInFile) ns.needrestartConfFile = rebasePath(linkDir, filepath.Base(ns.needrestartConfFile)) - err = ns.writeConfigFiles(ctx, linkDir) + err = ns.writeConfigFiles(ctx, linkDir, NewRevision("version", 0)) require.NoError(t, err) for _, tt := range []struct { @@ -388,6 +388,58 @@ func TestUnversionedTeleportConfig(t *testing.T) { require.Equal(t, in, out) } +func TestWriteTeleportService(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + + pidFile string + configFile string + pathDir string + flags autoupdate.InstallFlags + }{ + { + name: "default", + pidFile: "/var/run/teleport.pid", + configFile: "/etc/teleport.yaml", + pathDir: "/usr/local/bin", + }, + { + name: "custom", + pidFile: "/some/path/teleport.pid", + configFile: "/some/path/teleport.yaml", + pathDir: "/some/path/bin", + }, + { + name: "FIPS", + pidFile: "/var/run/teleport.pid", + configFile: "/etc/teleport.yaml", + pathDir: "/usr/local/bin", + flags: autoupdate.FlagFIPS, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + serviceFile := filepath.Join(t.TempDir(), "file") + ns := &Namespace{ + log: slog.Default(), + configFile: tt.configFile, + serviceFile: serviceFile, + pidFile: tt.pidFile, + } + err := ns.WriteTeleportService(context.Background(), tt.pathDir, NewRevision("version", tt.flags)) + require.NoError(t, err) + data, err := os.ReadFile(serviceFile) + require.NoError(t, err) + if golden.ShouldSet() { + golden.Set(t, data) + } + require.Equal(t, string(golden.Get(t)), string(data)) + }) + } +} + func TestReplaceTeleportService(t *testing.T) { t.Parallel() diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/deprecated.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/deprecated.golden index 3f18b9cdf3065..fcaaae54ce5d0 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/deprecated.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/deprecated.golden @@ -1,5 +1,6 @@ -# teleport-update +# teleport-update version # DO NOT EDIT THIS FILE + [Service] ExecStart= ExecStart=-/bin/echo "The teleport-upgrade script has been disabled by teleport-update. Please remove the teleport-ent-updater package." diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/dropin.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/dropin.golden index cb09143fe9fdf..4ca6b61b76342 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/dropin.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/dropin.golden @@ -1,5 +1,6 @@ -# teleport-update +# teleport-update version # DO NOT EDIT THIS FILE + [Service] Environment="TELEPORT_UPDATE_CONFIG_FILE=/opt/teleport/default/update.yaml" Environment="TELEPORT_UPDATE_INSTALL_DIR=/opt/teleport" diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/service.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/service.golden index 45d778ec09f25..e3038b3255fc0 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/service.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/service.golden @@ -1,5 +1,6 @@ -# teleport-update +# teleport-update version # DO NOT EDIT THIS FILE + [Unit] Description=Teleport auto-update service diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/timer.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/timer.golden index d14a43d679e53..df921a1e883f1 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/timer.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/no_namespace/timer.golden @@ -1,5 +1,6 @@ -# teleport-update +# teleport-update version # DO NOT EDIT THIS FILE + [Unit] Description=Teleport auto-update timer unit diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/dropin.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/dropin.golden index dc6445dc6e7f9..14ce4fd431951 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/dropin.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/dropin.golden @@ -1,5 +1,6 @@ -# teleport-update +# teleport-update version # DO NOT EDIT THIS FILE + [Service] Environment="TELEPORT_UPDATE_CONFIG_FILE=/opt/teleport/test/update.yaml" Environment="TELEPORT_UPDATE_INSTALL_DIR=/opt/teleport" diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/service.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/service.golden index f698deec24bb9..2f8b0f13d68a8 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/service.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/service.golden @@ -1,5 +1,6 @@ -# teleport-update +# teleport-update version # DO NOT EDIT THIS FILE + [Unit] Description=Teleport auto-update service diff --git a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/timer.golden b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/timer.golden index f57a3c08055bc..38f8e37a02c72 100644 --- a/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/timer.golden +++ b/lib/autoupdate/agent/testdata/TestWriteConfigFiles/test_namespace/timer.golden @@ -1,5 +1,6 @@ -# teleport-update +# teleport-update version # DO NOT EDIT THIS FILE + [Unit] Description=Teleport auto-update timer unit diff --git a/lib/autoupdate/agent/testdata/TestWriteTeleportService/FIPS.golden b/lib/autoupdate/agent/testdata/TestWriteTeleportService/FIPS.golden new file mode 100644 index 0000000000000..fead0e35cf34f --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteTeleportService/FIPS.golden @@ -0,0 +1,20 @@ +# teleport-update version_ent_fips +# DO NOT EDIT THIS FILE + +[Unit] +Description=Teleport Service +After=network.target + +[Service] +Type=simple +Restart=always +RestartSec=5 +EnvironmentFile=-/etc/default/teleport +ExecStart=/usr/local/bin/teleport start --fips --config /etc/teleport.yaml --pid-file=/var/run/teleport.pid +# systemd before 239 needs an absolute path +ExecReload=/bin/sh -c "exec pkill -HUP -L -F /var/run/teleport.pid" +PIDFile=/var/run/teleport.pid +LimitNOFILE=524288 + +[Install] +WantedBy=multi-user.target diff --git a/lib/autoupdate/agent/testdata/TestWriteTeleportService/custom.golden b/lib/autoupdate/agent/testdata/TestWriteTeleportService/custom.golden new file mode 100644 index 0000000000000..a7ee6334eb88a --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteTeleportService/custom.golden @@ -0,0 +1,20 @@ +# teleport-update version +# DO NOT EDIT THIS FILE + +[Unit] +Description=Teleport Service +After=network.target + +[Service] +Type=simple +Restart=always +RestartSec=5 +EnvironmentFile=-/etc/default/teleport +ExecStart=/some/path/bin/teleport start --config /some/path/teleport.yaml --pid-file=/some/path/teleport.pid +# systemd before 239 needs an absolute path +ExecReload=/bin/sh -c "exec pkill -HUP -L -F /some/path/teleport.pid" +PIDFile=/some/path/teleport.pid +LimitNOFILE=524288 + +[Install] +WantedBy=multi-user.target diff --git a/lib/autoupdate/agent/testdata/TestWriteTeleportService/default.golden b/lib/autoupdate/agent/testdata/TestWriteTeleportService/default.golden new file mode 100644 index 0000000000000..a1db5d008ddd6 --- /dev/null +++ b/lib/autoupdate/agent/testdata/TestWriteTeleportService/default.golden @@ -0,0 +1,20 @@ +# teleport-update version +# DO NOT EDIT THIS FILE + +[Unit] +Description=Teleport Service +After=network.target + +[Service] +Type=simple +Restart=always +RestartSec=5 +EnvironmentFile=-/etc/default/teleport +ExecStart=/usr/local/bin/teleport start --config /etc/teleport.yaml --pid-file=/var/run/teleport.pid +# systemd before 239 needs an absolute path +ExecReload=/bin/sh -c "exec pkill -HUP -L -F /var/run/teleport.pid" +PIDFile=/var/run/teleport.pid +LimitNOFILE=524288 + +[Install] +WantedBy=multi-user.target diff --git a/lib/autoupdate/agent/updater.go b/lib/autoupdate/agent/updater.go index 6b63482b3210c..2ab1e141befb5 100644 --- a/lib/autoupdate/agent/updater.go +++ b/lib/autoupdate/agent/updater.go @@ -35,6 +35,7 @@ import ( "path/filepath" "runtime" "slices" + "strings" "time" "github.com/google/uuid" @@ -52,6 +53,11 @@ import ( const ( // BinaryName specifies the name of the updater binary. BinaryName = "teleport-update" + + // SetupVersionEnvVar specifies the Teleport version. + SetupVersionEnvVar = "TELEPORT_UPDATE_SETUP_VERSION" + // SetupFlagsEnvVar specifies Teleport version flags. + SetupFlagsEnvVar = "TELEPORT_UPDATE_SETUP_FLAGS" ) const ( @@ -131,15 +137,21 @@ func NewLocalUpdater(cfg LocalUpdaterConfig, ns *Namespace) (*Updater, error) { DefaultProxyAddr: ns.defaultProxyAddr, DefaultPathDir: ns.defaultPathDir, Installer: &LocalInstaller{ - InstallDir: filepath.Join(ns.Dir(), versionsDirName), - TargetServiceFile: ns.serviceFile, + InstallDir: filepath.Join(ns.Dir(), versionsDirName), + TargetServices: []ServiceFile{ + { + Path: ns.serviceFile, + Binary: "teleport", + ExampleName: serviceName, + ExampleFunc: ns.ReplaceTeleportService, + }, + }, SystemBinDir: filepath.Join(cfg.SystemDir, "bin"), - SystemServiceFile: filepath.Join(cfg.SystemDir, serviceDir, serviceName), + SystemServiceDir: filepath.Join(cfg.SystemDir, serviceDir), HTTP: client, Log: cfg.Log, ReservedFreeTmpDisk: reservedFreeDisk, ReservedFreeInstallDisk: reservedFreeDisk, - TransformService: ns.ReplaceTeleportService, ValidateBinary: validator.IsBinary, Template: autoupdate.DefaultCDNURITemplate, }, @@ -149,11 +161,14 @@ func NewLocalUpdater(cfg LocalUpdaterConfig, ns *Namespace) (*Updater, error) { Ready: debugClient, Log: cfg.Log, }, - ReexecSetup: func(ctx context.Context, pathDir string, reload bool) error { + WriteTeleportService: ns.WriteTeleportService, + ReexecSetup: func(ctx context.Context, pathDir string, rev Revision, reload bool) error { name := filepath.Join(pathDir, BinaryName) if cfg.SelfSetup && runtime.GOOS == constants.LinuxOS { name = "/proc/self/exe" } + // New arguments must never be added here, as older versions of the + // updater may be invoked by this logic. args := []string{ "--install-dir", ns.installDir, "--install-suffix", ns.name, @@ -169,6 +184,10 @@ func NewLocalUpdater(cfg LocalUpdaterConfig, ns *Namespace) (*Updater, error) { cmd := exec.CommandContext(ctx, name, args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout + cmd.Env = append(slices.Clone(os.Environ()), + SetupVersionEnvVar+"="+rev.Version, + SetupFlagsEnvVar+"="+strings.Join(rev.Flags.Strings(), "\n"), + ) cfg.Log.InfoContext(ctx, "Executing new teleport-update binary to update configuration.") defer cfg.Log.InfoContext(ctx, "Finished executing new teleport-update binary.") return trace.Wrap(cmd.Run()) @@ -229,11 +248,14 @@ type Updater struct { Installer Installer // Process manages a running instance of Teleport. Process Process + // WriteTeleportService writes the teleport systemd service for the version of Teleport + // matching the currently running updater. + WriteTeleportService func(ctx context.Context, path string, rev Revision) error // ReexecSetup re-execs teleport-update with the setup command. // This configures the updater service, verifies the installation, and optionally reloads Teleport. - ReexecSetup func(ctx context.Context, path string, reload bool) error + ReexecSetup func(ctx context.Context, path string, rev Revision, reload bool) error // SetupNamespace configures the Teleport updater service for the current Namespace. - SetupNamespace func(ctx context.Context, path string) error + SetupNamespace func(ctx context.Context, path string, rev Revision) error // TeardownNamespace removes all traces of the updater service in the current Namespace, including Teleport. TeardownNamespace func(ctx context.Context) error // LogConfigWarnings logs warnings related to the configuration Namespace. @@ -1003,38 +1025,18 @@ func (u *Updater) update(ctx context.Context, cfg *UpdateConfig, target Revision u.Log.ErrorContext(ctx, "Failed to revert Teleport symlinks. Installation likely broken.") return false } - if err := u.SetupNamespace(ctx, cfg.Spec.Path); err != nil { + // Note: this version may be inaccurate if the active installation was modified + if err := u.SetupNamespace(ctx, cfg.Spec.Path, cfg.Status.Active); err != nil { u.Log.ErrorContext(ctx, "Failed to revert configuration after failed restart.", errorKey, err) return false } return true } - if cfg.Status.Active != target { - err := u.ReexecSetup(ctx, cfg.Spec.Path, true) - if errors.Is(err, context.Canceled) { - return trace.Errorf("check canceled") - } - if err != nil { - // If reloading Teleport at the new version fails, revert and reload. - u.Log.ErrorContext(ctx, "Reverting symlinks due to failed restart.") - if ok := revertConfig(ctx); ok { - if err := u.Process.Reload(ctx); err != nil && !errors.Is(err, ErrNotNeeded) { - u.Log.ErrorContext(ctx, "Failed to reload Teleport after reverting. Installation likely broken.", errorKey, err) - } else { - u.Log.WarnContext(ctx, "Teleport updater detected an error with the new installation and successfully reverted it.") - } - } - return trace.Wrap(err, "failed to start new version %s of Teleport", target) - } - u.Log.InfoContext(ctx, "Target version successfully installed.", targetKey, target) + // If re-linking the same version, do not attempt to restart services. - if r := cfg.Status.Active; r.Version != "" { - cfg.Status.Backup = toPtr(r) - } - cfg.Status.Active = target - } else { - err := u.ReexecSetup(ctx, cfg.Spec.Path, false) + if cfg.Status.Active == target { + err := u.ReexecSetup(ctx, cfg.Spec.Path, target, false) if errors.Is(err, context.Canceled) { return trace.Errorf("check canceled") } @@ -1047,10 +1049,36 @@ func (u *Updater) update(ctx context.Context, cfg *UpdateConfig, target Revision return trace.Wrap(err, "failed to validate new version %s of Teleport", target) } u.Log.InfoContext(ctx, "Target version successfully validated.", targetKey, target) + u.cleanup(ctx, cfg, []Revision{ + target, active, backup, + }) + return nil } - if r := deref(cfg.Status.Backup); r.Version != "" { - u.Log.InfoContext(ctx, "Backup version set.", backupKey, r) + + // If a new version was linked, restart services (including on revert). + + err = u.ReexecSetup(ctx, cfg.Spec.Path, target, true) + if errors.Is(err, context.Canceled) { + return trace.Errorf("check canceled") + } + if err != nil { + // If reloading Teleport at the new version fails, revert and reload. + u.Log.ErrorContext(ctx, "Reverting symlinks due to failed restart.") + if ok := revertConfig(ctx); ok { + if err := u.Process.Reload(ctx); err != nil && !errors.Is(err, ErrNotNeeded) { + u.Log.ErrorContext(ctx, "Failed to reload Teleport after reverting. Installation likely broken.", errorKey, err) + } else { + u.Log.WarnContext(ctx, "Teleport updater detected an error with the new installation and successfully reverted it.") + } + } + return trace.Wrap(err, "failed to start new version %s of Teleport", target) + } + u.Log.InfoContext(ctx, "Target version successfully installed.", targetKey, target) + + if r := cfg.Status.Active; r.Version != "" { + cfg.Status.Backup = toPtr(r) } + cfg.Status.Active = target u.cleanup(ctx, cfg, []Revision{ target, active, backup, }) @@ -1060,10 +1088,17 @@ func (u *Updater) update(ctx context.Context, cfg *UpdateConfig, target Revision // Setup writes updater configuration and verifies the Teleport installation. // If restart is true, Setup also restarts Teleport. // Setup is safe to run concurrently with other Updater commands. -func (u *Updater) Setup(ctx context.Context, path string, restart bool) error { +func (u *Updater) Setup(ctx context.Context, path string, rev Revision, restart bool) error { + + // Write Teleport systemd service. + + if err := u.WriteTeleportService(ctx, path, rev); err != nil { + return trace.Wrap(err, "failed to write teleport systemd service") + } + // Setup teleport-updater configuration and sync systemd. - err := u.SetupNamespace(ctx, path) + err := u.SetupNamespace(ctx, path, rev) if errors.Is(err, context.Canceled) { return trace.Errorf("sync canceled") } @@ -1071,6 +1106,8 @@ func (u *Updater) Setup(ctx context.Context, path string, restart bool) error { return trace.Wrap(err, "failed to setup updater") } + // Validations + present, err := u.Process.IsPresent(ctx) if errors.Is(err, context.Canceled) { return trace.Errorf("config check canceled") @@ -1142,6 +1179,9 @@ func (u *Updater) notices(ctx context.Context) { // cleanup orphan installations func (u *Updater) cleanup(ctx context.Context, cfg *UpdateConfig, keep []Revision) { + if r := deref(cfg.Status.Backup); r.Version != "" { + u.Log.InfoContext(ctx, "Backup version set.", backupKey, r) + } revs, err := u.Installer.List(ctx) if err != nil { u.Log.ErrorContext(ctx, "Failed to read installed versions.", errorKey, err) diff --git a/lib/autoupdate/agent/updater_test.go b/lib/autoupdate/agent/updater_test.go index f54a230663cef..008987603749c 100644 --- a/lib/autoupdate/agent/updater_test.go +++ b/lib/autoupdate/agent/updater_test.go @@ -840,12 +840,12 @@ func TestUpdater_Update(t *testing.T) { }, } var restarted bool - updater.ReexecSetup = func(_ context.Context, path string, reload bool) error { + updater.ReexecSetup = func(_ context.Context, path string, rev Revision, reload bool) error { restarted = reload setupCalls++ return tt.setupErr } - updater.SetupNamespace = func(_ context.Context, path string) error { + updater.SetupNamespace = func(_ context.Context, path string, rev Revision) error { revertSetupCalls++ return nil } @@ -1793,12 +1793,12 @@ func TestUpdater_Install(t *testing.T) { }, } var restarted bool - updater.ReexecSetup = func(_ context.Context, path string, reload bool) error { + updater.ReexecSetup = func(_ context.Context, path string, rev Revision, reload bool) error { setupCalls++ restarted = reload return tt.setupErr } - updater.SetupNamespace = func(_ context.Context, path string) error { + updater.SetupNamespace = func(_ context.Context, path string, rev Revision) error { revertSetupCalls++ return nil } @@ -1982,13 +1982,18 @@ func TestUpdater_Setup(t *testing.T) { return tt.present, tt.presentErr }, } - updater.SetupNamespace = func(_ context.Context, path string) error { + updater.SetupNamespace = func(_ context.Context, path string, rev Revision) error { require.Equal(t, "test", path) return tt.setupErr } + updater.WriteTeleportService = func(_ context.Context, path string, rev Revision) error { + require.Equal(t, "test", path) + require.Equal(t, "version", rev.Version) + return tt.setupErr + } ctx := context.Background() - err = updater.Setup(ctx, "test", tt.restart) + err = updater.Setup(ctx, "test", Revision{Version: "version"}, tt.restart) if tt.errMatch != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.errMatch) diff --git a/lib/autoupdate/agent/validate.go b/lib/autoupdate/agent/validate.go index 8ce1732d3d5d1..e2dec446d8e50 100644 --- a/lib/autoupdate/agent/validate.go +++ b/lib/autoupdate/agent/validate.go @@ -117,7 +117,7 @@ func isTextScript(data []byte) bool { return true } -// readFileLimit the first n bytes of a file. +// readFileLimit the first n bytes of a file, or less if shorter. func readFileLimit(name string, n int64) ([]byte, error) { f, err := os.Open(name) if err != nil { diff --git a/lib/config/systemd.go b/lib/config/systemd/systemd.go similarity index 65% rename from lib/config/systemd.go rename to lib/config/systemd/systemd.go index 1418e73272227..010318d0ce5f6 100644 --- a/lib/config/systemd.go +++ b/lib/config/systemd/systemd.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package config +package systemd import ( "io" @@ -29,16 +29,16 @@ import ( ) const ( - // SystemdDefaultEnvironmentFile is the default path to the env file for the systemd unit file config - SystemdDefaultEnvironmentFile = "/etc/default/teleport" - // SystemdDefaultPIDFile is the default path to the PID file for the systemd unit file config - SystemdDefaultPIDFile = "/run/teleport.pid" - // SystemdDefaultFileDescriptorLimit is the default max number of open file descriptors for the systemd unit file config - SystemdDefaultFileDescriptorLimit = 524288 + // DefaultEnvironmentFile is the default path to the env file for the systemd unit file config + DefaultEnvironmentFile = "/etc/default/teleport" + // DefaultPIDFile is the default path to the PID file for the systemd unit file config + DefaultPIDFile = "/run/teleport.pid" + // DefaultFileDescriptorLimit is the default max number of open file descriptors for the systemd unit file config + DefaultFileDescriptorLimit = 524288 ) -// systemdUnitFileTemplate is the systemd unit file configuration template. -var systemdUnitFileTemplate = template.Must(template.New("").Parse(`[Unit] +// unitFileTemplate is the systemd unit file configuration template. +var unitFileTemplate = template.Must(template.New("").Parse(`[Unit] Description=Teleport Service After=network.target @@ -47,7 +47,7 @@ Type=simple Restart=always RestartSec=5 EnvironmentFile=-{{ .EnvironmentFile }} -ExecStart={{ .TeleportInstallationFile }} start --config {{ .TeleportConfigPath }} --pid-file={{ .PIDFile }} +ExecStart={{ .TeleportInstallationFile }} start {{ if .FIPS }}--fips {{ end }}--config {{ .TeleportConfigPath }} --pid-file={{ .PIDFile }} # systemd before 239 needs an absolute path ExecReload=/bin/sh -c "exec pkill -HUP -L -F {{ .PIDFile }}" PIDFile={{ .PIDFile }} @@ -57,8 +57,8 @@ LimitNOFILE={{ .FileDescriptorLimit }} WantedBy=multi-user.target `)) -// SystemdFlags specifies configuration parameters for a systemd unit file. -type SystemdFlags struct { +// Flags specifies configuration parameters for a systemd unit file. +type Flags struct { // EnvironmentFile is the environment file path provided by the user. EnvironmentFile string // PIDFile is the process ID (PID) file path provided by the user. @@ -69,10 +69,12 @@ type SystemdFlags struct { TeleportInstallationFile string // TeleportConfigPath is the path to the teleport config file (as set by Teleport defaults) TeleportConfigPath string + // FIPS configures teleport to run in a FIPS compliant mode. + FIPS bool } // CheckAndSetDefaults checks and sets default values for the flags. -func (f *SystemdFlags) CheckAndSetDefaults() error { +func (f *Flags) CheckAndSetDefaults() error { if f.TeleportInstallationFile == "" { teleportPath, err := os.Readlink("/proc/self/exe") if err != nil { @@ -81,18 +83,19 @@ func (f *SystemdFlags) CheckAndSetDefaults() error { f.TeleportInstallationFile = teleportPath } // set Teleport config path to the default - f.TeleportConfigPath = defaults.ConfigFilePath - + if f.TeleportConfigPath == "" { + f.TeleportConfigPath = defaults.ConfigFilePath + } return nil } -// WriteSystemdUnitFile accepts flags and an io.Writer +// WriteUnitFile accepts flags and an io.Writer // and writes the systemd unit file configuration to it -func WriteSystemdUnitFile(flags SystemdFlags, dest io.Writer) error { +func WriteUnitFile(flags Flags, dest io.Writer) error { err := flags.CheckAndSetDefaults() if err != nil { return trace.Wrap(err) } - return trace.Wrap(systemdUnitFileTemplate.Execute(dest, flags)) + return trace.Wrap(unitFileTemplate.Execute(dest, flags)) } diff --git a/lib/config/systemd_test.go b/lib/config/systemd/systemd_test.go similarity index 89% rename from lib/config/systemd_test.go rename to lib/config/systemd/systemd_test.go index 8283a861147f1..19b38aacb57bb 100644 --- a/lib/config/systemd_test.go +++ b/lib/config/systemd/systemd_test.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package config +package systemd import ( "bytes" @@ -27,16 +27,17 @@ import ( "github.com/gravitational/teleport/lib/utils/testutils/golden" ) -func TestWriteSystemdUnitFile(t *testing.T) { - flags := SystemdFlags{ +func TestWriteUnitFile(t *testing.T) { + flags := Flags{ EnvironmentFile: "/custom/env/dir/teleport", PIDFile: "/custom/pid/dir/teleport.pid", FileDescriptorLimit: 16384, TeleportInstallationFile: "/custom/install/dir/teleport", + FIPS: true, } stdout := new(bytes.Buffer) - err := WriteSystemdUnitFile(flags, stdout) + err := WriteUnitFile(flags, stdout) require.NoError(t, err) data := stdout.Bytes() if golden.ShouldSet() { diff --git a/lib/config/testdata/TestWriteSystemdUnitFile.golden b/lib/config/systemd/testdata/TestWriteUnitFile.golden similarity index 75% rename from lib/config/testdata/TestWriteSystemdUnitFile.golden rename to lib/config/systemd/testdata/TestWriteUnitFile.golden index 1d3283771b57d..743fa77d0628b 100644 --- a/lib/config/testdata/TestWriteSystemdUnitFile.golden +++ b/lib/config/systemd/testdata/TestWriteUnitFile.golden @@ -7,7 +7,7 @@ Type=simple Restart=always RestartSec=5 EnvironmentFile=-/custom/env/dir/teleport -ExecStart=/custom/install/dir/teleport start --config /etc/teleport.yaml --pid-file=/custom/pid/dir/teleport.pid +ExecStart=/custom/install/dir/teleport start --fips --config /etc/teleport.yaml --pid-file=/custom/pid/dir/teleport.pid # systemd before 239 needs an absolute path ExecReload=/bin/sh -c "exec pkill -HUP -L -F /custom/pid/dir/teleport.pid" PIDFile=/custom/pid/dir/teleport.pid diff --git a/tool/teleport-update/main.go b/tool/teleport-update/main.go index 8e6890a5baf2a..039e5b826e219 100644 --- a/tool/teleport-update/main.go +++ b/tool/teleport-update/main.go @@ -52,8 +52,10 @@ const ( proxyServerEnvVar = "TELEPORT_PROXY" // updateGroupEnvVar allows the update group to be specified via env var. updateGroupEnvVar = "TELEPORT_UPDATE_GROUP" - // updateVersionEnvVar forces the version to specified value. + // updateVersionEnvVar specifies the Teleport version. updateVersionEnvVar = "TELEPORT_UPDATE_VERSION" + // updateFlagsEnvVar specifies Teleport version flags. + updateFlagsEnvVar = "TELEPORT_UPDATE_FLAGS" // updateLockTimeout is the duration commands will wait for update to complete before failing. updateLockTimeout = 10 * time.Minute @@ -130,7 +132,7 @@ func Run(args []string) int { enableCmd.Flag("force-version", "Force the provided version instead of using the version provided by the Teleport cluster."). Hidden().Short('f').Envar(updateVersionEnvVar).StringVar(&ccfg.ForceVersion) enableCmd.Flag("force-flag", "Force the provided version flags instead of using the version flags provided by the Teleport cluster."). - Hidden().StringsVar(&ccfg.ForceFlags) + Hidden().Envar(updateFlagsEnvVar).StringsVar(&ccfg.ForceFlags) enableCmd.Flag("self-setup", "Use the current teleport-update binary to create systemd service config for managed updates."). Hidden().BoolVar(&ccfg.SelfSetup) enableCmd.Flag("path", "Directory to link the active Teleport installation's binaries into."). @@ -152,7 +154,7 @@ func Run(args []string) int { pinCmd.Flag("force-version", "Force the provided version instead of using the version provided by the Teleport cluster."). Short('f').Envar(updateVersionEnvVar).StringVar(&ccfg.ForceVersion) pinCmd.Flag("force-flag", "Force the provided version flags instead of using the version flags provided by the Teleport cluster."). - Hidden().StringsVar(&ccfg.ForceFlags) + Hidden().Envar(updateFlagsEnvVar).StringsVar(&ccfg.ForceFlags) pinCmd.Flag("self-setup", "Use the current teleport-update binary to create systemd service config for managed updates."). Hidden().BoolVar(&ccfg.SelfSetup) pinCmd.Flag("path", "Directory to link the active Teleport installation's binaries into."). @@ -169,12 +171,18 @@ func Run(args []string) int { linkCmd := app.Command("link-package", "Link the system installation of Teleport from the Teleport package, if managed updates is disabled.") unlinkCmd := app.Command("unlink-package", "Unlink the system installation of Teleport from the Teleport package.") + // setupCmd is invoked by other versions of the updater, so this contract must be stable. + // New flags may be added, but they may only be passed via env vars when setup is invoked via the updater. setupCmd := app.Command("setup", "Write configuration files that run the update subcommand on a timer and verify the Teleport installation."). Hidden() setupCmd.Flag("reload", "Reload the Teleport agent. If not set, Teleport is not reloaded or restarted."). BoolVar(&ccfg.Reload) setupCmd.Flag("path", "Directory that the active Teleport installation's binaries are linked into."). Required().StringVar(&ccfg.Path) + setupCmd.Flag("version", "Use the provided version to generate configuration files."). + Envar(autoupdate.SetupVersionEnvVar).StringVar(&ccfg.ForceVersion) + setupCmd.Flag("flag", "Use the provided flags to generate configuration files."). + Envar(autoupdate.SetupFlagsEnvVar).StringsVar(&ccfg.ForceFlags) statusCmd := app.Command("status", "Show Teleport agent auto-update status.") statusCmd.Flag("err-if-should-update-now", @@ -468,7 +476,9 @@ func cmdSetup(ctx context.Context, ccfg *cliConfig) error { if err != nil { return trace.Wrap(err) } - err = updater.Setup(ctx, ccfg.Path, ccfg.Reload) + flags := common.NewInstallFlagsFromStrings(ccfg.ForceFlags) + rev := autoupdate.NewRevision(ccfg.ForceVersion, flags) + err = updater.Setup(ctx, ccfg.Path, rev, ccfg.Reload) if err != nil { return trace.Wrap(err) } diff --git a/tool/teleport/common/configurator.go b/tool/teleport/common/configurator.go index 887f330620d5e..32493f03e390d 100644 --- a/tool/teleport/common/configurator.go +++ b/tool/teleport/common/configurator.go @@ -31,6 +31,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/config" + "github.com/gravitational/teleport/lib/config/systemd" "github.com/gravitational/teleport/lib/configurators" awsconfigurators "github.com/gravitational/teleport/lib/configurators/aws" "github.com/gravitational/teleport/lib/configurators/configuratorbuilder" @@ -52,7 +53,7 @@ var awsDatabaseTypes = []string{ } type installSystemdFlags struct { - config.SystemdFlags + systemd.Flags // output is the destination to write the systemd unit file to. output string } @@ -76,7 +77,7 @@ func onDumpSystemdUnitFile(flags installSystemdFlags) error { } buf := new(bytes.Buffer) - err := config.WriteSystemdUnitFile(flags.SystemdFlags, buf) + err := systemd.WriteUnitFile(flags.Flags, buf) if err != nil { return trace.Wrap(err) } diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index 8f6c8b056208c..53d4140c98949 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -43,6 +43,7 @@ import ( debugclient "github.com/gravitational/teleport/lib/client/debug" awslib "github.com/gravitational/teleport/lib/cloud/aws" "github.com/gravitational/teleport/lib/config" + "github.com/gravitational/teleport/lib/config/systemd" "github.com/gravitational/teleport/lib/configurators" awsconfigurators "github.com/gravitational/teleport/lib/configurators/aws" "github.com/gravitational/teleport/lib/defaults" @@ -379,9 +380,9 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con // "teleport install" command and its subcommands installCmd := app.Command("install", "Teleport install commands.") systemdInstall := installCmd.Command("systemd", "Creates a systemd unit file configuration.") - systemdInstall.Flag("env-file", "Full path to the environment file.").Default(config.SystemdDefaultEnvironmentFile).StringVar(&systemdInstallFlags.EnvironmentFile) - systemdInstall.Flag("pid-file", "Full path to the PID file.").Default(config.SystemdDefaultPIDFile).StringVar(&systemdInstallFlags.PIDFile) - systemdInstall.Flag("fd-limit", "Maximum number of open file descriptors.").Default(fmt.Sprintf("%v", config.SystemdDefaultFileDescriptorLimit)).IntVar(&systemdInstallFlags.FileDescriptorLimit) + systemdInstall.Flag("env-file", "Full path to the environment file.").Default(systemd.DefaultEnvironmentFile).StringVar(&systemdInstallFlags.EnvironmentFile) + systemdInstall.Flag("pid-file", "Full path to the PID file.").Default(systemd.DefaultPIDFile).StringVar(&systemdInstallFlags.PIDFile) + systemdInstall.Flag("fd-limit", "Maximum number of open file descriptors.").Default(fmt.Sprintf("%v", systemd.DefaultFileDescriptorLimit)).IntVar(&systemdInstallFlags.FileDescriptorLimit) systemdInstall.Flag("teleport-path", "Full path to the Teleport binary.").StringVar(&systemdInstallFlags.TeleportInstallationFile) systemdInstall.Flag("output", `Write to stdout with "--output=stdout" or custom path with --output=file:///path`).Short('o').Default(teleport.SchemeStdout).StringVar(&systemdInstallFlags.output) systemdInstall.Alias(systemdInstallExamples) // We're using "alias" section to display usage examples.