diff --git a/lib/service/service.go b/lib/service/service.go
index e2e76574b414d..7defc6e192fad 100644
--- a/lib/service/service.go
+++ b/lib/service/service.go
@@ -146,6 +146,7 @@ import (
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/cert"
vc "github.com/gravitational/teleport/lib/versioncontrol"
+ "github.com/gravitational/teleport/lib/versioncontrol/endpoint"
uw "github.com/gravitational/teleport/lib/versioncontrol/upgradewindow"
"github.com/gravitational/teleport/lib/web"
)
@@ -1076,6 +1077,37 @@ func NewTeleport(cfg *servicecfg.Config) (*TeleportProcess, error) {
process.log.Warnf("Use of external upgraders on control-plane instances is not recommended.")
}
+ if upgraderKind == "unit" {
+ process.RegisterFunc("autoupdates.endpoint.export", func() error {
+ component := teleport.Component("autoupdates:endpoint:export", process.id)
+ logger := process.log.WithFields(logrus.Fields{
+ trace.Component: component,
+ })
+ conn, err := waitForInstanceConnector(process, logger)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ if conn == nil {
+ return trace.BadParameter("process exiting and Instance connector never became available")
+ }
+
+ resp, err := conn.Client.Ping(process.ExitContext())
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ if !resp.GetServerFeatures().GetCloud() {
+ return nil
+ }
+
+ if err := endpoint.Export(process.ExitContext(), resolverAddr.String()); err != nil {
+ logger.Warnf("Failed to export and validate autoupdates endpoint (addr=%s): %v", resolverAddr.String(), err)
+ return trace.Wrap(err)
+ }
+ logger.Infof("Exported autoupdates endpoint (addr=%s).", resolverAddr.String())
+ return nil
+ })
+ }
+
driver, err := uw.NewDriver(upgraderKind)
if err != nil {
return nil, trace.Wrap(err)
diff --git a/lib/versioncontrol/constants.go b/lib/versioncontrol/constants.go
new file mode 100644
index 0000000000000..c5a5e7a59308b
--- /dev/null
+++ b/lib/versioncontrol/constants.go
@@ -0,0 +1,24 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package versioncontrol
+
+const (
+ // UnitConfigDir is the configuration directory of the teleport-upgrade unit.
+ UnitConfigDir = "/etc/teleport-upgrade.d"
+)
diff --git a/lib/versioncontrol/endpoint/endpoint.go b/lib/versioncontrol/endpoint/endpoint.go
new file mode 100644
index 0000000000000..41d451612a80c
--- /dev/null
+++ b/lib/versioncontrol/endpoint/endpoint.go
@@ -0,0 +1,102 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package endpoint
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+
+ "github.com/gravitational/trace"
+
+ versionlib "github.com/gravitational/teleport/lib/automaticupgrades/version"
+ "github.com/gravitational/teleport/lib/defaults"
+ "github.com/gravitational/teleport/lib/versioncontrol"
+)
+
+const stableCloudPath = "v1/webapi/automaticupgrades/channel/stable/cloud"
+
+// Export exports the proxy version server config.
+func Export(ctx context.Context, proxyAddr string) error {
+ versionEndpoint := fmt.Sprint(path.Join(proxyAddr, stableCloudPath))
+ if err := verifyVersionEndpoint(ctx, versionEndpoint); err != nil {
+ return trace.Wrap(err, "version endpoint may be invalid or unreachable")
+ }
+
+ appliedEndpoint, err := exportEndpoint(versioncontrol.UnitConfigDir, versionEndpoint)
+ if err != nil {
+ return trace.Wrap(err, "failed to export version endpoint")
+ }
+
+ if err := verifyVersionEndpoint(ctx, appliedEndpoint); err != nil {
+ return trace.Wrap(err, "applied version endpoint may be invalid or unreachable")
+ }
+
+ return nil
+}
+
+// verifyVersionEndpoint verifies that the provided endpoint serves a valid teleport
+// version.
+func verifyVersionEndpoint(ctx context.Context, endpoint string) error {
+ baseURL, err := url.Parse(fmt.Sprintf("https://%s", endpoint))
+ if err != nil {
+ return trace.Wrap(err)
+ }
+ versionGetter := versionlib.NewBasicHTTPVersionGetter(baseURL)
+ _, err = versionGetter.GetVersion(ctx)
+ return trace.Wrap(err)
+}
+
+// exportEndpoint exports the versionEndpoint to the specified config directory.
+// If an existing value is already present, it will not be overwritten. The resulting
+// version endpoint value will be returned.
+func exportEndpoint(configDir, versionEndpoint string) (string, error) {
+ // ensure config dir exists. if created it is set to 755, which is reasonably safe and seems to
+ // be the standard choice for config dirs like this in /etc/.
+ if err := os.MkdirAll(configDir, defaults.DirectoryPermissions); err != nil {
+ return "", trace.Wrap(err)
+ }
+
+ // open/create endpoint file. if created it is set to 644, which is reasonable for a sensitive but non-secret config value.
+ endpointFile, err := os.OpenFile(path.Join(configDir, "endpoint"), os.O_RDWR|os.O_CREATE, defaults.FilePermissions)
+ if err != nil {
+ return "", trace.Wrap(err, "failed to open endpoint config file")
+ }
+ defer endpointFile.Close()
+
+ b, err := io.ReadAll(endpointFile)
+ if err != nil {
+ return "", trace.Wrap(err, "failed to read endpoint config file")
+ }
+
+ // Do not overwrite if an endpoint value is already configured.
+ if len(b) != 0 {
+ return strings.TrimSuffix(string(b), "\n"), nil
+ }
+
+ _, err = endpointFile.Write([]byte(versionEndpoint))
+ if err != nil {
+ return "", trace.Wrap(err, "failed to write endpoint config file")
+ }
+ return versionEndpoint, nil
+}
diff --git a/lib/versioncontrol/endpoint/endpoint_test.go b/lib/versioncontrol/endpoint/endpoint_test.go
new file mode 100644
index 0000000000000..be6fca21a3b68
--- /dev/null
+++ b/lib/versioncontrol/endpoint/endpoint_test.go
@@ -0,0 +1,91 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package endpoint
+
+import (
+ "os"
+ "path"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+const testDir = "export-endpoint-test"
+
+func Test_exportEndpoint(t *testing.T) {
+ tests := []struct {
+ name string
+ endpoint string
+ expected string
+ initConfigDir func() string
+ }{
+ {
+ name: "create endpoint file and write value",
+ endpoint: "v1/stable/cloud",
+ expected: "v1/stable/cloud",
+ initConfigDir: func() string {
+ tmpDir, err := os.MkdirTemp("", testDir)
+ require.NoError(t, err)
+ t.Cleanup(func() { os.RemoveAll(tmpDir) })
+ return tmpDir
+ },
+ },
+ {
+ name: "write value",
+ endpoint: "v1/stable/cloud",
+ expected: "v1/stable/cloud",
+ initConfigDir: func() string {
+ tmpDir, err := os.MkdirTemp("", testDir)
+ require.NoError(t, err)
+ t.Cleanup(func() { os.RemoveAll(tmpDir) })
+
+ endpointFile, err := os.Create(path.Join(tmpDir, "endpoint"))
+ require.NoError(t, err)
+ require.NoError(t, endpointFile.Close())
+ return tmpDir
+ },
+ },
+ {
+ name: "endpoint value already configured",
+ endpoint: "v1/stable/cloud",
+ expected: "existing/endpoint",
+ initConfigDir: func() string {
+ tmpDir, err := os.MkdirTemp("", testDir)
+ require.NoError(t, err)
+ t.Cleanup(func() { os.RemoveAll(tmpDir) })
+
+ endpointFile, err := os.Create(path.Join(tmpDir, "endpoint"))
+ require.NoError(t, err)
+
+ _, err = endpointFile.Write([]byte("existing/endpoint"))
+ require.NoError(t, err)
+ require.NoError(t, endpointFile.Close())
+ return tmpDir
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ configDir := tt.initConfigDir()
+ appliedEndpoint, err := exportEndpoint(configDir, tt.endpoint)
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, appliedEndpoint)
+ })
+ }
+}
diff --git a/lib/versioncontrol/upgradewindow/upgradewindow.go b/lib/versioncontrol/upgradewindow/upgradewindow.go
index 51a05a7cd5b93..53c2905a6dccd 100644
--- a/lib/versioncontrol/upgradewindow/upgradewindow.go
+++ b/lib/versioncontrol/upgradewindow/upgradewindow.go
@@ -34,6 +34,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/interval"
+ "github.com/gravitational/teleport/lib/versioncontrol"
)
const (
@@ -42,9 +43,6 @@ const (
// unitScheduleFile is the name of the file to which the unit schedule is exported.
unitScheduleFile = "schedule"
-
- // unitConfigDir is the configuration directory of the teleport-upgrade unit.
- unitConfigDir = "/etc/teleport-upgrade.d"
)
// ExportFunc represents the ExportUpgradeWindows rpc exposed by auth servers.
@@ -397,7 +395,7 @@ type systemdDriver struct {
func NewSystemdUnitDriver(cfg SystemdUnitDriverConfig) (Driver, error) {
if cfg.ConfigDir == "" {
- cfg.ConfigDir = unitConfigDir
+ cfg.ConfigDir = versioncontrol.UnitConfigDir
}
return &systemdDriver{cfg: cfg}, nil