Skip to content
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
6 changes: 5 additions & 1 deletion api/types/maintenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ import (
)

const (
// UpgraderKindKuberController is a short name used to identify the kube-controller-based
// UpgraderKindKubeController is a short name used to identify the kube-controller-based
// external upgrader variant.
UpgraderKindKubeController = "kube"

// UpgraderKindSystemdUnit is a short name used to identify the systemd-unit-based
// external upgrader variant.
UpgraderKindSystemdUnit = "unit"

// UpgraderKindTeleportUpdate is a short name used to identify the teleport-update
// external upgrader variant.
UpgraderKindTeleportUpdate = "binary"
Comment thread
codingllama marked this conversation as resolved.
)

var validWeekdays = [7]time.Weekday{
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6634,7 +6634,7 @@ func (a *Server) ExportUpgradeWindows(ctx context.Context, req proto.ExportUpgra
}

switch req.UpgraderKind {
case "":
case "", types.UpgraderKindTeleportUpdate:
rsp.CanonicalSchedule = cached.CanonicalSchedule.Clone()
case types.UpgraderKindKubeController:
rsp.KubeControllerSchedule = cached.KubeControllerSchedule
Expand Down
20 changes: 18 additions & 2 deletions lib/autoupdate/agent/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,15 @@ func (s SystemdService) Enable(ctx context.Context, now bool) error {
}

// Disable the systemd service.
func (s SystemdService) Disable(ctx context.Context) error {
func (s SystemdService) Disable(ctx context.Context, now bool) error {
if err := s.checkSystem(ctx); err != nil {
return trace.Wrap(err)
}
code := s.systemctl(ctx, slog.LevelInfo, "disable", s.ServiceName)
args := []string{"disable", s.ServiceName}
if now {
args = append(args, "--now")
}
code := s.systemctl(ctx, slog.LevelInfo, args...)
if code != 0 {
return trace.Errorf("unable to disable systemd service")
}
Expand Down Expand Up @@ -312,6 +316,18 @@ func (s SystemdService) IsEnabled(ctx context.Context) (bool, error) {
return false, nil
}

// IsPresent returns true if the service exists.
func (s SystemdService) IsPresent(ctx context.Context) (bool, error) {
if err := s.checkSystem(ctx); err != nil {
return false, trace.Wrap(err)
}
code := s.systemctl(ctx, slog.LevelDebug, "list-unit-files", "--quiet", s.ServiceName)
if code < 0 {
return false, trace.Errorf("unable to determine if systemd service %s is present", s.ServiceName)
}
return code == 0, nil
}

// checkSystem returns an error if the system is not compatible with this process manager.
func (s SystemdService) checkSystem(ctx context.Context) error {
_, err := os.Stat("/run/systemd/system")
Expand Down
54 changes: 48 additions & 6 deletions lib/autoupdate/agent/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ const (
systemNamespace = "system"
)

const (
// deprecatedTimerName is the timer for the deprecated upgrader should be disabled on setup.
deprecatedTimerName = "teleport-upgrade.timer"
)

const (
updateServiceTemplate = `# teleport-update
# DO NOT EDIT THIS FILE
Expand Down Expand Up @@ -105,7 +110,7 @@ type Namespace struct {
versionsDir string
// serviceFile for the Teleport systemd service (ns: /etc/systemd/system/teleport_myns.service)
serviceFile string
// configFile for Teleport config (ns: /opt/teleport/myns/etc/teleport.yaml)
// configFile for Teleport config (ns: /etc/teleport_myns.yaml)
configFile string
// pidFile for Teleport (ns: /run/teleport_myns.pid)
pidFile string
Expand Down Expand Up @@ -211,16 +216,35 @@ func (ns *Namespace) Setup(ctx context.Context) error {
if err != nil {
return trace.Wrap(err, "failed to write teleport-update systemd config files")
}
svc := &SystemdService{
timer := &SystemdService{
ServiceName: filepath.Base(ns.updaterTimerFile),
Log: ns.log,
}
if err := svc.Sync(ctx); err != nil {
if err := timer.Sync(ctx); err != nil {
return trace.Wrap(err, "failed to sync systemd config")
}
if err := svc.Enable(ctx, true); err != nil {
if err := timer.Enable(ctx, true); err != nil {
return trace.Wrap(err, "failed to enable teleport-update systemd timer")
}
if ns.name == "" {
oldTimer := &SystemdService{
ServiceName: deprecatedTimerName,
Log: ns.log,
}
// If the old teleport-upgrade script is detected, disable it to ensure they do not interfere.
// Note that the schedule is also set to nop by the Teleport agent -- this just prevents restarts.
enabled, err := oldTimer.IsEnabled(ctx)
if err != nil {
return trace.Wrap(err, "failed to determine if deprecated teleport-upgrade systemd timer is enabled")
Comment thread
sclevine marked this conversation as resolved.
}
if enabled {
if err := oldTimer.Disable(ctx, true); err != nil {
ns.log.ErrorContext(ctx, "The deprecated teleport-ent-updater package is installed on this server, and it cannot be disabled due to an error. You must remove the teleport-ent-updater package after verifying that teleport-update is working.", errorKey, err)
} else {
ns.log.WarnContext(ctx, "The deprecated teleport-ent-updater package is installed on this server. This package has been disabled to prevent conflicts. Please remove the teleport-ent-updater package after verifying that teleport-update is working.")
}
}
}
return nil
}

Expand All @@ -230,7 +254,7 @@ func (ns *Namespace) Teardown(ctx context.Context) error {
ServiceName: filepath.Base(ns.updaterTimerFile),
Log: ns.log,
}
if err := svc.Disable(ctx); err != nil {
if err := svc.Disable(ctx, true); err != nil {
return trace.Wrap(err, "failed to disable teleport-update systemd timer")
}
for _, p := range []string{
Expand All @@ -246,9 +270,27 @@ func (ns *Namespace) Teardown(ctx context.Context) error {
if err := svc.Sync(ctx); err != nil {
return trace.Wrap(err, "failed to sync systemd config")
}
if err := os.RemoveAll(ns.versionsDir); err != nil {
if err := os.RemoveAll(namespaceDir(ns.name)); err != nil {
return trace.Wrap(err, "failed to remove versions directory")
}
if ns.name == "" {
oldTimer := &SystemdService{
ServiceName: deprecatedTimerName,
Log: ns.log,
}
// If the old upgrader exists, attempt to re-enable it automatically
Comment thread
codingllama marked this conversation as resolved.
present, err := oldTimer.IsPresent(ctx)
if err != nil {
return trace.Wrap(err, "failed to determine if deprecated teleport-upgrade systemd timer is present")
Comment thread
sclevine marked this conversation as resolved.
}
if present {
if err := oldTimer.Enable(ctx, true); err != nil {
ns.log.ErrorContext(ctx, "The deprecated teleport-ent-updater package is installed on this server, and it cannot be re-enabled due to an error. Please fix the teleport-ent-updater package if you intend to use the deprecated updater.", errorKey, err)
} else {
ns.log.WarnContext(ctx, "The deprecated teleport-ent-updater package is installed on this server. This package has been re-enabled to ensure continued updates. To disable automatic updates entirely, please remove the teleport-ent-updater package.")
}
}
}
return nil
}

Expand Down
80 changes: 80 additions & 0 deletions lib/autoupdate/agent/telemetry.go
Comment thread
sclevine marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Teleport
* Copyright (C) 2025 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 (
"os"
"path/filepath"
"strings"

"github.com/gravitational/trace"
)

// IsManagedByUpdater returns true if the local Teleport binary is managed by teleport-update.
// Note that true may be returned even if auto-updates is disabled or the version is pinned.
// The binary is considered managed if it lives under /opt/teleport, but not within the package
Comment thread
codingllama marked this conversation as resolved.
// path at /opt/teleport/system.
func IsManagedByUpdater() (bool, error) {
teleportPath, err := os.Readlink("/proc/self/exe")
if err != nil {
return false, trace.Wrap(err, "cannot find Teleport binary")
}
// Check if current binary is under the updater-managed path.
managed, err := hasParentDir(teleportPath, teleportOptDir)
if err != nil {
return false, trace.Wrap(err)
}
if !managed {
return false, nil
}
// Return false if the binary is under the updater-managed path, but in the system prefix reserved for the package.
system, err := hasParentDir(teleportPath, filepath.Join(teleportOptDir, systemNamespace))
return !system, err
}

// IsManagedAndDefault returns true if the local Teleport binary is both managed by teleport-update
// and the default installation (with teleport.service as the unit file name).
// The binary is considered managed and default if it lives within /opt/teleport/default.
func IsManagedAndDefault() (bool, error) {
teleportPath, err := os.Readlink("/proc/self/exe")
if err != nil {
return false, trace.Wrap(err, "cannot find Teleport binary")
}
return hasParentDir(teleportPath, filepath.Join(teleportOptDir, defaultNamespace))
}

// hasParentDir returns true if dir is any parent directory of parent.
// hasParentDir does not resolve symlinks, and requires that files be represented the same way in dir and parent.
func hasParentDir(dir, parent string) (bool, error) {
// Note that os.Stat + os.SameFile would be more reliable,
// but does not work well for arbitrarily nested subdirectories.
absDir, err := filepath.Abs(dir)
if err != nil {
return false, trace.Wrap(err, "cannot get absolute path for directory %s", dir)
}
absParent, err := filepath.Abs(parent)
if err != nil {
return false, trace.Wrap(err, "cannot get absolute path for parent directory %s", dir)
}
sep := string(filepath.Separator)
if !strings.HasSuffix(absParent, sep) {
absParent += sep
}
return strings.HasPrefix(absDir, absParent), nil
}
109 changes: 109 additions & 0 deletions lib/autoupdate/agent/telemetry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Teleport
* Copyright (C) 2025 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 (
"testing"

"github.com/stretchr/testify/require"
)

func TestHasParentDir(t *testing.T) {
tests := []struct {
name string
path string
parent string
wantResult bool
}{
{
name: "Has valid parent directory",
path: "/opt/teleport/dir/test",
parent: "/opt/teleport",
wantResult: true,
},
{
name: "Has valid parent directory with slash",
path: "/opt/teleport/dir/test",
parent: "/opt/teleport/",
wantResult: true,
},
{
name: "Parent directory is root",
path: "/opt/teleport/dir",
parent: "/",
wantResult: true,
},
{
name: "Parent is the same as the path",
path: "/opt/teleport/dir",
parent: "/opt/teleport/dir",
wantResult: false,
},
{
name: "Parent the same as the path but without slash",
path: "/opt/teleport/dir/",
parent: "/opt/teleport/dir",
wantResult: false,
},
{
name: "Parent the same as the path but with slash",
path: "/opt/teleport/dir",
parent: "/opt/teleport/dir/",
wantResult: false,
},
{
name: "Parent is substring of the path",
path: "/opt/teleport/dir-place",
parent: "/opt/teleport/dir",
wantResult: false,
},
{
name: "Parent is in path",
path: "/opt/teleport",
parent: "/opt/teleport/dir",
wantResult: false,
},
{
name: "Empty parent",
path: "/opt/teleport/dir",
parent: "",
wantResult: false,
},
{
name: "Empty path",
path: "",
parent: "/opt/teleport",
wantResult: false,
},
{
name: "Both empty",
path: "",
parent: "",
wantResult: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := hasParentDir(tt.path, tt.parent)
require.NoError(t, err)
require.Equal(t, tt.wantResult, result)
})
}
}
Loading