From 4ffaa8956a603cf2759fa11f48b8ea8917ef9726 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Fri, 24 Jan 2025 13:27:32 -0800 Subject: [PATCH] Add template for client tools auto-update download url (#51210) * Add templates for client tools auto-update download url * Change to base url setting by env MakeURL moved to common function to be general for both, agent and client tools * Add godoc for common function and constant for default package * Use flags and version arguments instead of revision * Move base url env to shared constant --- integration/autoupdate/tools/main_test.go | 8 +-- lib/autoupdate/package_url.go | 82 +++++++++++++++++++++++ lib/autoupdate/tools/helper.go | 18 +++-- lib/autoupdate/tools/updater.go | 20 ++++-- lib/autoupdate/tools/utils.go | 58 ++++++++-------- 5 files changed, 139 insertions(+), 47 deletions(-) create mode 100644 lib/autoupdate/package_url.go diff --git a/integration/autoupdate/tools/main_test.go b/integration/autoupdate/tools/main_test.go index 71173892ca2f1..50aeea81fea30 100644 --- a/integration/autoupdate/tools/main_test.go +++ b/integration/autoupdate/tools/main_test.go @@ -83,7 +83,7 @@ func TestMain(m *testing.M) { })) baseURL = server.URL for _, version := range testVersions { - if err := buildAndArchiveApps(ctx, tmp, toolsDir, version, server.URL); err != nil { + if err := buildAndArchiveApps(ctx, tmp, version, server.URL); err != nil { log.Fatalf("failed to build testing app binary archive: %v", err) } } @@ -131,7 +131,7 @@ func serve256File(w http.ResponseWriter, _ *http.Request, filePath string) { } // buildAndArchiveApps compiles the updater integration and pack it depends on platform is used. -func buildAndArchiveApps(ctx context.Context, path string, toolsDir string, version string, baseURL string) error { +func buildAndArchiveApps(ctx context.Context, path string, version string, baseURL string) error { versionPath := filepath.Join(path, version) for _, app := range []string{"tsh", "tctl"} { output := filepath.Join(versionPath, app) @@ -141,7 +141,7 @@ func buildAndArchiveApps(ctx context.Context, path string, toolsDir string, vers case constants.DarwinOS: output = filepath.Join(versionPath, app+".app", "Contents", "MacOS", app) } - if err := buildBinary(output, toolsDir, version, baseURL, app); err != nil { + if err := buildBinary(output, version, baseURL, app); err != nil { return trace.Wrap(err) } } @@ -159,7 +159,7 @@ func buildAndArchiveApps(ctx context.Context, path string, toolsDir string, vers } // buildBinary executes command to build client tool binary with updater logic for testing. -func buildBinary(output string, toolsDir string, version string, baseURL string, app string) error { +func buildBinary(output string, version string, baseURL string, app string) error { cmd := exec.Command( "go", "build", "-o", output, "-ldflags", strings.Join([]string{ diff --git a/lib/autoupdate/package_url.go b/lib/autoupdate/package_url.go new file mode 100644 index 0000000000000..9b283c3da59c2 --- /dev/null +++ b/lib/autoupdate/package_url.go @@ -0,0 +1,82 @@ +/* + * 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 . + */ + +package autoupdate + +import ( + "bytes" + "runtime" + "text/template" + + "github.com/gravitational/trace" +) + +// InstallFlags sets flags for the Teleport installation. +type InstallFlags int + +const ( + // FlagEnterprise installs enterprise Teleport. + FlagEnterprise InstallFlags = 1 << iota + // FlagFIPS installs FIPS Teleport + FlagFIPS +) + +const ( + // DefaultBaseURL is CDN URL for downloading official Teleport packages. + DefaultBaseURL = "https://cdn.teleport.dev" + // DefaultPackage is the name of Teleport package. + DefaultPackage = "teleport" + // DefaultCDNURITemplate is the default template for the Teleport CDN download URL. + DefaultCDNURITemplate = `{{ .BaseURL }}/ + {{- if eq .OS "darwin" }} + {{- .Package }}{{ if and .Enterprise (eq .Package "teleport") }}-ent{{ end }}-{{ .Version }}.pkg + {{- else if eq .OS "windows" }} + {{- .Package }}-v{{ .Version }}-{{ .OS }}-amd64-bin.zip + {{- else }} + {{- .Package }}{{ if .Enterprise }}-ent{{ end }}-v{{ .Version }}-{{ .OS }}-{{ .Arch }}{{ if .FIPS }}-fips{{ end }}-bin.tar.gz + {{- end }}` + // BaseURLEnvVar allows to override base URL for the Teleport package URL via env var. + BaseURLEnvVar = "TELEPORT_CDN_BASE_URL" +) + +// MakeURL constructs the package download URL from template, base URL and revision. +func MakeURL(uriTmpl string, baseURL string, pkg string, version string, flags InstallFlags) (string, error) { + tmpl, err := template.New("uri").Parse(uriTmpl) + if err != nil { + return "", trace.Wrap(err) + } + var uriBuf bytes.Buffer + params := struct { + BaseURL, OS, Version, Arch, Package string + FIPS, Enterprise bool + }{ + BaseURL: baseURL, + OS: runtime.GOOS, + Version: version, + Arch: runtime.GOARCH, + FIPS: flags&FlagFIPS != 0, + Enterprise: flags&(FlagEnterprise|FlagFIPS) != 0, + Package: pkg, + } + err = tmpl.Execute(&uriBuf, params) + if err != nil { + return "", trace.Wrap(err) + } + + return uriBuf.String(), nil +} diff --git a/lib/autoupdate/tools/helper.go b/lib/autoupdate/tools/helper.go index a3322f88d767b..f7d3e691b2ef4 100644 --- a/lib/autoupdate/tools/helper.go +++ b/lib/autoupdate/tools/helper.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/autoupdate" stacksignal "github.com/gravitational/teleport/lib/utils/signal" ) @@ -35,7 +36,7 @@ var ( // version is the current version of the Teleport. version = teleport.Version // baseURL is CDN URL for downloading official Teleport packages. - baseURL = defaultBaseURL + baseURL = autoupdate.DefaultBaseURL ) // CheckAndUpdateLocal verifies if the TELEPORT_TOOLS_VERSION environment variable @@ -52,6 +53,11 @@ func CheckAndUpdateLocal(ctx context.Context, reExecArgs []string) error { return nil } + // Overrides default base URL for custom CDN for downloading updates. + if envBaseURL := os.Getenv(autoupdate.BaseURLEnvVar); envBaseURL != "" { + baseURL = envBaseURL + } + updater := NewUpdater(toolsDir, version, WithBaseURL(baseURL)) // At process startup, check if a version has already been downloaded to // $TELEPORT_HOME/bin or if the user has set the TELEPORT_TOOLS_VERSION @@ -80,12 +86,12 @@ func CheckAndUpdateRemote(ctx context.Context, proxy string, insecure bool, reEx slog.WarnContext(ctx, "Client tools update is disabled", "error", err) return nil } + // Overrides default base URL for custom CDN for downloading updates. + if envBaseURL := os.Getenv(autoupdate.BaseURLEnvVar); envBaseURL != "" { + baseURL = envBaseURL + } + updater := NewUpdater(toolsDir, version, WithBaseURL(baseURL)) - // The user has typed a command like `tsh ssh ...` without being logged in, - // if the running binary needs to be updated, update and re-exec. - // - // If needed, download the new version of client tools and re-exec. Make - // sure to exit this process with the same exit code as the child process. toolsVersion, reExec, err := updater.CheckRemote(ctx, proxy, insecure) if err != nil { return trace.Wrap(err) diff --git a/lib/autoupdate/tools/updater.go b/lib/autoupdate/tools/updater.go index 2845864f9c5ef..b148be735aedb 100644 --- a/lib/autoupdate/tools/updater.go +++ b/lib/autoupdate/tools/updater.go @@ -42,6 +42,7 @@ import ( "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/teleport/lib/autoupdate" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/packaging" ) @@ -49,8 +50,6 @@ import ( const ( // teleportToolsVersionEnv is environment name for requesting specific version for update. teleportToolsVersionEnv = "TELEPORT_TOOLS_VERSION" - // defaultBaseURL is CDN URL for downloading official Teleport packages. - defaultBaseURL = "https://cdn.teleport.dev" // reservedFreeDisk is the predefined amount of free disk space (in bytes) required // to remain available after downloading archives. reservedFreeDisk = 10 * 1024 * 1024 // 10 Mb @@ -75,6 +74,13 @@ func WithBaseURL(baseURL string) Option { } } +// WithURITemplate defines custom URI template for the updater. +func WithURITemplate(uriTemplate string) Option { + return func(u *Updater) { + u.uriTemplate = uriTemplate + } +} + // WithClient defines custom http client for the Updater. func WithClient(client *http.Client) Option { return func(u *Updater) { @@ -94,9 +100,10 @@ type Updater struct { toolsDir string localVersion string tools []string + uriTemplate string + baseURL string - baseURL string - client *http.Client + client *http.Client } // NewUpdater initializes the updater for client tools auto updates. We need to specify the tools directory @@ -109,7 +116,8 @@ func NewUpdater(toolsDir, localVersion string, options ...Option) *Updater { tools: DefaultClientTools(), toolsDir: toolsDir, localVersion: localVersion, - baseURL: defaultBaseURL, + uriTemplate: autoupdate.DefaultCDNURITemplate, + baseURL: autoupdate.DefaultBaseURL, client: http.DefaultClient, } for _, option := range options { @@ -255,7 +263,7 @@ func (u *Updater) UpdateWithLock(ctx context.Context, updateToolsVersion string) // with defined updater directory suffix. func (u *Updater) Update(ctx context.Context, toolsVersion string) error { // Get platform specific download URLs. - packages, err := teleportPackageURLs(u.baseURL, toolsVersion) + packages, err := teleportPackageURLs(u.uriTemplate, u.baseURL, toolsVersion) if err != nil { return trace.Wrap(err) } diff --git a/lib/autoupdate/tools/utils.go b/lib/autoupdate/tools/utils.go index f937d228b5cd4..eb9e5f7ad8a9f 100644 --- a/lib/autoupdate/tools/utils.go +++ b/lib/autoupdate/tools/utils.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/autoupdate" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/utils" ) @@ -125,41 +126,36 @@ type packageURL struct { Optional bool } -// teleportPackageURLs returns the URL for the Teleport archive to download. The format is: -// https://cdn.teleport.dev/teleport-{, ent-}v15.3.0-{linux, darwin, windows}-{amd64,arm64,arm,386}-{fips-}bin.tar.gz -func teleportPackageURLs(baseURL, toolsVersion string) ([]packageURL, error) { - switch runtime.GOOS { - case "darwin": - tsh := baseURL + "/tsh-" + toolsVersion + ".pkg" - teleport := baseURL + "/teleport-" + toolsVersion + ".pkg" - return []packageURL{ - {Archive: teleport, Hash: teleport + ".sha256"}, - {Archive: tsh, Hash: tsh + ".sha256", Optional: true}, - }, nil - case "windows": - archive := baseURL + "/teleport-v" + toolsVersion + "-windows-amd64-bin.zip" - return []packageURL{ - {Archive: archive, Hash: archive + ".sha256"}, - }, nil - case "linux": - m := modules.GetModules() - var b strings.Builder - b.WriteString(baseURL + "/teleport-") - if m.IsEnterpriseBuild() || m.IsBoringBinary() { - b.WriteString("ent-") - } - b.WriteString("v" + toolsVersion + "-" + runtime.GOOS + "-" + runtime.GOARCH + "-") - if m.IsBoringBinary() { - b.WriteString("fips-") +// teleportPackageURLs returns the URL for the Teleport archive to download. +func teleportPackageURLs(uriTmpl string, baseURL, version string) ([]packageURL, error) { + var flags autoupdate.InstallFlags + m := modules.GetModules() + if m.IsBoringBinary() { + flags |= autoupdate.FlagFIPS + } + if m.IsEnterpriseBuild() || m.IsBoringBinary() { + flags |= autoupdate.FlagEnterprise + } + + teleportURL, err := autoupdate.MakeURL(uriTmpl, baseURL, autoupdate.DefaultPackage, version, flags) + if err != nil { + return nil, trace.Wrap(err) + } + if runtime.GOOS == constants.DarwinOS { + tshURL, err := autoupdate.MakeURL(uriTmpl, baseURL, "tsh", version, flags) + if err != nil { + return nil, trace.Wrap(err) } - b.WriteString("bin.tar.gz") - archive := b.String() + return []packageURL{ - {Archive: archive, Hash: archive + ".sha256"}, + {Archive: teleportURL, Hash: teleportURL + ".sha256"}, + {Archive: tshURL, Hash: tshURL + ".sha256", Optional: true}, }, nil - default: - return nil, trace.BadParameter("unsupported runtime: %v", runtime.GOOS) } + + return []packageURL{ + {Archive: teleportURL, Hash: teleportURL + ".sha256"}, + }, nil } // toolName returns the path to {tsh, tctl} for the executable that started