From ddcbaf92ac59d2862f242e8f54f9cfdb606955a0 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Thu, 10 Oct 2024 14:52:35 -0400 Subject: [PATCH 01/14] Add client tools auto update --- integration/autoupdate/client_update_test.go | 231 ++++++++++++ integration/autoupdate/helper_test.go | 89 +++++ integration/autoupdate/helper_unix_test.go | 37 ++ integration/autoupdate/helper_windows_test.go | 42 +++ integration/autoupdate/main_test.go | 189 ++++++++++ integration/autoupdate/updater/main.go | 85 +++++ lib/autoupdate/client_update.go | 353 ++++++++++++++++++ lib/autoupdate/feature_ent.go | 26 ++ lib/autoupdate/feature_fips.go | 26 ++ lib/autoupdate/progress.go | 81 ++++ lib/autoupdate/utils.go | 168 +++++++++ lib/utils/disk.go | 6 +- lib/utils/packaging/unarchive_unix.go | 4 +- 13 files changed, 1332 insertions(+), 5 deletions(-) create mode 100644 integration/autoupdate/client_update_test.go create mode 100644 integration/autoupdate/helper_test.go create mode 100644 integration/autoupdate/helper_unix_test.go create mode 100644 integration/autoupdate/helper_windows_test.go create mode 100644 integration/autoupdate/main_test.go create mode 100644 integration/autoupdate/updater/main.go create mode 100644 lib/autoupdate/client_update.go create mode 100644 lib/autoupdate/feature_ent.go create mode 100644 lib/autoupdate/feature_fips.go create mode 100644 lib/autoupdate/progress.go create mode 100644 lib/autoupdate/utils.go diff --git a/integration/autoupdate/client_update_test.go b/integration/autoupdate/client_update_test.go new file mode 100644 index 000000000000..0fb76bef5c3d --- /dev/null +++ b/integration/autoupdate/client_update_test.go @@ -0,0 +1,231 @@ +/* + * 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 autoupdate_test + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/teleport/lib/autoupdate" +) + +var ( + // pattern is template for response on version command for client tools {tsh, tctl}. + pattern = regexp.MustCompile(`(?m)Teleport v(.*) git`) +) + +// TestUpdate verifies the basic update logic. We first download a lower version, then request +// an update to a newer version, expecting it to re-execute with the updated version. +func TestUpdate(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Fetch compiled test binary with updater logic and install to $TELEPORT_HOME. + updater := autoupdate.NewClientUpdater( + clientTools(), + toolsDir, + testVersions[0], + autoupdate.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + ) + err := updater.Update(ctx, testVersions[0]) + require.NoError(t, err) + + // Verify that the installed version is equal to requested one. + cmd := exec.CommandContext(ctx, filepath.Join(toolsDir, "tsh"), "version") + out, err := cmd.Output() + require.NoError(t, err) + + matches := pattern.FindStringSubmatch(string(out)) + require.Len(t, matches, 2) + require.Equal(t, testVersions[0], matches[1]) + + // Execute version command again with setting the new version which must + // trigger re-execution of the same command after downloading requested version. + cmd = exec.CommandContext(ctx, filepath.Join(toolsDir, "tsh"), "version") + cmd.Env = append( + os.Environ(), + fmt.Sprintf("%s=%s", teleportToolsVersion, testVersions[1]), + ) + out, err = cmd.Output() + require.NoError(t, err) + + matches = pattern.FindStringSubmatch(string(out)) + require.Len(t, matches, 2) + require.Equal(t, testVersions[1], matches[1]) +} + +// TestParallelUpdate launches multiple updater commands in parallel while defining a new version. +// The first process should acquire a lock and block execution for the other processes. After the +// first update is complete, other processes should acquire the lock one by one and re-execute +// the command with the updated version without any new downloads. +func TestParallelUpdate(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Initial fetch the updater binary un-archive and replace. + updater := autoupdate.NewClientUpdater( + clientTools(), + toolsDir, + testVersions[0], + autoupdate.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + ) + err := updater.Update(ctx, testVersions[0]) + require.NoError(t, err) + + // By setting the limit request next test http serving file going blocked until unlock is sent. + lock := make(chan struct{}) + limitedWriter.SetLimitRequest(limitRequest{ + limit: 1024, + lock: lock, + }) + + var outputs [3]bytes.Buffer + errChan := make(chan error, cap(outputs)) + for i := 0; i < cap(outputs); i++ { + cmd := exec.Command(filepath.Join(toolsDir, "tsh"), "version") + cmd.Stdout = &outputs[i] + cmd.Stderr = &outputs[i] + cmd.Env = append( + os.Environ(), + fmt.Sprintf("%s=%s", teleportToolsVersion, testVersions[1]), + ) + err = cmd.Start() + require.NoError(t, err, "failed to start updater") + + go func(cmd *exec.Cmd) { + errChan <- cmd.Wait() + }(cmd) + } + + select { + case err := <-errChan: + require.Fail(t, "we shouldn't receive any error", err) + case <-time.After(5 * time.Second): + require.Fail(t, "failed to wait till the download is started") + case <-lock: + // Wait for a short period to allow other processes to launch and attempt to acquire the lock. + time.Sleep(100 * time.Millisecond) + lock <- struct{}{} + } + + // Wait till process finished with exit code 0, but we still should get progress + // bar in output content. + for i := 0; i < cap(outputs); i++ { + select { + case <-time.After(5 * time.Second): + require.Fail(t, "failed to wait till the process is finished") + case err := <-errChan: + require.NoError(t, err) + } + } + + var progressCount int + for i := 0; i < cap(outputs); i++ { + matches := pattern.FindStringSubmatch(outputs[i].String()) + require.Len(t, matches, 2) + assert.Equal(t, testVersions[1], matches[1]) + if strings.Contains(outputs[i].String(), "Update progress:") { + progressCount++ + } + } + assert.Equal(t, 1, progressCount, "we should have only one progress bar downloading new version") +} + +// TestUpdateInterruptSignal verifies the interrupt signal send to the process must stop downloading. +func TestUpdateInterruptSignal(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Initial fetch the updater binary un-archive and replace. + updater := autoupdate.NewClientUpdater( + clientTools(), + toolsDir, + testVersions[0], + autoupdate.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + ) + err := updater.Update(ctx, testVersions[0]) + require.NoError(t, err) + + var output bytes.Buffer + cmd := exec.Command(filepath.Join(toolsDir, "tsh"), "version") + cmd.Stdout = &output + cmd.Stderr = &output + cmd.Env = append( + os.Environ(), + fmt.Sprintf("%s=%s", teleportToolsVersion, testVersions[1]), + ) + err = cmd.Start() + require.NoError(t, err, "failed to start updater") + pid := cmd.Process.Pid + + errChan := make(chan error) + go func() { + errChan <- cmd.Wait() + }() + + // By setting the limit request next test http serving file going blocked until unlock is sent. + lock := make(chan struct{}) + limitedWriter.SetLimitRequest(limitRequest{ + limit: 1024, + lock: lock, + }) + + select { + case err := <-errChan: + require.Fail(t, "we shouldn't receive any error", err) + case <-time.After(5 * time.Second): + require.Fail(t, "failed to wait till the download is started") + case <-lock: + time.Sleep(100 * time.Millisecond) + require.NoError(t, sendInterrupt(pid)) + lock <- struct{}{} + } + + // Wait till process finished with exit code 0, but we still should get progress + // bar in output content. + select { + case <-time.After(5 * time.Second): + require.Fail(t, "failed to wait till the process interrupted") + case err := <-errChan: + require.NoError(t, err) + } + assert.Contains(t, output.String(), "Update progress:") +} + +func clientTools() []string { + switch runtime.GOOS { + case constants.WindowsOS: + return []string{"tsh.exe", "tctl.exe"} + default: + return []string{"tsh", "tctl"} + } +} diff --git a/integration/autoupdate/helper_test.go b/integration/autoupdate/helper_test.go new file mode 100644 index 000000000000..ada3539186a1 --- /dev/null +++ b/integration/autoupdate/helper_test.go @@ -0,0 +1,89 @@ +/* + * 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 autoupdate_test + +import ( + "net/http" + "sync" +) + +type limitRequest struct { + limit int64 + lock chan struct{} +} + +// limitedResponseWriter wraps http.ResponseWriter and enforces a write limit +// then block the response until signal is received. +type limitedResponseWriter struct { + requests chan limitRequest +} + +// newLimitedResponseWriter creates a new limitedResponseWriter with the lock. +func newLimitedResponseWriter() *limitedResponseWriter { + lw := &limitedResponseWriter{ + requests: make(chan limitRequest, 10), + } + return lw +} + +// Wrap wraps response writer if limit was previously requested, if not, return original one. +func (lw *limitedResponseWriter) Wrap(w http.ResponseWriter) http.ResponseWriter { + select { + case request := <-lw.requests: + return &wrapper{ + ResponseWriter: w, + request: request, + } + default: + return w + } +} + +// SetLimitRequest sends limit request to the pool to wrap next response writer with defined limits. +func (lw *limitedResponseWriter) SetLimitRequest(limit limitRequest) { + lw.requests <- limit +} + +// wrapper wraps the http response writer to control writing operation by blocking it. +type wrapper struct { + http.ResponseWriter + + written int64 + request limitRequest + released bool + + mutex sync.Mutex +} + +// Write writes data to the underlying ResponseWriter but respects the byte limit. +func (lw *wrapper) Write(p []byte) (int, error) { + lw.mutex.Lock() + defer lw.mutex.Unlock() + + if lw.written >= lw.request.limit && !lw.released { + // Send signal that lock is acquired and wait till it was released by response. + lw.request.lock <- struct{}{} + <-lw.request.lock + lw.released = true + } + + n, err := lw.ResponseWriter.Write(p) + lw.written += int64(n) + return n, err +} diff --git a/integration/autoupdate/helper_unix_test.go b/integration/autoupdate/helper_unix_test.go new file mode 100644 index 000000000000..632defbc5f29 --- /dev/null +++ b/integration/autoupdate/helper_unix_test.go @@ -0,0 +1,37 @@ +//go:build !windows + +/* + * 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 autoupdate_test + +import ( + "errors" + "syscall" + + "github.com/gravitational/trace" +) + +// sendInterrupt sends a SIGINT to the process. +func sendInterrupt(pid int) error { + err := syscall.Kill(pid, syscall.SIGINT) + if errors.Is(err, syscall.ESRCH) { + return trace.BadParameter("can't find the process: %v", pid) + } + return trace.Wrap(err) +} diff --git a/integration/autoupdate/helper_windows_test.go b/integration/autoupdate/helper_windows_test.go new file mode 100644 index 000000000000..89d109b0ad26 --- /dev/null +++ b/integration/autoupdate/helper_windows_test.go @@ -0,0 +1,42 @@ +//go:build windows + +/* + * 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 autoupdate_test + +import ( + "syscall" + + "github.com/gravitational/trace" + "golang.org/x/sys/windows" +) + +var ( + kernel = windows.NewLazyDLL("kernel32.dll") + ctrlEvent = kernel.NewProc("GenerateConsoleCtrlEvent") +) + +// sendInterrupt sends a Ctrl-Break event to the process. +func sendInterrupt(pid int) error { + r, _, err := ctrlEvent.Call(uintptr(syscall.CTRL_BREAK_EVENT), uintptr(pid)) + if r == 0 { + return trace.Wrap(err) + } + return nil +} diff --git a/integration/autoupdate/main_test.go b/integration/autoupdate/main_test.go new file mode 100644 index 000000000000..4df033950c45 --- /dev/null +++ b/integration/autoupdate/main_test.go @@ -0,0 +1,189 @@ +/* + * 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 autoupdate_test + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/integration/helpers" +) + +const ( + testBinaryName = "updater" + teleportToolsVersion = "TELEPORT_TOOLS_VERSION" +) + +var ( + // testVersions list of the pre-compiled binaries with encoded versions to check. + testVersions = []string{ + "1.2.3", + "3.2.1", + } + limitedWriter = newLimitedResponseWriter() + + toolsDir string + baseURL string +) + +func TestMain(m *testing.M) { + ctx := context.Background() + tmp, err := os.MkdirTemp(os.TempDir(), testBinaryName) + if err != nil { + log.Fatalf("failed to create temporary directory: %v", err) + } + + toolsDir, err = os.MkdirTemp(os.TempDir(), "tools") + if err != nil { + log.Fatalf("failed to create temporary directory: %v", err) + } + + var srv *http.Server + srv, baseURL = startTestHTTPServer(tmp) + for _, version := range testVersions { + if err := buildAndArchiveApps(ctx, tmp, toolsDir, version, baseURL); err != nil { + log.Fatalf("failed to build testing app binary archive: %v", err) + } + } + + // Run tests after binary is built. + code := m.Run() + + if err := srv.Close(); err != nil { + log.Fatalf("failed to shutdown server: %v", err) + } + if err := os.RemoveAll(tmp); err != nil { + log.Fatalf("failed to remove temporary directory: %v", err) + } + if err := os.RemoveAll(toolsDir); err != nil { + log.Fatalf("failed to remove tools directory: %v", err) + } + + os.Exit(code) +} + +// serve256File calculates sha256 checksum for requested file. +func serve256File(w http.ResponseWriter, _ *http.Request, filePath string) { + log.Printf("Calculating and serving file checksum: %s\n", filePath) + + w.Header().Set("Content-Disposition", "attachment; filename=\""+filepath.Base(filePath)+".sha256\"") + w.Header().Set("Content-Type", "plain/text") + + file, err := os.Open(filePath) + if err != nil { + http.Error(w, "failed to open file", http.StatusInternalServerError) + return + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + http.Error(w, "failed to write to hash", http.StatusInternalServerError) + return + } + if _, err := hex.NewEncoder(w).Write(hash.Sum(nil)); err != nil { + http.Error(w, "failed to write checksum", http.StatusInternalServerError) + } +} + +// startTestHTTPServer starts the file-serving HTTP server for testing. +func startTestHTTPServer(baseDir string) (*http.Server, string) { + srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + filePath := filepath.Join(baseDir, r.URL.Path) + switch { + case strings.HasSuffix(r.URL.Path, ".sha256"): + serve256File(w, r, strings.TrimSuffix(filePath, ".sha256")) + default: + http.ServeFile(limitedWriter.Wrap(w), r, filePath) + } + })} + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + log.Fatalf("failed to create listener: %v", err) + } + + go func() { + if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Printf("failed to start server: %s", err) + } + }() + + return srv, listener.Addr().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 { + versionPath := filepath.Join(path, version) + for _, app := range []string{"tsh", "tctl"} { + output := filepath.Join(versionPath, app) + switch runtime.GOOS { + case "windows": + output = filepath.Join(versionPath, app+".exe") + case "darwin": + output = filepath.Join(versionPath, app+".app", "Contents", "MacOS", app) + } + if err := buildBinary(output, toolsDir, version, baseURL); err != nil { + return trace.Wrap(err) + } + } + switch runtime.GOOS { + case "darwin": + archivePath := filepath.Join(path, fmt.Sprintf("tsh-%s.pkg", version)) + return trace.Wrap(helpers.CompressDirToPkgFile(ctx, versionPath, archivePath, "com.example.pkgtest")) + case "windows": + archivePath := filepath.Join(path, fmt.Sprintf("teleport-v%s-windows-amd64-bin.zip", version)) + return trace.Wrap(helpers.CompressDirToZipFile(ctx, versionPath, archivePath)) + default: + archivePath := filepath.Join(path, fmt.Sprintf("teleport-v%s-linux-%s-bin.tar.gz", version, runtime.GOARCH)) + return trace.Wrap(helpers.CompressDirToTarGzFile(ctx, versionPath, archivePath)) + } +} + +// buildBinary executes command to build binary with updater logic only for testing. +func buildBinary(output string, toolsDir string, version string, baseURL string) error { + cmd := exec.Command( + "go", "build", "-o", output, + "-ldflags", strings.Join([]string{ + fmt.Sprintf("-X 'main.toolsDir=%s'", toolsDir), + fmt.Sprintf("-X 'main.version=%s'", version), + fmt.Sprintf("-X 'main.baseUrl=http://%s'", baseURL), + }, " "), + "./updater", + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return trace.Wrap(cmd.Run()) +} diff --git a/integration/autoupdate/updater/main.go b/integration/autoupdate/updater/main.go new file mode 100644 index 000000000000..63c82adf1d13 --- /dev/null +++ b/integration/autoupdate/updater/main.go @@ -0,0 +1,85 @@ +/* + * 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 main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "runtime" + "time" + + "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/teleport/lib/autoupdate" +) + +var ( + version = "development" + baseUrl = "http://localhost" + toolsDir = "" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + updater := autoupdate.NewClientUpdater( + clientTools(), + toolsDir, + version, + autoupdate.WithBaseURL(baseUrl), + ) + toolsVersion, reExec := updater.CheckLocal() + if reExec { + // Download and update the version of client tools required by the cluster. + // This is required if the user passed in the TELEPORT_TOOLS_VERSION explicitly. + err := updater.UpdateWithLock(ctx, toolsVersion) + if errors.Is(err, autoupdate.ErrCanceled) { + os.Exit(0) + return + } + if err != nil { + log.Fatalf("failed to download version (%v): %v\n", toolsVersion, err) + return + } + + // Re-execute client tools with the correct version of client tools. + code, err := updater.Exec() + if err != nil { + log.Fatalf("Failed to re-exec client tool: %v\n", err) + } else { + os.Exit(code) + } + } + if len(os.Args) > 1 && os.Args[1] == "version" { + fmt.Printf("Teleport v%v git\n", version) + } +} + +// clientTools list of the client tools needs to be updated. +func clientTools() []string { + switch runtime.GOOS { + case constants.WindowsOS: + return []string{"tsh.exe", "tctl.exe"} + default: + return []string{"tsh", "tctl"} + } +} diff --git a/lib/autoupdate/client_update.go b/lib/autoupdate/client_update.go new file mode 100644 index 000000000000..ac089b073e12 --- /dev/null +++ b/lib/autoupdate/client_update.go @@ -0,0 +1,353 @@ +/* + * 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 autoupdate + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "syscall" + "time" + + "github.com/google/uuid" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/client/webclient" + "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/packaging" +) + +const ( + // teleportToolsVersionEnv is environment name for requesting specific version for update. + teleportToolsVersionEnv = "TELEPORT_TOOLS_VERSION" + // baseUrl is CDN URL for downloading official Teleport packages. + baseUrl = "https://cdn.teleport.dev" + // checksumHexLen is length of the hash sum. + checksumHexLen = 64 + // reservedFreeDisk is the predefined amount of free disk space (in bytes) required + // to remain available after downloading archives. + reservedFreeDisk = 10 * 1024 * 1024 + // lockFileName is file used for locking update process in parallel. + lockFileName = ".lock" + // updatePackageSuffix is directory suffix used for package extraction in tools directory. + updatePackageSuffix = "-update-pkg" +) + +var ( + // // pattern is template for response on version command for client tools {tsh, tctl}. + pattern = regexp.MustCompile(`(?m)Teleport v(.*) git`) +) + +// Option applies an option value for the ClientUpdater. +type Option func(u *ClientUpdater) + +// WithBaseURL defines custom base url for the updater. +func WithBaseURL(baseUrl string) Option { + return func(u *ClientUpdater) { + u.baseUrl = baseUrl + } +} + +// WithClient defines custom http client for the ClientUpdater. +func WithClient(client *http.Client) Option { + return func(u *ClientUpdater) { + u.client = client + } +} + +// ClientUpdater is updater implementation for the client tools auto updates. +type ClientUpdater struct { + toolsDir string + localVersion string + tools []string + + baseUrl string + client *http.Client +} + +// NewClientUpdater initiate updater for the client tools auto updates. +func NewClientUpdater(tools []string, toolsDir string, localVersion string, options ...Option) *ClientUpdater { + updater := &ClientUpdater{ + tools: tools, + toolsDir: toolsDir, + localVersion: localVersion, + baseUrl: baseUrl, + client: http.DefaultClient, + } + for _, option := range options { + option(updater) + } + + return updater +} + +// CheckLocal is run at client tool startup and will only perform local checks. +func (u *ClientUpdater) CheckLocal() (string, bool) { + // Check if the user has requested a specific version of client tools. + requestedVersion := os.Getenv(teleportToolsVersionEnv) + switch { + // The user has turned off any form of automatic updates. + case requestedVersion == "off": + return "", false + // Requested version already the same as client version. + case u.localVersion == requestedVersion: + return requestedVersion, false + } + + // If a version of client tools has already been downloaded to + // tools directory, return that. + toolsVersion, err := version(u.toolsDir) + if err != nil { + return "", false + } + // The user has requested a specific version of client tools. + if requestedVersion != "" && requestedVersion != toolsVersion { + return requestedVersion, true + } + + return toolsVersion, false +} + +// CheckRemote will check against Proxy Service if client tools need to be +// updated. +func (u *ClientUpdater) CheckRemote(ctx context.Context, proxyAddr string) (string, bool, error) { + // Check if the user has requested a specific version of client tools. + requestedVersion := os.Getenv(teleportToolsVersionEnv) + switch { + // The user has turned off any form of automatic updates. + case requestedVersion == "off": + return "", false, nil + // Requested version already the same as client version. + case u.localVersion == requestedVersion: + return requestedVersion, false, nil + } + + certPool, err := x509.SystemCertPool() + if err != nil { + return "", false, trace.Wrap(err) + } + resp, err := webclient.Find(&webclient.Config{ + Context: ctx, + ProxyAddr: proxyAddr, + Pool: certPool, + Timeout: 30 * time.Second, + }) + if err != nil { + return "", false, trace.Wrap(err) + } + + // If a version of client tools has already been downloaded to + // tools directory, return that. + toolsVersion, err := version(u.toolsDir) + if err != nil { + return "", false, trace.Wrap(err) + } + + switch { + case requestedVersion != "" && requestedVersion != toolsVersion: + return requestedVersion, true, nil + case !resp.AutoUpdate.ToolsAutoUpdate || resp.AutoUpdate.ToolsVersion == "": + return "", false, nil + case u.localVersion == resp.AutoUpdate.ToolsVersion: + return resp.AutoUpdate.ToolsVersion, false, nil + case resp.AutoUpdate.ToolsVersion != toolsVersion: + return resp.AutoUpdate.ToolsVersion, true, nil + } + + return toolsVersion, false, nil +} + +// UpdateWithLock acquires filesystem lock, downloads requested version package, unarchive and replace existing one. +func (u *ClientUpdater) UpdateWithLock(ctx context.Context, toolsVersion string) (err error) { + // Create tools directory if it does not exist. + if err := os.MkdirAll(u.toolsDir, 0o755); err != nil { + return trace.Wrap(err) + } + // Lock concurrent {tsh, tctl} execution util requested version is updated. + unlock, err := utils.FSWriteLock(filepath.Join(u.toolsDir, lockFileName)) + if err != nil { + return trace.Wrap(err) + } + defer func() { + err = trace.NewAggregate(err, unlock()) + }() + + // If the version of the running binary or the version downloaded to + // tools directory is the same as the requested version of client tools, + // nothing to be done, exit early. + teleportVersion, err := version(u.toolsDir) + if err != nil && !trace.IsNotFound(err) { + return trace.Wrap(err) + + } + if toolsVersion == u.localVersion || toolsVersion == teleportVersion { + return nil + } + + // Download and update {tsh, tctl} in tools directory. + if err := u.Update(ctx, toolsVersion); err != nil { + return trace.Wrap(err) + } + + return +} + +// Update downloads requested version and replace it with existing one and cleanups the previous downloads +// with defined updater directory suffix. +func (u *ClientUpdater) Update(ctx context.Context, toolsVersion string) error { + // Get platform specific download URLs. + archiveURL, hashURL, err := urls(u.baseUrl, toolsVersion) + if err != nil { + return trace.Wrap(err) + } + + // Download the archive and validate against the hash. Download to a + // temporary path within tools directory. + hash, err := u.downloadHash(ctx, hashURL) + if err != nil { + return trace.Wrap(err) + } + archivePath, err := u.downloadArchive(ctx, u.toolsDir, archiveURL, hash) + if err != nil { + return trace.Wrap(err) + } + defer os.Remove(archivePath) + + pkgName := fmt.Sprint(uuid.New().String(), updatePackageSuffix) + extractDir := filepath.Join(u.toolsDir, pkgName) + if runtime.GOOS != constants.DarwinOS { + if err := os.Mkdir(extractDir, 0o755); err != nil { + return trace.Wrap(err) + } + } + + // Perform atomic replace so concurrent exec do not fail. + if err := packaging.ReplaceToolsBinaries(u.toolsDir, archivePath, extractDir, u.tools); err != nil { + return trace.Wrap(err) + } + // Cleanup the tools directory with previously downloaded and un-archived versions. + if err := packaging.RemoveWithSuffix(u.toolsDir, updatePackageSuffix, pkgName); err != nil { + return trace.Wrap(err) + } + + return nil +} + +// Exec re-executes tool command with same arguments and environ variables. +func (u *ClientUpdater) Exec() (int, error) { + path, err := toolName(u.toolsDir) + if err != nil { + return 0, trace.Wrap(err) + } + + cmd := exec.Command(path, os.Args[1:]...) + // To prevent re-execution loop we have to disable update logic for re-execution. + cmd.Env = append(os.Environ(), teleportToolsVersionEnv+"=off") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return 0, trace.Wrap(err) + } + + return cmd.ProcessState.ExitCode(), nil +} + +func (u *ClientUpdater) downloadHash(ctx context.Context, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", trace.Wrap(err) + } + resp, err := u.client.Do(req) + if err != nil { + return "", trace.Wrap(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", trace.BadParameter("bad status when downloading archive hash: %v", resp.StatusCode) + } + + var buf bytes.Buffer + _, err = io.CopyN(&buf, resp.Body, checksumHexLen) + if err != nil { + return "", trace.Wrap(err) + } + raw := buf.String() + if _, err = hex.DecodeString(raw); err != nil { + return "", trace.Wrap(err) + } + return raw, nil +} + +func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, url string, hash string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", trace.Wrap(err) + } + resp, err := u.client.Do(req) + if err != nil { + return "", trace.Wrap(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", trace.BadParameter("bad status when downloading archive: %v", resp.StatusCode) + } + + if resp.ContentLength != -1 { + if err := checkFreeSpace(u.toolsDir, uint64(resp.ContentLength)); err != nil { + return "", trace.Wrap(err) + } + } + + // Caller of this function will remove this file after the atomic swap has + // occurred. + f, err := os.CreateTemp(downloadDir, "tmp-") + if err != nil { + return "", trace.Wrap(err) + } + + h := sha256.New() + pw := &progressWriter{n: 0, limit: resp.ContentLength} + body := cancelableTeeReader(io.TeeReader(resp.Body, h), pw, syscall.SIGINT, syscall.SIGTERM) + + // It is a little inefficient to download the file to disk and then re-load + // it into memory to unarchive later, but this is safer as it allows {tsh, + // tctl} to validate the hash before trying to operate on the archive. + _, err = io.Copy(f, body) + if err != nil { + return "", trace.Wrap(err) + } + if fmt.Sprintf("%x", h.Sum(nil)) != hash { + return "", trace.BadParameter("hash of archive does not match downloaded archive") + } + + return f.Name(), nil +} diff --git a/lib/autoupdate/feature_ent.go b/lib/autoupdate/feature_ent.go new file mode 100644 index 000000000000..804f4977a037 --- /dev/null +++ b/lib/autoupdate/feature_ent.go @@ -0,0 +1,26 @@ +//go:build webassets_ent +// +build webassets_ent + +/* + * 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 autoupdate + +func init() { + featureFlag |= FlagEnt +} diff --git a/lib/autoupdate/feature_fips.go b/lib/autoupdate/feature_fips.go new file mode 100644 index 000000000000..d11e7e36a0de --- /dev/null +++ b/lib/autoupdate/feature_fips.go @@ -0,0 +1,26 @@ +//go:build fips +// +build fips + +/* + * 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 autoupdate + +func init() { + featureFlag |= FlagFips +} diff --git a/lib/autoupdate/progress.go b/lib/autoupdate/progress.go new file mode 100644 index 000000000000..d455c2bfee48 --- /dev/null +++ b/lib/autoupdate/progress.go @@ -0,0 +1,81 @@ +/* + * 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 autoupdate + +import ( + "fmt" + "io" + "os" + "os/signal" + "strings" +) + +var ( + // ErrCanceled represent the cancellation error for Ctrl-Break/Ctrl-C, depends on the platform. + ErrCanceled = fmt.Errorf("canceled") +) + +// cancelableTeeReader is a copy of TeeReader with ability to react on signal notifier +// to cancel reading process. +func cancelableTeeReader(r io.Reader, w io.Writer, signals ...os.Signal) io.Reader { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, signals...) + + return &teeReader{r, w, sigs} +} + +type teeReader struct { + r io.Reader + w io.Writer + sigs chan os.Signal +} + +func (t *teeReader) Read(p []byte) (n int, err error) { + select { + case <-t.sigs: + return 0, ErrCanceled + default: + n, err = t.r.Read(p) + if n > 0 { + if n, err := t.w.Write(p[:n]); err != nil { + return n, err + } + } + } + return +} + +type progressWriter struct { + n int64 + limit int64 +} + +func (w *progressWriter) Write(p []byte) (int, error) { + w.n = w.n + int64(len(p)) + + n := int((w.n*100)/w.limit) / 10 + bricks := strings.Repeat("▒", n) + strings.Repeat(" ", 10-n) + fmt.Print("\rUpdate progress: [" + bricks + "] (Ctrl-C to cancel update)") + + if w.n == w.limit { + fmt.Print("\n") + } + + return len(p), nil +} diff --git a/lib/autoupdate/utils.go b/lib/autoupdate/utils.go new file mode 100644 index 000000000000..d326888f14f4 --- /dev/null +++ b/lib/autoupdate/utils.go @@ -0,0 +1,168 @@ +/* + * 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 autoupdate + +import ( + "bufio" + "bytes" + "context" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/coreos/go-semver/semver" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/utils" +) + +const ( + // FlagEnt represents enterprise version. + FlagEnt = 1 << 0 + // FlagFips represents enterprise version with fips feature enabled. + FlagFips = 1 << 1 +) + +var ( + // featureFlag stores information about enable + featureFlag int +) + +// ToolsDir returns the path to {tsh, tctl} in $TELEPORT_HOME/bin. +func ToolsDir() (string, error) { + home := os.Getenv(types.HomeEnvVar) + if home == "" { + var err error + home, err = os.UserHomeDir() + if err != nil { + return "", trace.Wrap(err) + } + } + + return filepath.Join(filepath.Clean(home), ".tsh", "bin"), nil +} + +func version(toolsDir string) (string, error) { + // Find the path to the current executable. + path, err := toolName(toolsDir) + if err != nil { + return "", trace.Wrap(err) + } + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + return "", nil + } else if err != nil { + return "", trace.Wrap(err) + } + + // Set a timeout to not let "{tsh, tctl} version" block forever. Allow up + // to 10 seconds because sometimes MDM tools like Jamf cause a lot of + // latency in launching binaries. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Execute "{tsh, tctl} version" and pass in TELEPORT_TOOLS_VERSION=off to + // turn off all automatic updates code paths to prevent any recursion. + command := exec.CommandContext(ctx, path, "version") + command.Env = []string{teleportToolsVersionEnv + "=off"} + output, err := command.Output() + if err != nil { + return "", trace.Wrap(err) + } + + // The output for "{tsh, tctl} version" can be multiple lines. Find the + // actual version line and extract the version. + scanner := bufio.NewScanner(bytes.NewReader(output)) + for scanner.Scan() { + line := scanner.Text() + + if !strings.HasPrefix(line, "Teleport") { + continue + } + + matches := pattern.FindStringSubmatch(line) + if len(matches) != 2 { + return "", trace.BadParameter("invalid version line: %v", line) + } + version, err := semver.NewVersion(matches[1]) + if err != nil { + return "", trace.Wrap(err) + } + return version.String(), nil + } + + return "", trace.BadParameter("unable to determine version") +} + +// urls 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 urls(baseUrl, toolsVersion string) (string, string, error) { + var archive string + switch runtime.GOOS { + case "darwin": + archive = baseUrl + "/tsh-" + toolsVersion + ".pkg" + case "windows": + archive = baseUrl + "/teleport-v" + toolsVersion + "-windows-amd64-bin.zip" + case "linux": + var b strings.Builder + b.WriteString(baseUrl + "/teleport-") + if featureFlag&(FlagEnt|FlagFips) != 0 { + b.WriteString("ent-") + } + b.WriteString("v" + toolsVersion + "-" + runtime.GOOS + "-" + runtime.GOARCH + "-") + if featureFlag&FlagFips != 0 { + b.WriteString("fips-") + } + b.WriteString("bin.tar.gz") + archive = b.String() + default: + return "", "", trace.BadParameter("unsupported runtime: %v", runtime.GOOS) + } + + return archive, archive + ".sha256", nil +} + +// toolName returns the path to {tsh, tctl} for the executable that started +// the current process. +func toolName(toolsDir string) (string, error) { + executablePath, err := os.Executable() + if err != nil { + return "", trace.Wrap(err) + } + + return filepath.Join(toolsDir, filepath.Base(executablePath)), nil +} + +// checkFreeSpace verifies that we have enough requested space at specific directory. +func checkFreeSpace(path string, requested uint64) error { + free, err := utils.FreeDiskWithReserve(path, reservedFreeDisk) + if err != nil { + return trace.Errorf("failed to calculate free disk in %q: %v", path, err) + } + // Bail if there's not enough free disk space at the target. + if requested > free { + return trace.Errorf("%q needs %d additional bytes of disk space", path, requested-free) + } + + return nil +} diff --git a/lib/utils/disk.go b/lib/utils/disk.go index 9e2419527051..782c598d2622 100644 --- a/lib/utils/disk.go +++ b/lib/utils/disk.go @@ -53,10 +53,8 @@ func FreeDiskWithReserve(dir string, reservedFreeDisk uint64) (uint64, error) { if err != nil { return 0, trace.Wrap(err) } - if stat.Bsize < 0 { - return 0, trace.Errorf("invalid size") - } - avail := stat.Bavail * uint64(stat.Bsize) + //nolint:unconvert // The cast is only necessary for linux platform. + avail := uint64(stat.Bavail) * uint64(stat.Bsize) if reservedFreeDisk > avail { return 0, trace.Errorf("no free space left") } diff --git a/lib/utils/packaging/unarchive_unix.go b/lib/utils/packaging/unarchive_unix.go index 3be7d0c473ef..ea51afdbbc7f 100644 --- a/lib/utils/packaging/unarchive_unix.go +++ b/lib/utils/packaging/unarchive_unix.go @@ -33,6 +33,8 @@ import ( "github.com/google/renameio/v2" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/constants" ) // ReplaceToolsBinaries extracts executables specified by execNames from archivePath into @@ -43,7 +45,7 @@ import ( // For other POSIX, archivePath must be a gzipped tarball. func ReplaceToolsBinaries(toolsDir string, archivePath string, extractDir string, execNames []string) error { switch runtime.GOOS { - case "darwin": + case constants.DarwinOS: return replacePkg(toolsDir, archivePath, extractDir, execNames) default: return replaceTarGz(toolsDir, archivePath, extractDir, execNames) From a3fa2c3986129cc44d3dc7c722431ec3a7d96a1d Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Tue, 15 Oct 2024 17:35:00 -0400 Subject: [PATCH 02/14] Replace fork for posix platform for re-exec Move integration tests to client tools specific dir Use context cancellation with SIGTERM, SIGINT Remove cancelable tee reader with context replacement Renaming --- .../{ => tools}/client_update_test.go | 2 +- .../autoupdate/{ => tools}/helper_test.go | 2 +- .../{ => tools}/helper_unix_test.go | 2 +- .../{ => tools}/helper_windows_test.go | 2 +- .../autoupdate/{ => tools}/main_test.go | 2 +- .../autoupdate/{ => tools}/updater/main.go | 2 +- lib/autoupdate/client_update.go | 69 ++++++++++++------- lib/autoupdate/progress.go | 38 ---------- lib/autoupdate/utils.go | 2 +- 9 files changed, 50 insertions(+), 71 deletions(-) rename integration/autoupdate/{ => tools}/client_update_test.go (99%) rename integration/autoupdate/{ => tools}/helper_test.go (99%) rename integration/autoupdate/{ => tools}/helper_unix_test.go (97%) rename integration/autoupdate/{ => tools}/helper_windows_test.go (98%) rename integration/autoupdate/{ => tools}/main_test.go (99%) rename integration/autoupdate/{ => tools}/updater/main.go (97%) diff --git a/integration/autoupdate/client_update_test.go b/integration/autoupdate/tools/client_update_test.go similarity index 99% rename from integration/autoupdate/client_update_test.go rename to integration/autoupdate/tools/client_update_test.go index 0fb76bef5c3d..d30516705252 100644 --- a/integration/autoupdate/client_update_test.go +++ b/integration/autoupdate/tools/client_update_test.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package autoupdate_test +package tools_test import ( "bytes" diff --git a/integration/autoupdate/helper_test.go b/integration/autoupdate/tools/helper_test.go similarity index 99% rename from integration/autoupdate/helper_test.go rename to integration/autoupdate/tools/helper_test.go index ada3539186a1..a3c37a9e94b5 100644 --- a/integration/autoupdate/helper_test.go +++ b/integration/autoupdate/tools/helper_test.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package autoupdate_test +package tools_test import ( "net/http" diff --git a/integration/autoupdate/helper_unix_test.go b/integration/autoupdate/tools/helper_unix_test.go similarity index 97% rename from integration/autoupdate/helper_unix_test.go rename to integration/autoupdate/tools/helper_unix_test.go index 632defbc5f29..61ba0766b90d 100644 --- a/integration/autoupdate/helper_unix_test.go +++ b/integration/autoupdate/tools/helper_unix_test.go @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package autoupdate_test +package tools_test import ( "errors" diff --git a/integration/autoupdate/helper_windows_test.go b/integration/autoupdate/tools/helper_windows_test.go similarity index 98% rename from integration/autoupdate/helper_windows_test.go rename to integration/autoupdate/tools/helper_windows_test.go index 89d109b0ad26..b2ede9ade8c1 100644 --- a/integration/autoupdate/helper_windows_test.go +++ b/integration/autoupdate/tools/helper_windows_test.go @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package autoupdate_test +package tools_test import ( "syscall" diff --git a/integration/autoupdate/main_test.go b/integration/autoupdate/tools/main_test.go similarity index 99% rename from integration/autoupdate/main_test.go rename to integration/autoupdate/tools/main_test.go index 4df033950c45..06f36894217d 100644 --- a/integration/autoupdate/main_test.go +++ b/integration/autoupdate/tools/main_test.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package autoupdate_test +package tools_test import ( "context" diff --git a/integration/autoupdate/updater/main.go b/integration/autoupdate/tools/updater/main.go similarity index 97% rename from integration/autoupdate/updater/main.go rename to integration/autoupdate/tools/updater/main.go index 63c82adf1d13..ba6663f8a816 100644 --- a/integration/autoupdate/updater/main.go +++ b/integration/autoupdate/tools/updater/main.go @@ -52,7 +52,7 @@ func main() { // Download and update the version of client tools required by the cluster. // This is required if the user passed in the TELEPORT_TOOLS_VERSION explicitly. err := updater.UpdateWithLock(ctx, toolsVersion) - if errors.Is(err, autoupdate.ErrCanceled) { + if errors.Is(err, context.Canceled) { os.Exit(0) return } diff --git a/lib/autoupdate/client_update.go b/lib/autoupdate/client_update.go index ac089b073e12..534ad4aeadf4 100644 --- a/lib/autoupdate/client_update.go +++ b/lib/autoupdate/client_update.go @@ -26,9 +26,11 @@ import ( "encoding/hex" "fmt" "io" + "log/slog" "net/http" "os" "os/exec" + "os/signal" "path/filepath" "regexp" "runtime" @@ -47,13 +49,13 @@ import ( const ( // teleportToolsVersionEnv is environment name for requesting specific version for update. teleportToolsVersionEnv = "TELEPORT_TOOLS_VERSION" - // baseUrl is CDN URL for downloading official Teleport packages. - baseUrl = "https://cdn.teleport.dev" + // baseURL is CDN URL for downloading official Teleport packages. + baseURL = "https://cdn.teleport.dev" // checksumHexLen is length of the hash sum. checksumHexLen = 64 // reservedFreeDisk is the predefined amount of free disk space (in bytes) required // to remain available after downloading archives. - reservedFreeDisk = 10 * 1024 * 1024 + reservedFreeDisk = 10 * 1024 * 1024 // 10 Mb // lockFileName is file used for locking update process in parallel. lockFileName = ".lock" // updatePackageSuffix is directory suffix used for package extraction in tools directory. @@ -65,18 +67,18 @@ var ( pattern = regexp.MustCompile(`(?m)Teleport v(.*) git`) ) -// Option applies an option value for the ClientUpdater. -type Option func(u *ClientUpdater) +// ClientOption applies an option value for the ClientUpdater. +type ClientOption func(u *ClientUpdater) // WithBaseURL defines custom base url for the updater. -func WithBaseURL(baseUrl string) Option { +func WithBaseURL(baseUrl string) ClientOption { return func(u *ClientUpdater) { u.baseUrl = baseUrl } } // WithClient defines custom http client for the ClientUpdater. -func WithClient(client *http.Client) Option { +func WithClient(client *http.Client) ClientOption { return func(u *ClientUpdater) { u.client = client } @@ -93,12 +95,12 @@ type ClientUpdater struct { } // NewClientUpdater initiate updater for the client tools auto updates. -func NewClientUpdater(tools []string, toolsDir string, localVersion string, options ...Option) *ClientUpdater { +func NewClientUpdater(tools []string, toolsDir string, localVersion string, options ...ClientOption) *ClientUpdater { updater := &ClientUpdater{ tools: tools, toolsDir: toolsDir, localVersion: localVersion, - baseUrl: baseUrl, + baseUrl: baseURL, client: http.DefaultClient, } for _, option := range options { @@ -123,7 +125,7 @@ func (u *ClientUpdater) CheckLocal() (string, bool) { // If a version of client tools has already been downloaded to // tools directory, return that. - toolsVersion, err := version(u.toolsDir) + toolsVersion, err := checkClientToolVersion(u.toolsDir) if err != nil { return "", false } @@ -165,7 +167,7 @@ func (u *ClientUpdater) CheckRemote(ctx context.Context, proxyAddr string) (stri // If a version of client tools has already been downloaded to // tools directory, return that. - toolsVersion, err := version(u.toolsDir) + toolsVersion, err := checkClientToolVersion(u.toolsDir) if err != nil { return "", false, trace.Wrap(err) } @@ -190,7 +192,7 @@ func (u *ClientUpdater) UpdateWithLock(ctx context.Context, toolsVersion string) if err := os.MkdirAll(u.toolsDir, 0o755); err != nil { return trace.Wrap(err) } - // Lock concurrent {tsh, tctl} execution util requested version is updated. + // Lock concurrent client tools execution util requested version is updated. unlock, err := utils.FSWriteLock(filepath.Join(u.toolsDir, lockFileName)) if err != nil { return trace.Wrap(err) @@ -202,7 +204,7 @@ func (u *ClientUpdater) UpdateWithLock(ctx context.Context, toolsVersion string) // If the version of the running binary or the version downloaded to // tools directory is the same as the requested version of client tools, // nothing to be done, exit early. - teleportVersion, err := version(u.toolsDir) + teleportVersion, err := checkClientToolVersion(u.toolsDir) if err != nil && !trace.IsNotFound(err) { return trace.Wrap(err) @@ -211,7 +213,7 @@ func (u *ClientUpdater) UpdateWithLock(ctx context.Context, toolsVersion string) return nil } - // Download and update {tsh, tctl} in tools directory. + // Download and update client tools in tools directory. if err := u.Update(ctx, toolsVersion); err != nil { return trace.Wrap(err) } @@ -228,17 +230,24 @@ func (u *ClientUpdater) Update(ctx context.Context, toolsVersion string) error { return trace.Wrap(err) } + signalCtx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + // Download the archive and validate against the hash. Download to a // temporary path within tools directory. - hash, err := u.downloadHash(ctx, hashURL) + hash, err := u.downloadHash(signalCtx, hashURL) if err != nil { return trace.Wrap(err) } - archivePath, err := u.downloadArchive(ctx, u.toolsDir, archiveURL, hash) + archivePath, err := u.downloadArchive(signalCtx, u.toolsDir, archiveURL, hash) if err != nil { return trace.Wrap(err) } - defer os.Remove(archivePath) + defer func() { + if err := os.Remove(archivePath); err != nil { + slog.WarnContext(ctx, "failed to remove archive", "error", err) + } + }() pkgName := fmt.Sprint(uuid.New().String(), updatePackageSuffix) extractDir := filepath.Join(u.toolsDir, pkgName) @@ -266,19 +275,27 @@ func (u *ClientUpdater) Exec() (int, error) { if err != nil { return 0, trace.Wrap(err) } - - cmd := exec.Command(path, os.Args[1:]...) // To prevent re-execution loop we have to disable update logic for re-execution. - cmd.Env = append(os.Environ(), teleportToolsVersionEnv+"=off") - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + env := append(os.Environ(), teleportToolsVersionEnv+"=off") + + if runtime.GOOS == constants.WindowsOS { + cmd := exec.Command(path, os.Args[1:]...) + cmd.Env = env + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return 0, trace.Wrap(err) + } + + return cmd.ProcessState.ExitCode(), nil + } - if err := cmd.Run(); err != nil { + if err := syscall.Exec(path, os.Args, env); err != nil { return 0, trace.Wrap(err) } - return cmd.ProcessState.ExitCode(), nil + return 0, nil } func (u *ClientUpdater) downloadHash(ctx context.Context, url string) (string, error) { @@ -336,7 +353,7 @@ func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, h := sha256.New() pw := &progressWriter{n: 0, limit: resp.ContentLength} - body := cancelableTeeReader(io.TeeReader(resp.Body, h), pw, syscall.SIGINT, syscall.SIGTERM) + body := io.TeeReader(io.TeeReader(resp.Body, h), pw) // It is a little inefficient to download the file to disk and then re-load // it into memory to unarchive later, but this is safer as it allows {tsh, diff --git a/lib/autoupdate/progress.go b/lib/autoupdate/progress.go index d455c2bfee48..60d94cb65e88 100644 --- a/lib/autoupdate/progress.go +++ b/lib/autoupdate/progress.go @@ -20,47 +20,9 @@ package autoupdate import ( "fmt" - "io" - "os" - "os/signal" "strings" ) -var ( - // ErrCanceled represent the cancellation error for Ctrl-Break/Ctrl-C, depends on the platform. - ErrCanceled = fmt.Errorf("canceled") -) - -// cancelableTeeReader is a copy of TeeReader with ability to react on signal notifier -// to cancel reading process. -func cancelableTeeReader(r io.Reader, w io.Writer, signals ...os.Signal) io.Reader { - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, signals...) - - return &teeReader{r, w, sigs} -} - -type teeReader struct { - r io.Reader - w io.Writer - sigs chan os.Signal -} - -func (t *teeReader) Read(p []byte) (n int, err error) { - select { - case <-t.sigs: - return 0, ErrCanceled - default: - n, err = t.r.Read(p) - if n > 0 { - if n, err := t.w.Write(p[:n]); err != nil { - return n, err - } - } - } - return -} - type progressWriter struct { n int64 limit int64 diff --git a/lib/autoupdate/utils.go b/lib/autoupdate/utils.go index d326888f14f4..04733018e749 100644 --- a/lib/autoupdate/utils.go +++ b/lib/autoupdate/utils.go @@ -63,7 +63,7 @@ func ToolsDir() (string, error) { return filepath.Join(filepath.Clean(home), ".tsh", "bin"), nil } -func version(toolsDir string) (string, error) { +func checkClientToolVersion(toolsDir string) (string, error) { // Find the path to the current executable. path, err := toolName(toolsDir) if err != nil { From 1eeb647070bcfcd1f89515bd1a43b6e6f8759859 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Tue, 15 Oct 2024 19:54:17 -0400 Subject: [PATCH 03/14] Fix syscall path execution Fix archive cleanup if hash is not valid Limit the archive write bytes --- lib/autoupdate/client_update.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/autoupdate/client_update.go b/lib/autoupdate/client_update.go index 534ad4aeadf4..a82972f571ee 100644 --- a/lib/autoupdate/client_update.go +++ b/lib/autoupdate/client_update.go @@ -239,7 +239,7 @@ func (u *ClientUpdater) Update(ctx context.Context, toolsVersion string) error { if err != nil { return trace.Wrap(err) } - archivePath, err := u.downloadArchive(signalCtx, u.toolsDir, archiveURL, hash) + archivePath, archiveHash, err := u.downloadArchive(signalCtx, u.toolsDir, archiveURL) if err != nil { return trace.Wrap(err) } @@ -248,6 +248,9 @@ func (u *ClientUpdater) Update(ctx context.Context, toolsVersion string) error { slog.WarnContext(ctx, "failed to remove archive", "error", err) } }() + if archiveHash != hash { + return trace.BadParameter("hash of archive does not match downloaded archive") + } pkgName := fmt.Sprint(uuid.New().String(), updatePackageSuffix) extractDir := filepath.Join(u.toolsDir, pkgName) @@ -291,7 +294,7 @@ func (u *ClientUpdater) Exec() (int, error) { return cmd.ProcessState.ExitCode(), nil } - if err := syscall.Exec(path, os.Args, env); err != nil { + if err := syscall.Exec(path, append([]string{path}, os.Args[1:]...), env); err != nil { return 0, trace.Wrap(err) } @@ -324,23 +327,23 @@ func (u *ClientUpdater) downloadHash(ctx context.Context, url string) (string, e return raw, nil } -func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, url string, hash string) (string, error) { +func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, url string) (string, string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return "", trace.Wrap(err) + return "", "", trace.Wrap(err) } resp, err := u.client.Do(req) if err != nil { - return "", trace.Wrap(err) + return "", "", trace.Wrap(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", trace.BadParameter("bad status when downloading archive: %v", resp.StatusCode) + return "", "", trace.BadParameter("bad status when downloading archive: %v", resp.StatusCode) } if resp.ContentLength != -1 { if err := checkFreeSpace(u.toolsDir, uint64(resp.ContentLength)); err != nil { - return "", trace.Wrap(err) + return "", "", trace.Wrap(err) } } @@ -348,7 +351,7 @@ func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, // occurred. f, err := os.CreateTemp(downloadDir, "tmp-") if err != nil { - return "", trace.Wrap(err) + return "", "", trace.Wrap(err) } h := sha256.New() @@ -356,15 +359,12 @@ func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, body := io.TeeReader(io.TeeReader(resp.Body, h), pw) // It is a little inefficient to download the file to disk and then re-load - // it into memory to unarchive later, but this is safer as it allows {tsh, - // tctl} to validate the hash before trying to operate on the archive. - _, err = io.Copy(f, body) + // it into memory to unarchive later, but this is safer as it allows client + // tools to validate the hash before trying to operate on the archive. + _, err = io.CopyN(f, body, resp.ContentLength) if err != nil { - return "", trace.Wrap(err) - } - if fmt.Sprintf("%x", h.Sum(nil)) != hash { - return "", trace.BadParameter("hash of archive does not match downloaded archive") + return "", "", trace.Wrap(err) } - return f.Name(), nil + return f.Name(), fmt.Sprintf("%x", h.Sum(nil)), nil } From d5dd7706886a6730ee4de475f8ee2522e6789e58 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Tue, 15 Oct 2024 20:50:28 -0400 Subject: [PATCH 04/14] Cover the case with single package for darwin platform after v17 --- integration/autoupdate/tools/main_test.go | 6 +++- lib/autoupdate/client_update.go | 32 +++++++++++++++++---- lib/autoupdate/utils.go | 35 +++++++++++++++++------ 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/integration/autoupdate/tools/main_test.go b/integration/autoupdate/tools/main_test.go index 06f36894217d..a9f60a7737d8 100644 --- a/integration/autoupdate/tools/main_test.go +++ b/integration/autoupdate/tools/main_test.go @@ -101,6 +101,10 @@ func serve256File(w http.ResponseWriter, _ *http.Request, filePath string) { w.Header().Set("Content-Type", "plain/text") file, err := os.Open(filePath) + if errors.Is(err, os.ErrNotExist) { + http.Error(w, "file not found", http.StatusNotFound) + return + } if err != nil { http.Error(w, "failed to open file", http.StatusInternalServerError) return @@ -160,7 +164,7 @@ func buildAndArchiveApps(ctx context.Context, path string, toolsDir string, vers } switch runtime.GOOS { case "darwin": - archivePath := filepath.Join(path, fmt.Sprintf("tsh-%s.pkg", version)) + archivePath := filepath.Join(path, fmt.Sprintf("teleport-%s.pkg", version)) return trace.Wrap(helpers.CompressDirToPkgFile(ctx, versionPath, archivePath, "com.example.pkgtest")) case "windows": archivePath := filepath.Join(path, fmt.Sprintf("teleport-v%s-windows-amd64-bin.zip", version)) diff --git a/lib/autoupdate/client_update.go b/lib/autoupdate/client_update.go index a82972f571ee..90ee0318b823 100644 --- a/lib/autoupdate/client_update.go +++ b/lib/autoupdate/client_update.go @@ -225,7 +225,7 @@ func (u *ClientUpdater) UpdateWithLock(ctx context.Context, toolsVersion string) // with defined updater directory suffix. func (u *ClientUpdater) Update(ctx context.Context, toolsVersion string) error { // Get platform specific download URLs. - archiveURL, hashURL, err := urls(u.baseUrl, toolsVersion) + packages, err := teleportPackageURLs(u.baseUrl, toolsVersion) if err != nil { return trace.Wrap(err) } @@ -233,13 +233,29 @@ func (u *ClientUpdater) Update(ctx context.Context, toolsVersion string) error { signalCtx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer cancel() - // Download the archive and validate against the hash. Download to a - // temporary path within tools directory. - hash, err := u.downloadHash(signalCtx, hashURL) + for _, pkg := range packages { + if err := u.update(signalCtx, pkg); err != nil { + return trace.Wrap(err) + } + } + + return nil +} + +// update downloads the archive and validate against the hash. Download to a +// temporary path within tools directory. +func (u *ClientUpdater) update(ctx context.Context, pkg packageURL) error { + hash, err := u.downloadHash(ctx, pkg.Hash) + if pkg.Optional && trace.IsNotFound(err) { + return nil + } if err != nil { return trace.Wrap(err) } - archivePath, archiveHash, err := u.downloadArchive(signalCtx, u.toolsDir, archiveURL) + archivePath, archiveHash, err := u.downloadArchive(ctx, u.toolsDir, pkg.Archive) + if pkg.Optional && trace.IsNotFound(err) { + return nil + } if err != nil { return trace.Wrap(err) } @@ -311,6 +327,9 @@ func (u *ClientUpdater) downloadHash(ctx context.Context, url string) (string, e return "", trace.Wrap(err) } defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return "", trace.NotFound("hash file is not found: %v", resp.StatusCode) + } if resp.StatusCode != http.StatusOK { return "", trace.BadParameter("bad status when downloading archive hash: %v", resp.StatusCode) } @@ -337,6 +356,9 @@ func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, return "", "", trace.Wrap(err) } defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return "", "", trace.NotFound("archive file is not found: %v", resp.StatusCode) + } if resp.StatusCode != http.StatusOK { return "", "", trace.BadParameter("bad status when downloading archive: %v", resp.StatusCode) } diff --git a/lib/autoupdate/utils.go b/lib/autoupdate/utils.go index 04733018e749..db7bdb21e3c7 100644 --- a/lib/autoupdate/utils.go +++ b/lib/autoupdate/utils.go @@ -114,15 +114,31 @@ func checkClientToolVersion(toolsDir string) (string, error) { return "", trace.BadParameter("unable to determine version") } -// urls returns the URL for the Teleport archive to download. The format is: +// packageURL defines URLs to the archive and their archive sha256 hash file, and marks +// if this package is optional, for such case download needs to be ignored if package +// not found in CDN. +type packageURL struct { + Archive string + Hash string + 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 urls(baseUrl, toolsVersion string) (string, string, error) { - var archive string +func teleportPackageURLs(baseUrl, toolsVersion string) ([]packageURL, error) { switch runtime.GOOS { case "darwin": - archive = baseUrl + "/tsh-" + toolsVersion + ".pkg" + 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" + archive := baseUrl + "/teleport-v" + toolsVersion + "-windows-amd64-bin.zip" + return []packageURL{ + {Archive: archive, Hash: archive + ".sha256"}, + }, nil case "linux": var b strings.Builder b.WriteString(baseUrl + "/teleport-") @@ -134,12 +150,13 @@ func urls(baseUrl, toolsVersion string) (string, string, error) { b.WriteString("fips-") } b.WriteString("bin.tar.gz") - archive = b.String() + archive := b.String() + return []packageURL{ + {Archive: archive, Hash: archive + ".sha256"}, + }, nil default: - return "", "", trace.BadParameter("unsupported runtime: %v", runtime.GOOS) + return nil, trace.BadParameter("unsupported runtime: %v", runtime.GOOS) } - - return archive, archive + ".sha256", nil } // toolName returns the path to {tsh, tctl} for the executable that started From 5f5688eadb6dc2200f6b463ff0ff1e84c031bd43 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Wed, 16 Oct 2024 15:38:15 -0400 Subject: [PATCH 05/14] Move updater logic to tools package --- integration/autoupdate/tools/updater/main.go | 6 +-- ...{client_update_test.go => updater_test.go} | 14 ++--- lib/autoupdate/{ => tools}/feature_ent.go | 2 +- lib/autoupdate/{ => tools}/feature_fips.go | 2 +- lib/autoupdate/{ => tools}/progress.go | 2 +- .../{client_update.go => tools/updater.go} | 54 +++++++++---------- lib/autoupdate/{ => tools}/utils.go | 8 +-- 7 files changed, 43 insertions(+), 45 deletions(-) rename integration/autoupdate/tools/{client_update_test.go => updater_test.go} (94%) rename lib/autoupdate/{ => tools}/feature_ent.go (97%) rename lib/autoupdate/{ => tools}/feature_fips.go (97%) rename lib/autoupdate/{ => tools}/progress.go (98%) rename lib/autoupdate/{client_update.go => tools/updater.go} (86%) rename lib/autoupdate/{ => tools}/utils.go (96%) diff --git a/integration/autoupdate/tools/updater/main.go b/integration/autoupdate/tools/updater/main.go index ba6663f8a816..3d11d5ffeff4 100644 --- a/integration/autoupdate/tools/updater/main.go +++ b/integration/autoupdate/tools/updater/main.go @@ -28,7 +28,7 @@ import ( "time" "github.com/gravitational/teleport/api/constants" - "github.com/gravitational/teleport/lib/autoupdate" + "github.com/gravitational/teleport/lib/autoupdate/tools" ) var ( @@ -41,11 +41,11 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - updater := autoupdate.NewClientUpdater( + updater := tools.NewUpdater( clientTools(), toolsDir, version, - autoupdate.WithBaseURL(baseUrl), + tools.WithBaseURL(baseUrl), ) toolsVersion, reExec := updater.CheckLocal() if reExec { diff --git a/integration/autoupdate/tools/client_update_test.go b/integration/autoupdate/tools/updater_test.go similarity index 94% rename from integration/autoupdate/tools/client_update_test.go rename to integration/autoupdate/tools/updater_test.go index d30516705252..92edc454ac36 100644 --- a/integration/autoupdate/tools/client_update_test.go +++ b/integration/autoupdate/tools/updater_test.go @@ -35,7 +35,7 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/constants" - "github.com/gravitational/teleport/lib/autoupdate" + "github.com/gravitational/teleport/lib/autoupdate/tools" ) var ( @@ -50,11 +50,11 @@ func TestUpdate(t *testing.T) { defer cancel() // Fetch compiled test binary with updater logic and install to $TELEPORT_HOME. - updater := autoupdate.NewClientUpdater( + updater := tools.NewUpdater( clientTools(), toolsDir, testVersions[0], - autoupdate.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + tools.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), ) err := updater.Update(ctx, testVersions[0]) require.NoError(t, err) @@ -92,11 +92,11 @@ func TestParallelUpdate(t *testing.T) { defer cancel() // Initial fetch the updater binary un-archive and replace. - updater := autoupdate.NewClientUpdater( + updater := tools.NewUpdater( clientTools(), toolsDir, testVersions[0], - autoupdate.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + tools.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), ) err := updater.Update(ctx, testVersions[0]) require.NoError(t, err) @@ -166,11 +166,11 @@ func TestUpdateInterruptSignal(t *testing.T) { defer cancel() // Initial fetch the updater binary un-archive and replace. - updater := autoupdate.NewClientUpdater( + updater := tools.NewUpdater( clientTools(), toolsDir, testVersions[0], - autoupdate.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + tools.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), ) err := updater.Update(ctx, testVersions[0]) require.NoError(t, err) diff --git a/lib/autoupdate/feature_ent.go b/lib/autoupdate/tools/feature_ent.go similarity index 97% rename from lib/autoupdate/feature_ent.go rename to lib/autoupdate/tools/feature_ent.go index 804f4977a037..e3cd5ca15ae3 100644 --- a/lib/autoupdate/feature_ent.go +++ b/lib/autoupdate/tools/feature_ent.go @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package autoupdate +package tools func init() { featureFlag |= FlagEnt diff --git a/lib/autoupdate/feature_fips.go b/lib/autoupdate/tools/feature_fips.go similarity index 97% rename from lib/autoupdate/feature_fips.go rename to lib/autoupdate/tools/feature_fips.go index d11e7e36a0de..569118a76c60 100644 --- a/lib/autoupdate/feature_fips.go +++ b/lib/autoupdate/tools/feature_fips.go @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package autoupdate +package tools func init() { featureFlag |= FlagFips diff --git a/lib/autoupdate/progress.go b/lib/autoupdate/tools/progress.go similarity index 98% rename from lib/autoupdate/progress.go rename to lib/autoupdate/tools/progress.go index 60d94cb65e88..95395003730e 100644 --- a/lib/autoupdate/progress.go +++ b/lib/autoupdate/tools/progress.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package autoupdate +package tools import ( "fmt" diff --git a/lib/autoupdate/client_update.go b/lib/autoupdate/tools/updater.go similarity index 86% rename from lib/autoupdate/client_update.go rename to lib/autoupdate/tools/updater.go index 90ee0318b823..a3277ca32db6 100644 --- a/lib/autoupdate/client_update.go +++ b/lib/autoupdate/tools/updater.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package autoupdate +package tools import ( "bytes" @@ -51,8 +51,6 @@ const ( teleportToolsVersionEnv = "TELEPORT_TOOLS_VERSION" // baseURL is CDN URL for downloading official Teleport packages. baseURL = "https://cdn.teleport.dev" - // checksumHexLen is length of the hash sum. - checksumHexLen = 64 // reservedFreeDisk is the predefined amount of free disk space (in bytes) required // to remain available after downloading archives. reservedFreeDisk = 10 * 1024 * 1024 // 10 Mb @@ -67,25 +65,25 @@ var ( pattern = regexp.MustCompile(`(?m)Teleport v(.*) git`) ) -// ClientOption applies an option value for the ClientUpdater. -type ClientOption func(u *ClientUpdater) +// Option applies an option value for the Updater. +type Option func(u *Updater) // WithBaseURL defines custom base url for the updater. -func WithBaseURL(baseUrl string) ClientOption { - return func(u *ClientUpdater) { +func WithBaseURL(baseUrl string) Option { + return func(u *Updater) { u.baseUrl = baseUrl } } -// WithClient defines custom http client for the ClientUpdater. -func WithClient(client *http.Client) ClientOption { - return func(u *ClientUpdater) { +// WithClient defines custom http client for the Updater. +func WithClient(client *http.Client) Option { + return func(u *Updater) { u.client = client } } -// ClientUpdater is updater implementation for the client tools auto updates. -type ClientUpdater struct { +// Updater is updater implementation for the client tools auto updates. +type Updater struct { toolsDir string localVersion string tools []string @@ -94,9 +92,9 @@ type ClientUpdater struct { client *http.Client } -// NewClientUpdater initiate updater for the client tools auto updates. -func NewClientUpdater(tools []string, toolsDir string, localVersion string, options ...ClientOption) *ClientUpdater { - updater := &ClientUpdater{ +// NewUpdater initiate updater for the client tools auto updates. +func NewUpdater(tools []string, toolsDir string, localVersion string, options ...Option) *Updater { + updater := &Updater{ tools: tools, toolsDir: toolsDir, localVersion: localVersion, @@ -111,7 +109,7 @@ func NewClientUpdater(tools []string, toolsDir string, localVersion string, opti } // CheckLocal is run at client tool startup and will only perform local checks. -func (u *ClientUpdater) CheckLocal() (string, bool) { +func (u *Updater) CheckLocal() (string, bool) { // Check if the user has requested a specific version of client tools. requestedVersion := os.Getenv(teleportToolsVersionEnv) switch { @@ -125,7 +123,7 @@ func (u *ClientUpdater) CheckLocal() (string, bool) { // If a version of client tools has already been downloaded to // tools directory, return that. - toolsVersion, err := checkClientToolVersion(u.toolsDir) + toolsVersion, err := checkToolVersion(u.toolsDir) if err != nil { return "", false } @@ -139,7 +137,7 @@ func (u *ClientUpdater) CheckLocal() (string, bool) { // CheckRemote will check against Proxy Service if client tools need to be // updated. -func (u *ClientUpdater) CheckRemote(ctx context.Context, proxyAddr string) (string, bool, error) { +func (u *Updater) CheckRemote(ctx context.Context, proxyAddr string) (string, bool, error) { // Check if the user has requested a specific version of client tools. requestedVersion := os.Getenv(teleportToolsVersionEnv) switch { @@ -167,7 +165,7 @@ func (u *ClientUpdater) CheckRemote(ctx context.Context, proxyAddr string) (stri // If a version of client tools has already been downloaded to // tools directory, return that. - toolsVersion, err := checkClientToolVersion(u.toolsDir) + toolsVersion, err := checkToolVersion(u.toolsDir) if err != nil { return "", false, trace.Wrap(err) } @@ -187,7 +185,7 @@ func (u *ClientUpdater) CheckRemote(ctx context.Context, proxyAddr string) (stri } // UpdateWithLock acquires filesystem lock, downloads requested version package, unarchive and replace existing one. -func (u *ClientUpdater) UpdateWithLock(ctx context.Context, toolsVersion string) (err error) { +func (u *Updater) UpdateWithLock(ctx context.Context, toolsVersion string) (err error) { // Create tools directory if it does not exist. if err := os.MkdirAll(u.toolsDir, 0o755); err != nil { return trace.Wrap(err) @@ -204,7 +202,7 @@ func (u *ClientUpdater) UpdateWithLock(ctx context.Context, toolsVersion string) // If the version of the running binary or the version downloaded to // tools directory is the same as the requested version of client tools, // nothing to be done, exit early. - teleportVersion, err := checkClientToolVersion(u.toolsDir) + teleportVersion, err := checkToolVersion(u.toolsDir) if err != nil && !trace.IsNotFound(err) { return trace.Wrap(err) @@ -223,7 +221,7 @@ func (u *ClientUpdater) UpdateWithLock(ctx context.Context, toolsVersion string) // Update downloads requested version and replace it with existing one and cleanups the previous downloads // with defined updater directory suffix. -func (u *ClientUpdater) Update(ctx context.Context, toolsVersion string) error { +func (u *Updater) Update(ctx context.Context, toolsVersion string) error { // Get platform specific download URLs. packages, err := teleportPackageURLs(u.baseUrl, toolsVersion) if err != nil { @@ -244,7 +242,7 @@ func (u *ClientUpdater) Update(ctx context.Context, toolsVersion string) error { // update downloads the archive and validate against the hash. Download to a // temporary path within tools directory. -func (u *ClientUpdater) update(ctx context.Context, pkg packageURL) error { +func (u *Updater) update(ctx context.Context, pkg packageURL) error { hash, err := u.downloadHash(ctx, pkg.Hash) if pkg.Optional && trace.IsNotFound(err) { return nil @@ -289,7 +287,7 @@ func (u *ClientUpdater) update(ctx context.Context, pkg packageURL) error { } // Exec re-executes tool command with same arguments and environ variables. -func (u *ClientUpdater) Exec() (int, error) { +func (u *Updater) Exec() (int, error) { path, err := toolName(u.toolsDir) if err != nil { return 0, trace.Wrap(err) @@ -317,7 +315,7 @@ func (u *ClientUpdater) Exec() (int, error) { return 0, nil } -func (u *ClientUpdater) downloadHash(ctx context.Context, url string) (string, error) { +func (u *Updater) downloadHash(ctx context.Context, url string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", trace.Wrap(err) @@ -335,7 +333,7 @@ func (u *ClientUpdater) downloadHash(ctx context.Context, url string) (string, e } var buf bytes.Buffer - _, err = io.CopyN(&buf, resp.Body, checksumHexLen) + _, err = io.CopyN(&buf, resp.Body, sha256.Size*2) if err != nil { return "", trace.Wrap(err) } @@ -346,7 +344,7 @@ func (u *ClientUpdater) downloadHash(ctx context.Context, url string) (string, e return raw, nil } -func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, url string) (string, string, error) { +func (u *Updater) downloadArchive(ctx context.Context, downloadDir string, url string) (string, string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", "", trace.Wrap(err) @@ -388,5 +386,5 @@ func (u *ClientUpdater) downloadArchive(ctx context.Context, downloadDir string, return "", "", trace.Wrap(err) } - return f.Name(), fmt.Sprintf("%x", h.Sum(nil)), nil + return f.Name(), hex.EncodeToString(h.Sum(nil)), nil } diff --git a/lib/autoupdate/utils.go b/lib/autoupdate/tools/utils.go similarity index 96% rename from lib/autoupdate/utils.go rename to lib/autoupdate/tools/utils.go index db7bdb21e3c7..88dabb62de83 100644 --- a/lib/autoupdate/utils.go +++ b/lib/autoupdate/tools/utils.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package autoupdate +package tools import ( "bufio" @@ -49,8 +49,8 @@ var ( featureFlag int ) -// ToolsDir returns the path to {tsh, tctl} in $TELEPORT_HOME/bin. -func ToolsDir() (string, error) { +// Dir returns the path to client tools in $TELEPORT_HOME/bin. +func Dir() (string, error) { home := os.Getenv(types.HomeEnvVar) if home == "" { var err error @@ -63,7 +63,7 @@ func ToolsDir() (string, error) { return filepath.Join(filepath.Clean(home), ".tsh", "bin"), nil } -func checkClientToolVersion(toolsDir string) (string, error) { +func checkToolVersion(toolsDir string) (string, error) { // Find the path to the current executable. path, err := toolName(toolsDir) if err != nil { From 892f6f7a36a13d326a465b96187afc19296e040a Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Wed, 16 Oct 2024 17:24:13 -0400 Subject: [PATCH 06/14] Move context out from the library Base URL renaming --- integration/autoupdate/tools/main_test.go | 2 +- integration/autoupdate/tools/updater/main.go | 11 +++++++---- lib/autoupdate/tools/updater.go | 18 +++++++----------- lib/autoupdate/tools/utils.go | 10 +++++----- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/integration/autoupdate/tools/main_test.go b/integration/autoupdate/tools/main_test.go index a9f60a7737d8..5feab36f2222 100644 --- a/integration/autoupdate/tools/main_test.go +++ b/integration/autoupdate/tools/main_test.go @@ -182,7 +182,7 @@ func buildBinary(output string, toolsDir string, version string, baseURL string) "-ldflags", strings.Join([]string{ fmt.Sprintf("-X 'main.toolsDir=%s'", toolsDir), fmt.Sprintf("-X 'main.version=%s'", version), - fmt.Sprintf("-X 'main.baseUrl=http://%s'", baseURL), + fmt.Sprintf("-X 'main.baseURL=http://%s'", baseURL), }, " "), "./updater", ) diff --git a/integration/autoupdate/tools/updater/main.go b/integration/autoupdate/tools/updater/main.go index 3d11d5ffeff4..f38e08bb496c 100644 --- a/integration/autoupdate/tools/updater/main.go +++ b/integration/autoupdate/tools/updater/main.go @@ -24,7 +24,9 @@ import ( "fmt" "log" "os" + "os/signal" "runtime" + "syscall" "time" "github.com/gravitational/teleport/api/constants" @@ -33,25 +35,26 @@ import ( var ( version = "development" - baseUrl = "http://localhost" + baseURL = "http://localhost" toolsDir = "" ) func main() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + signalCtx, _ := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) updater := tools.NewUpdater( clientTools(), toolsDir, version, - tools.WithBaseURL(baseUrl), + tools.WithBaseURL(baseURL), ) toolsVersion, reExec := updater.CheckLocal() if reExec { // Download and update the version of client tools required by the cluster. // This is required if the user passed in the TELEPORT_TOOLS_VERSION explicitly. - err := updater.UpdateWithLock(ctx, toolsVersion) + err := updater.UpdateWithLock(signalCtx, toolsVersion) if errors.Is(err, context.Canceled) { os.Exit(0) return diff --git a/lib/autoupdate/tools/updater.go b/lib/autoupdate/tools/updater.go index a3277ca32db6..107912a4c562 100644 --- a/lib/autoupdate/tools/updater.go +++ b/lib/autoupdate/tools/updater.go @@ -30,7 +30,6 @@ import ( "net/http" "os" "os/exec" - "os/signal" "path/filepath" "regexp" "runtime" @@ -69,9 +68,9 @@ var ( type Option func(u *Updater) // WithBaseURL defines custom base url for the updater. -func WithBaseURL(baseUrl string) Option { +func WithBaseURL(baseURL string) Option { return func(u *Updater) { - u.baseUrl = baseUrl + u.baseURL = baseURL } } @@ -88,7 +87,7 @@ type Updater struct { localVersion string tools []string - baseUrl string + baseURL string client *http.Client } @@ -98,7 +97,7 @@ func NewUpdater(tools []string, toolsDir string, localVersion string, options .. tools: tools, toolsDir: toolsDir, localVersion: localVersion, - baseUrl: baseURL, + baseURL: baseURL, client: http.DefaultClient, } for _, option := range options { @@ -223,16 +222,13 @@ func (u *Updater) UpdateWithLock(ctx context.Context, toolsVersion string) (err // 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.baseURL, toolsVersion) if err != nil { return trace.Wrap(err) } - signalCtx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) - defer cancel() - for _, pkg := range packages { - if err := u.update(signalCtx, pkg); err != nil { + if err := u.update(ctx, pkg); err != nil { return trace.Wrap(err) } } @@ -333,7 +329,7 @@ func (u *Updater) downloadHash(ctx context.Context, url string) (string, error) } var buf bytes.Buffer - _, err = io.CopyN(&buf, resp.Body, sha256.Size*2) + _, err = io.CopyN(&buf, resp.Body, sha256.Size*2) // SHA bytes to hex if err != nil { return "", trace.Wrap(err) } diff --git a/lib/autoupdate/tools/utils.go b/lib/autoupdate/tools/utils.go index 88dabb62de83..a243862a6d92 100644 --- a/lib/autoupdate/tools/utils.go +++ b/lib/autoupdate/tools/utils.go @@ -125,23 +125,23 @@ type packageURL struct { // 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) { +func teleportPackageURLs(baseURL, toolsVersion string) ([]packageURL, error) { switch runtime.GOOS { case "darwin": - tsh := baseUrl + "/tsh-" + toolsVersion + ".pkg" - teleport := baseUrl + "/teleport-" + toolsVersion + ".pkg" + 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" + archive := baseURL + "/teleport-v" + toolsVersion + "-windows-amd64-bin.zip" return []packageURL{ {Archive: archive, Hash: archive + ".sha256"}, }, nil case "linux": var b strings.Builder - b.WriteString(baseUrl + "/teleport-") + b.WriteString(baseURL + "/teleport-") if featureFlag&(FlagEnt|FlagFips) != 0 { b.WriteString("ent-") } From bfb33f6c1db6dcf78382a4b678ddb1e1f29813aa Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Wed, 16 Oct 2024 17:59:43 -0400 Subject: [PATCH 07/14] Add more context in comments --- integration/autoupdate/tools/updater/main.go | 4 ++-- lib/autoupdate/tools/updater.go | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/integration/autoupdate/tools/updater/main.go b/integration/autoupdate/tools/updater/main.go index f38e08bb496c..e14c76e5d5aa 100644 --- a/integration/autoupdate/tools/updater/main.go +++ b/integration/autoupdate/tools/updater/main.go @@ -42,8 +42,8 @@ var ( func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + ctx, _ = signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) - signalCtx, _ := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) updater := tools.NewUpdater( clientTools(), toolsDir, @@ -54,7 +54,7 @@ func main() { if reExec { // Download and update the version of client tools required by the cluster. // This is required if the user passed in the TELEPORT_TOOLS_VERSION explicitly. - err := updater.UpdateWithLock(signalCtx, toolsVersion) + err := updater.UpdateWithLock(ctx, toolsVersion) if errors.Is(err, context.Canceled) { os.Exit(0) return diff --git a/lib/autoupdate/tools/updater.go b/lib/autoupdate/tools/updater.go index 107912a4c562..3054eb3f4f70 100644 --- a/lib/autoupdate/tools/updater.go +++ b/lib/autoupdate/tools/updater.go @@ -91,7 +91,10 @@ type Updater struct { client *http.Client } -// NewUpdater initiate updater for the client tools auto updates. +// NewUpdater initializes the updater for client tools auto updates. We need to specify the list +// of tools (e.g., `tsh`, `tctl`) that should be updated, the tools directory path where we +// download, extract package archives with the new version, and replace symlinks (e.g., `$TELEPORT_HOME/bin`). +// The base URL of the CDN with Teleport packages and the `http.Client` can be customized via options. func NewUpdater(tools []string, toolsDir string, localVersion string, options ...Option) *Updater { updater := &Updater{ tools: tools, @@ -183,7 +186,8 @@ func (u *Updater) CheckRemote(ctx context.Context, proxyAddr string) (string, bo return toolsVersion, false, nil } -// UpdateWithLock acquires filesystem lock, downloads requested version package, unarchive and replace existing one. +// UpdateWithLock acquires filesystem lock, downloads requested version package, +// unarchive and replace existing one. func (u *Updater) UpdateWithLock(ctx context.Context, toolsVersion string) (err error) { // Create tools directory if it does not exist. if err := os.MkdirAll(u.toolsDir, 0o755); err != nil { From f74b472cc786ada5cd04a8b15594787ece047d92 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Thu, 17 Oct 2024 11:39:38 -0400 Subject: [PATCH 08/14] Changes in find endpoint --- lib/autoupdate/tools/updater.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/autoupdate/tools/updater.go b/lib/autoupdate/tools/updater.go index 3054eb3f4f70..2cb9a139d4e6 100644 --- a/lib/autoupdate/tools/updater.go +++ b/lib/autoupdate/tools/updater.go @@ -41,6 +41,7 @@ import ( "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/teleport/api/types/autoupdate" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/packaging" ) @@ -175,7 +176,7 @@ func (u *Updater) CheckRemote(ctx context.Context, proxyAddr string) (string, bo switch { case requestedVersion != "" && requestedVersion != toolsVersion: return requestedVersion, true, nil - case !resp.AutoUpdate.ToolsAutoUpdate || resp.AutoUpdate.ToolsVersion == "": + case resp.AutoUpdate.ToolsMode != autoupdate.ToolsUpdateModeEnabled || resp.AutoUpdate.ToolsVersion == "": return "", false, nil case u.localVersion == resp.AutoUpdate.ToolsVersion: return resp.AutoUpdate.ToolsVersion, false, nil From 52e47c88ee5ac6b1a76a3b72f9d557956ede9a7e Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Thu, 17 Oct 2024 16:17:31 -0400 Subject: [PATCH 09/14] Replace test http server with `httptest` Replace hash for bytes matching Proper temp file close for archive download --- integration/autoupdate/tools/main_test.go | 48 ++++------- integration/autoupdate/tools/updater_test.go | 12 +-- lib/autoupdate/tools/updater.go | 87 +++++++++++--------- lib/autoupdate/tools/utils.go | 4 +- 4 files changed, 68 insertions(+), 83 deletions(-) diff --git a/integration/autoupdate/tools/main_test.go b/integration/autoupdate/tools/main_test.go index 5feab36f2222..a14a6dc9fc68 100644 --- a/integration/autoupdate/tools/main_test.go +++ b/integration/autoupdate/tools/main_test.go @@ -26,8 +26,8 @@ import ( "fmt" "io" "log" - "net" "net/http" + "net/http/httptest" "os" "os/exec" "path/filepath" @@ -69,10 +69,18 @@ func TestMain(m *testing.M) { log.Fatalf("failed to create temporary directory: %v", err) } - var srv *http.Server - srv, baseURL = startTestHTTPServer(tmp) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + filePath := filepath.Join(tmp, r.URL.Path) + switch { + case strings.HasSuffix(r.URL.Path, ".sha256"): + serve256File(w, r, strings.TrimSuffix(filePath, ".sha256")) + default: + http.ServeFile(limitedWriter.Wrap(w), r, filePath) + } + })) + baseURL = server.URL for _, version := range testVersions { - if err := buildAndArchiveApps(ctx, tmp, toolsDir, version, baseURL); err != nil { + if err := buildAndArchiveApps(ctx, tmp, toolsDir, version, server.URL); err != nil { log.Fatalf("failed to build testing app binary archive: %v", err) } } @@ -80,9 +88,7 @@ func TestMain(m *testing.M) { // Run tests after binary is built. code := m.Run() - if err := srv.Close(); err != nil { - log.Fatalf("failed to shutdown server: %v", err) - } + server.Close() if err := os.RemoveAll(tmp); err != nil { log.Fatalf("failed to remove temporary directory: %v", err) } @@ -121,32 +127,6 @@ func serve256File(w http.ResponseWriter, _ *http.Request, filePath string) { } } -// startTestHTTPServer starts the file-serving HTTP server for testing. -func startTestHTTPServer(baseDir string) (*http.Server, string) { - srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - filePath := filepath.Join(baseDir, r.URL.Path) - switch { - case strings.HasSuffix(r.URL.Path, ".sha256"): - serve256File(w, r, strings.TrimSuffix(filePath, ".sha256")) - default: - http.ServeFile(limitedWriter.Wrap(w), r, filePath) - } - })} - - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - log.Fatalf("failed to create listener: %v", err) - } - - go func() { - if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Printf("failed to start server: %s", err) - } - }() - - return srv, listener.Addr().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 { versionPath := filepath.Join(path, version) @@ -182,7 +162,7 @@ func buildBinary(output string, toolsDir string, version string, baseURL string) "-ldflags", strings.Join([]string{ fmt.Sprintf("-X 'main.toolsDir=%s'", toolsDir), fmt.Sprintf("-X 'main.version=%s'", version), - fmt.Sprintf("-X 'main.baseURL=http://%s'", baseURL), + fmt.Sprintf("-X 'main.baseURL=%s'", baseURL), }, " "), "./updater", ) diff --git a/integration/autoupdate/tools/updater_test.go b/integration/autoupdate/tools/updater_test.go index 92edc454ac36..96d548646206 100644 --- a/integration/autoupdate/tools/updater_test.go +++ b/integration/autoupdate/tools/updater_test.go @@ -54,7 +54,7 @@ func TestUpdate(t *testing.T) { clientTools(), toolsDir, testVersions[0], - tools.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + tools.WithBaseURL(baseURL), ) err := updater.Update(ctx, testVersions[0]) require.NoError(t, err) @@ -96,7 +96,7 @@ func TestParallelUpdate(t *testing.T) { clientTools(), toolsDir, testVersions[0], - tools.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + tools.WithBaseURL(baseURL), ) err := updater.Update(ctx, testVersions[0]) require.NoError(t, err) @@ -108,9 +108,9 @@ func TestParallelUpdate(t *testing.T) { lock: lock, }) - var outputs [3]bytes.Buffer - errChan := make(chan error, cap(outputs)) - for i := 0; i < cap(outputs); i++ { + outputs := make([]bytes.Buffer, 3) + errChan := make(chan error, 3) + for i := 0; i < len(outputs); i++ { cmd := exec.Command(filepath.Join(toolsDir, "tsh"), "version") cmd.Stdout = &outputs[i] cmd.Stderr = &outputs[i] @@ -170,7 +170,7 @@ func TestUpdateInterruptSignal(t *testing.T) { clientTools(), toolsDir, testVersions[0], - tools.WithBaseURL(fmt.Sprintf("http://%s", baseURL)), + tools.WithBaseURL(baseURL), ) err := updater.Update(ctx, testVersions[0]) require.NoError(t, err) diff --git a/lib/autoupdate/tools/updater.go b/lib/autoupdate/tools/updater.go index 2cb9a139d4e6..222f3ebc72ba 100644 --- a/lib/autoupdate/tools/updater.go +++ b/lib/autoupdate/tools/updater.go @@ -112,15 +112,16 @@ func NewUpdater(tools []string, toolsDir string, localVersion string, options .. } // CheckLocal is run at client tool startup and will only perform local checks. +// Returns the version needs to be updated and re-executed. func (u *Updater) CheckLocal() (string, bool) { // Check if the user has requested a specific version of client tools. requestedVersion := os.Getenv(teleportToolsVersionEnv) - switch { + switch requestedVersion { // The user has turned off any form of automatic updates. - case requestedVersion == "off": + case "off": return "", false // Requested version already the same as client version. - case u.localVersion == requestedVersion: + case u.localVersion: return requestedVersion, false } @@ -138,17 +139,17 @@ func (u *Updater) CheckLocal() (string, bool) { return toolsVersion, false } -// CheckRemote will check against Proxy Service if client tools need to be -// updated. +// CheckRemote will check against Proxy Service if client tools need to be updated. +// Returns the version needs to be updated and re-executed. func (u *Updater) CheckRemote(ctx context.Context, proxyAddr string) (string, bool, error) { // Check if the user has requested a specific version of client tools. requestedVersion := os.Getenv(teleportToolsVersionEnv) - switch { + switch requestedVersion { // The user has turned off any form of automatic updates. - case requestedVersion == "off": + case "off": return "", false, nil // Requested version already the same as client version. - case u.localVersion == requestedVersion: + case u.localVersion: return requestedVersion, false, nil } @@ -251,19 +252,26 @@ func (u *Updater) update(ctx context.Context, pkg packageURL) error { if err != nil { return trace.Wrap(err) } - archivePath, archiveHash, err := u.downloadArchive(ctx, u.toolsDir, pkg.Archive) - if pkg.Optional && trace.IsNotFound(err) { - return nil - } + + f, err := os.CreateTemp(u.toolsDir, "tmp-") if err != nil { return trace.Wrap(err) } defer func() { - if err := os.Remove(archivePath); err != nil { - slog.WarnContext(ctx, "failed to remove archive", "error", err) + _ = f.Close() + if err := os.Remove(f.Name()); err != nil { + slog.WarnContext(ctx, "failed to remove temporary archive file", "error", err) } }() - if archiveHash != hash { + + archiveHash, err := u.downloadArchive(ctx, pkg.Archive, f) + if pkg.Optional && trace.IsNotFound(err) { + return nil + } + if err != nil { + return trace.Wrap(err) + } + if !bytes.Equal(archiveHash, hash) { return trace.BadParameter("hash of archive does not match downloaded archive") } @@ -276,7 +284,7 @@ func (u *Updater) update(ctx context.Context, pkg packageURL) error { } // Perform atomic replace so concurrent exec do not fail. - if err := packaging.ReplaceToolsBinaries(u.toolsDir, archivePath, extractDir, u.tools); err != nil { + if err := packaging.ReplaceToolsBinaries(u.toolsDir, f.Name(), extractDir, u.tools); err != nil { return trace.Wrap(err) } // Cleanup the tools directory with previously downloaded and un-archived versions. @@ -316,65 +324,62 @@ func (u *Updater) Exec() (int, error) { return 0, nil } -func (u *Updater) downloadHash(ctx context.Context, url string) (string, error) { +// downloadHash downloads the hash file `.sha256` for package checksum validation and return the hash sum. +func (u *Updater) downloadHash(ctx context.Context, url string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return "", trace.Wrap(err) + return nil, trace.Wrap(err) } resp, err := u.client.Do(req) if err != nil { - return "", trace.Wrap(err) + return nil, trace.Wrap(err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { - return "", trace.NotFound("hash file is not found: %v", resp.StatusCode) + return nil, trace.NotFound("hash file is not found: %v", resp.StatusCode) } if resp.StatusCode != http.StatusOK { - return "", trace.BadParameter("bad status when downloading archive hash: %v", resp.StatusCode) + return nil, trace.BadParameter("bad status when downloading archive hash: %v", resp.StatusCode) } var buf bytes.Buffer _, err = io.CopyN(&buf, resp.Body, sha256.Size*2) // SHA bytes to hex if err != nil { - return "", trace.Wrap(err) + return nil, trace.Wrap(err) } - raw := buf.String() - if _, err = hex.DecodeString(raw); err != nil { - return "", trace.Wrap(err) + hexBytes, err := hex.DecodeString(buf.String()) + if err != nil { + return nil, trace.Wrap(err) } - return raw, nil + + return hexBytes, nil } -func (u *Updater) downloadArchive(ctx context.Context, downloadDir string, url string) (string, string, error) { +// downloadArchive downloads the archive package by `url` and writes content to the writer interface, +// return calculated sha256 hash sum of the content. +func (u *Updater) downloadArchive(ctx context.Context, url string, f io.Writer) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return "", "", trace.Wrap(err) + return nil, trace.Wrap(err) } resp, err := u.client.Do(req) if err != nil { - return "", "", trace.Wrap(err) + return nil, trace.Wrap(err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { - return "", "", trace.NotFound("archive file is not found: %v", resp.StatusCode) + return nil, trace.NotFound("archive file is not found: %v", resp.StatusCode) } if resp.StatusCode != http.StatusOK { - return "", "", trace.BadParameter("bad status when downloading archive: %v", resp.StatusCode) + return nil, trace.BadParameter("bad status when downloading archive: %v", resp.StatusCode) } if resp.ContentLength != -1 { if err := checkFreeSpace(u.toolsDir, uint64(resp.ContentLength)); err != nil { - return "", "", trace.Wrap(err) + return nil, trace.Wrap(err) } } - // Caller of this function will remove this file after the atomic swap has - // occurred. - f, err := os.CreateTemp(downloadDir, "tmp-") - if err != nil { - return "", "", trace.Wrap(err) - } - h := sha256.New() pw := &progressWriter{n: 0, limit: resp.ContentLength} body := io.TeeReader(io.TeeReader(resp.Body, h), pw) @@ -384,8 +389,8 @@ func (u *Updater) downloadArchive(ctx context.Context, downloadDir string, url s // tools to validate the hash before trying to operate on the archive. _, err = io.CopyN(f, body, resp.ContentLength) if err != nil { - return "", "", trace.Wrap(err) + return nil, trace.Wrap(err) } - return f.Name(), hex.EncodeToString(h.Sum(nil)), nil + return h.Sum(nil), nil } diff --git a/lib/autoupdate/tools/utils.go b/lib/autoupdate/tools/utils.go index a243862a6d92..5625866a20cb 100644 --- a/lib/autoupdate/tools/utils.go +++ b/lib/autoupdate/tools/utils.go @@ -39,9 +39,9 @@ import ( const ( // FlagEnt represents enterprise version. - FlagEnt = 1 << 0 + FlagEnt = 1 << iota // FlagFips represents enterprise version with fips feature enabled. - FlagFips = 1 << 1 + FlagFips ) var ( From e635b8571f73fabbeb76e26c5b62b6bb5edcd6ee Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Thu, 17 Oct 2024 16:39:15 -0400 Subject: [PATCH 10/14] Add more context to comments --- lib/autoupdate/tools/updater.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/autoupdate/tools/updater.go b/lib/autoupdate/tools/updater.go index 222f3ebc72ba..96991044ccc3 100644 --- a/lib/autoupdate/tools/updater.go +++ b/lib/autoupdate/tools/updater.go @@ -112,8 +112,9 @@ func NewUpdater(tools []string, toolsDir string, localVersion string, options .. } // CheckLocal is run at client tool startup and will only perform local checks. -// Returns the version needs to be updated and re-executed. -func (u *Updater) CheckLocal() (string, bool) { +// Returns the version needs to be updated and re-executed, by re-execution flag we +// understand that update and re-execute is required. +func (u *Updater) CheckLocal() (version string, reExec bool) { // Check if the user has requested a specific version of client tools. requestedVersion := os.Getenv(teleportToolsVersionEnv) switch requestedVersion { @@ -122,7 +123,7 @@ func (u *Updater) CheckLocal() (string, bool) { return "", false // Requested version already the same as client version. case u.localVersion: - return requestedVersion, false + return u.localVersion, false } // If a version of client tools has already been downloaded to @@ -139,9 +140,12 @@ func (u *Updater) CheckLocal() (string, bool) { return toolsVersion, false } -// CheckRemote will check against Proxy Service if client tools need to be updated. -// Returns the version needs to be updated and re-executed. -func (u *Updater) CheckRemote(ctx context.Context, proxyAddr string) (string, bool, error) { +// CheckRemote first checks the version set by the environment variable. If not set or disabled, +// it checks against the Proxy Service to determine if client tools need updating by requesting +// the `webapi/find` handler, which stores information about the required client tools version to +// operate with this cluster. It returns the semantic version that needs updating and whether +// re-execution is necessary, by re-execution flag we understand that update and re-execute is required. +func (u *Updater) CheckRemote(ctx context.Context, proxyAddr string) (version string, reExec bool, err error) { // Check if the user has requested a specific version of client tools. requestedVersion := os.Getenv(teleportToolsVersionEnv) switch requestedVersion { @@ -150,7 +154,7 @@ func (u *Updater) CheckRemote(ctx context.Context, proxyAddr string) (string, bo return "", false, nil // Requested version already the same as client version. case u.localVersion: - return requestedVersion, false, nil + return u.localVersion, false, nil } certPool, err := x509.SystemCertPool() From b01920614e665c3fb143fc75ac93effe6f5e5c2e Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Thu, 17 Oct 2024 16:53:11 -0400 Subject: [PATCH 11/14] Move feature flag to main package to be reused --- lib/autoupdate/feature.go | 38 ++++++++++++++++++++++ lib/autoupdate/{tools => }/feature_ent.go | 2 +- lib/autoupdate/{tools => }/feature_fips.go | 2 +- lib/autoupdate/tools/utils.go | 17 ++-------- 4 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 lib/autoupdate/feature.go rename lib/autoupdate/{tools => }/feature_ent.go (97%) rename lib/autoupdate/{tools => }/feature_fips.go (97%) diff --git a/lib/autoupdate/feature.go b/lib/autoupdate/feature.go new file mode 100644 index 000000000000..aa4f077422b5 --- /dev/null +++ b/lib/autoupdate/feature.go @@ -0,0 +1,38 @@ +/* + * 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 autoupdate + +const ( + // FlagEnt represents enterprise version. + FlagEnt = 1 << iota + // FlagFips represents enterprise version with fips feature enabled. + FlagFips +) + +var ( + // featureFlag stores information about enabled feature to define which package needs + // to be downloaded for auto update (e.g. Enterprise, or package with FIPS). + featureFlag int +) + +// FeatureFlag returns information about enabled feature to identify which package needs +// to be downloaded for auto update (e.g. Enterprise, or package with FIPS). +func FeatureFlag() int { + return featureFlag +} diff --git a/lib/autoupdate/tools/feature_ent.go b/lib/autoupdate/feature_ent.go similarity index 97% rename from lib/autoupdate/tools/feature_ent.go rename to lib/autoupdate/feature_ent.go index e3cd5ca15ae3..804f4977a037 100644 --- a/lib/autoupdate/tools/feature_ent.go +++ b/lib/autoupdate/feature_ent.go @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package tools +package autoupdate func init() { featureFlag |= FlagEnt diff --git a/lib/autoupdate/tools/feature_fips.go b/lib/autoupdate/feature_fips.go similarity index 97% rename from lib/autoupdate/tools/feature_fips.go rename to lib/autoupdate/feature_fips.go index 569118a76c60..d11e7e36a0de 100644 --- a/lib/autoupdate/tools/feature_fips.go +++ b/lib/autoupdate/feature_fips.go @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package tools +package autoupdate func init() { featureFlag |= FlagFips diff --git a/lib/autoupdate/tools/utils.go b/lib/autoupdate/tools/utils.go index 5625866a20cb..b2e867ef18fe 100644 --- a/lib/autoupdate/tools/utils.go +++ b/lib/autoupdate/tools/utils.go @@ -34,21 +34,10 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/autoupdate" "github.com/gravitational/teleport/lib/utils" ) -const ( - // FlagEnt represents enterprise version. - FlagEnt = 1 << iota - // FlagFips represents enterprise version with fips feature enabled. - FlagFips -) - -var ( - // featureFlag stores information about enable - featureFlag int -) - // Dir returns the path to client tools in $TELEPORT_HOME/bin. func Dir() (string, error) { home := os.Getenv(types.HomeEnvVar) @@ -142,11 +131,11 @@ func teleportPackageURLs(baseURL, toolsVersion string) ([]packageURL, error) { case "linux": var b strings.Builder b.WriteString(baseURL + "/teleport-") - if featureFlag&(FlagEnt|FlagFips) != 0 { + if autoupdate.FeatureFlag()&(autoupdate.FlagEnt|autoupdate.FlagFips) != 0 { b.WriteString("ent-") } b.WriteString("v" + toolsVersion + "-" + runtime.GOOS + "-" + runtime.GOARCH + "-") - if featureFlag&FlagFips != 0 { + if autoupdate.FeatureFlag()&autoupdate.FlagFips != 0 { b.WriteString("fips-") } b.WriteString("bin.tar.gz") From 0e366998baf8eabdb1b670d3b0f45a3a3c789e84 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Thu, 17 Oct 2024 19:58:18 -0400 Subject: [PATCH 12/14] Constant rename --- lib/autoupdate/feature.go | 4 ++-- lib/autoupdate/feature_fips.go | 2 +- lib/autoupdate/tools/utils.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/autoupdate/feature.go b/lib/autoupdate/feature.go index aa4f077422b5..e1fc679c162c 100644 --- a/lib/autoupdate/feature.go +++ b/lib/autoupdate/feature.go @@ -21,8 +21,8 @@ package autoupdate const ( // FlagEnt represents enterprise version. FlagEnt = 1 << iota - // FlagFips represents enterprise version with fips feature enabled. - FlagFips + // FlagFIPS represents enterprise version with FIPS feature enabled. + FlagFIPS ) var ( diff --git a/lib/autoupdate/feature_fips.go b/lib/autoupdate/feature_fips.go index d11e7e36a0de..0d290b75bd75 100644 --- a/lib/autoupdate/feature_fips.go +++ b/lib/autoupdate/feature_fips.go @@ -22,5 +22,5 @@ package autoupdate func init() { - featureFlag |= FlagFips + featureFlag |= FlagFIPS } diff --git a/lib/autoupdate/tools/utils.go b/lib/autoupdate/tools/utils.go index b2e867ef18fe..968909b24133 100644 --- a/lib/autoupdate/tools/utils.go +++ b/lib/autoupdate/tools/utils.go @@ -49,7 +49,7 @@ func Dir() (string, error) { } } - return filepath.Join(filepath.Clean(home), ".tsh", "bin"), nil + return filepath.Join(home, ".tsh", "bin"), nil } func checkToolVersion(toolsDir string) (string, error) { @@ -131,11 +131,11 @@ func teleportPackageURLs(baseURL, toolsVersion string) ([]packageURL, error) { case "linux": var b strings.Builder b.WriteString(baseURL + "/teleport-") - if autoupdate.FeatureFlag()&(autoupdate.FlagEnt|autoupdate.FlagFips) != 0 { + if autoupdate.FeatureFlag()&(autoupdate.FlagEnt|autoupdate.FlagFIPS) != 0 { b.WriteString("ent-") } b.WriteString("v" + toolsVersion + "-" + runtime.GOOS + "-" + runtime.GOARCH + "-") - if autoupdate.FeatureFlag()&autoupdate.FlagFips != 0 { + if autoupdate.FeatureFlag()&autoupdate.FlagFIPS != 0 { b.WriteString("fips-") } b.WriteString("bin.tar.gz") From b06bf9cea7e7363226ccba859db41356f4d2d6cf Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Fri, 18 Oct 2024 14:55:56 -0400 Subject: [PATCH 13/14] Replace build tag with lib/modules to identify enterprise build --- lib/autoupdate/feature.go | 38 ------------------- .../{feature_ent.go => fips_disabled.go} | 9 ++--- .../{feature_fips.go => fips_enabled.go} | 5 +-- lib/autoupdate/tools/utils.go | 7 +++- 4 files changed, 11 insertions(+), 48 deletions(-) delete mode 100644 lib/autoupdate/feature.go rename lib/autoupdate/{feature_ent.go => fips_disabled.go} (87%) rename lib/autoupdate/{feature_fips.go => fips_enabled.go} (92%) diff --git a/lib/autoupdate/feature.go b/lib/autoupdate/feature.go deleted file mode 100644 index e1fc679c162c..000000000000 --- a/lib/autoupdate/feature.go +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 autoupdate - -const ( - // FlagEnt represents enterprise version. - FlagEnt = 1 << iota - // FlagFIPS represents enterprise version with FIPS feature enabled. - FlagFIPS -) - -var ( - // featureFlag stores information about enabled feature to define which package needs - // to be downloaded for auto update (e.g. Enterprise, or package with FIPS). - featureFlag int -) - -// FeatureFlag returns information about enabled feature to identify which package needs -// to be downloaded for auto update (e.g. Enterprise, or package with FIPS). -func FeatureFlag() int { - return featureFlag -} diff --git a/lib/autoupdate/feature_ent.go b/lib/autoupdate/fips_disabled.go similarity index 87% rename from lib/autoupdate/feature_ent.go rename to lib/autoupdate/fips_disabled.go index 804f4977a037..d29592369f39 100644 --- a/lib/autoupdate/feature_ent.go +++ b/lib/autoupdate/fips_disabled.go @@ -1,5 +1,5 @@ -//go:build webassets_ent -// +build webassets_ent +//go:build !fips +// +build !fips /* * Teleport @@ -21,6 +21,5 @@ package autoupdate -func init() { - featureFlag |= FlagEnt -} +// FIPS is disabled when build tag is not specified. +const FIPS = false diff --git a/lib/autoupdate/feature_fips.go b/lib/autoupdate/fips_enabled.go similarity index 92% rename from lib/autoupdate/feature_fips.go rename to lib/autoupdate/fips_enabled.go index 0d290b75bd75..2b79124d6c24 100644 --- a/lib/autoupdate/feature_fips.go +++ b/lib/autoupdate/fips_enabled.go @@ -21,6 +21,5 @@ package autoupdate -func init() { - featureFlag |= FlagFIPS -} +// FIPS is enabled when build tag is specified. +const FIPS = true diff --git a/lib/autoupdate/tools/utils.go b/lib/autoupdate/tools/utils.go index 968909b24133..4fd425391af9 100644 --- a/lib/autoupdate/tools/utils.go +++ b/lib/autoupdate/tools/utils.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/autoupdate" + "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/utils" ) @@ -129,13 +130,15 @@ func teleportPackageURLs(baseURL, toolsVersion string) ([]packageURL, error) { {Archive: archive, Hash: archive + ".sha256"}, }, nil case "linux": + m := modules.GetModules() + var b strings.Builder b.WriteString(baseURL + "/teleport-") - if autoupdate.FeatureFlag()&(autoupdate.FlagEnt|autoupdate.FlagFIPS) != 0 { + if m.IsEnterpriseBuild() || autoupdate.FIPS { b.WriteString("ent-") } b.WriteString("v" + toolsVersion + "-" + runtime.GOOS + "-" + runtime.GOARCH + "-") - if autoupdate.FeatureFlag()&autoupdate.FlagFIPS != 0 { + if autoupdate.FIPS { b.WriteString("fips-") } b.WriteString("bin.tar.gz") From 412124e70982f7993d5b38d43129e584783c9b4f Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Fri, 18 Oct 2024 15:25:26 -0400 Subject: [PATCH 14/14] Replace fips tag with modules flag --- lib/autoupdate/fips_disabled.go | 25 ------------------------- lib/autoupdate/fips_enabled.go | 25 ------------------------- lib/autoupdate/tools/utils.go | 6 ++---- 3 files changed, 2 insertions(+), 54 deletions(-) delete mode 100644 lib/autoupdate/fips_disabled.go delete mode 100644 lib/autoupdate/fips_enabled.go diff --git a/lib/autoupdate/fips_disabled.go b/lib/autoupdate/fips_disabled.go deleted file mode 100644 index d29592369f39..000000000000 --- a/lib/autoupdate/fips_disabled.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build !fips -// +build !fips - -/* - * 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 autoupdate - -// FIPS is disabled when build tag is not specified. -const FIPS = false diff --git a/lib/autoupdate/fips_enabled.go b/lib/autoupdate/fips_enabled.go deleted file mode 100644 index 2b79124d6c24..000000000000 --- a/lib/autoupdate/fips_enabled.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build fips -// +build fips - -/* - * 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 autoupdate - -// FIPS is enabled when build tag is specified. -const FIPS = true diff --git a/lib/autoupdate/tools/utils.go b/lib/autoupdate/tools/utils.go index 4fd425391af9..d552b31abefe 100644 --- a/lib/autoupdate/tools/utils.go +++ b/lib/autoupdate/tools/utils.go @@ -34,7 +34,6 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/autoupdate" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/utils" ) @@ -131,14 +130,13 @@ func teleportPackageURLs(baseURL, toolsVersion string) ([]packageURL, error) { }, nil case "linux": m := modules.GetModules() - var b strings.Builder b.WriteString(baseURL + "/teleport-") - if m.IsEnterpriseBuild() || autoupdate.FIPS { + if m.IsEnterpriseBuild() || m.IsBoringBinary() { b.WriteString("ent-") } b.WriteString("v" + toolsVersion + "-" + runtime.GOOS + "-" + runtime.GOARCH + "-") - if autoupdate.FIPS { + if m.IsBoringBinary() { b.WriteString("fips-") } b.WriteString("bin.tar.gz")