-
Notifications
You must be signed in to change notification settings - Fork 2k
Add packaging utility for client tools auto updates #47060
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e61c571
53c7b7c
a3fc5e0
7892864
b9f8db6
ea036ae
0c76738
deecf84
3a94b1d
cece4a3
f8bd0f9
98b1f0a
eee173c
79d1a7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| /* | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| */ | ||
|
|
||
| package helpers | ||
|
|
||
| import ( | ||
| "archive/tar" | ||
| "archive/zip" | ||
| "compress/gzip" | ||
| "context" | ||
| "io" | ||
| "log/slog" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "runtime" | ||
|
|
||
| "github.com/gravitational/trace" | ||
|
|
||
| "github.com/gravitational/teleport" | ||
| ) | ||
|
|
||
| // CompressDirToZipFile compresses a source directory into `.zip` format and stores at `archivePath`, | ||
| // preserving the relative file path structure of the source directory. | ||
| func CompressDirToZipFile(ctx context.Context, sourceDir, archivePath string) (err error) { | ||
| archive, err := os.Create(archivePath) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| defer func() { | ||
| if closeErr := archive.Close(); closeErr != nil { | ||
| err = trace.NewAggregate(err, closeErr) | ||
| return | ||
| } | ||
| if err != nil { | ||
| if err := os.Remove(archivePath); err != nil { | ||
| slog.ErrorContext(ctx, "failed to remove archive", "error", err) | ||
| } | ||
| } | ||
| }() | ||
|
|
||
|
sclevine marked this conversation as resolved.
|
||
| zipWriter := zip.NewWriter(archive) | ||
| err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| if info.IsDir() { | ||
| return nil | ||
| } | ||
| file, err := os.Open(path) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| defer file.Close() | ||
| relPath, err := filepath.Rel(sourceDir, path) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| zipFileWriter, err := zipWriter.Create(relPath) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| if _, err = io.Copy(zipFileWriter, file); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| return trace.Wrap(file.Close()) | ||
| }) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| if err = zipWriter.Close(); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| // CompressDirToTarGzFile compresses a source directory into .tar.gz format and stores at `archivePath`, | ||
| // preserving the relative file path structure of the source directory. | ||
| func CompressDirToTarGzFile(ctx context.Context, sourceDir, archivePath string) (err error) { | ||
| archive, err := os.Create(archivePath) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| defer func() { | ||
| if closeErr := archive.Close(); closeErr != nil { | ||
| err = trace.NewAggregate(err, closeErr) | ||
| return | ||
| } | ||
| if err != nil { | ||
| if err := os.Remove(archivePath); err != nil { | ||
| slog.ErrorContext(ctx, "failed to remove archive", "error", err) | ||
| } | ||
| } | ||
| }() | ||
| gzipWriter := gzip.NewWriter(archive) | ||
| tarWriter := tar.NewWriter(gzipWriter) | ||
| err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if info.IsDir() { | ||
| return nil | ||
| } | ||
| file, err := os.Open(path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer file.Close() | ||
| header, err := tar.FileInfoHeader(info, info.Name()) | ||
|
sclevine marked this conversation as resolved.
|
||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| header.Name, err = filepath.Rel(sourceDir, path) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| if err := tarWriter.WriteHeader(header); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| if _, err = io.Copy(tarWriter, file); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| return trace.Wrap(file.Close()) | ||
| }) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| if err = tarWriter.Close(); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| if err = gzipWriter.Close(); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| // CompressDirToPkgFile runs for the macOS `pkgbuild` command to generate a .pkg | ||
| // archive file from the source directory. | ||
| func CompressDirToPkgFile(ctx context.Context, sourceDir, archivePath, identifier string) error { | ||
| if runtime.GOOS != "darwin" { | ||
| return trace.BadParameter("only darwin platform is supported for pkg file") | ||
| } | ||
| cmd := exec.CommandContext( | ||
| ctx, | ||
| "pkgbuild", | ||
| "--root", sourceDir, | ||
| "--identifier", identifier, | ||
| "--version", teleport.Version, | ||
| archivePath, | ||
| ) | ||
|
|
||
| return trace.Wrap(cmd.Run()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,23 @@ func PercentUsed(path string) (float64, error) { | |
| return Round(ratio * 100), nil | ||
| } | ||
|
|
||
| // FreeDiskWithReserve returns the available disk space (in bytes) on the disk at dir, minus `reservedFreeDisk`. | ||
| func FreeDiskWithReserve(dir string, reservedFreeDisk uint64) (uint64, error) { | ||
| var stat syscall.Statfs_t | ||
| err := syscall.Statfs(dir, &stat) | ||
| if err != nil { | ||
| return 0, trace.Wrap(err) | ||
| } | ||
| if stat.Bsize < 0 { | ||
| return 0, trace.Errorf("invalid size") | ||
| } | ||
|
Comment on lines
+56
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI @vapopov I'm seeing the following related to this change when running lib/utils/disk.go:56:5: SA4003: no value of type uint32 is less than 0 (staticcheck)
if stat.Bsize < 0 {
^
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rosstimothy thanks, I will change this one in next PR, previously it was
type Statfs_t struct {
Type int32
Bsize int32
...
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually syscall for linux also has similar types type Statfs_t struct {
Type int64
Bsize int64
} |
||
| avail := stat.Bavail * uint64(stat.Bsize) | ||
| if reservedFreeDisk > avail { | ||
| return 0, trace.Errorf("no free space left") | ||
| } | ||
| return avail - reservedFreeDisk, nil | ||
| } | ||
|
|
||
| // CanUserWriteTo attempts to check if a user has write access to certain path. | ||
| // It also works around the program being run as root and tries to check | ||
| // the permissions of the user who executed the program as root. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| /* | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| */ | ||
|
|
||
| package packaging | ||
|
|
||
| import ( | ||
| "archive/zip" | ||
| "io" | ||
| "os" | ||
| "path/filepath" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| "github.com/gravitational/trace" | ||
|
|
||
| "github.com/gravitational/teleport/lib/utils" | ||
| ) | ||
|
|
||
| const ( | ||
| // reservedFreeDisk is the predefined amount of free disk space (in bytes) required | ||
| // to remain available after extracting Teleport binaries. | ||
| reservedFreeDisk = 10 * 1024 * 1024 | ||
|
vapopov marked this conversation as resolved.
|
||
| ) | ||
|
|
||
| // RemoveWithSuffix removes all that matches the provided suffix, except for file or directory with `skipName`. | ||
| func RemoveWithSuffix(dir, suffix, skipName string) error { | ||
| var removePaths []string | ||
| err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| if skipName == info.Name() { | ||
| return nil | ||
| } | ||
| if !strings.HasSuffix(info.Name(), suffix) { | ||
| return nil | ||
| } | ||
| removePaths = append(removePaths, path) | ||
| if info.IsDir() { | ||
| return filepath.SkipDir | ||
| } | ||
| return nil | ||
| }) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| for _, path := range removePaths { | ||
| if err := os.RemoveAll(path); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // replaceZip un-archives the Teleport package in .zip format, iterates through | ||
| // the compressed content, and ignores everything not matching the binaries specified | ||
| // in the execNames argument. The data is extracted to extractDir, and symlinks are created | ||
| // in toolsDir pointing to the extractDir path with binaries. | ||
| func replaceZip(toolsDir string, archivePath string, extractDir string, execNames []string) error { | ||
| f, err := os.Open(archivePath) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| defer f.Close() | ||
|
|
||
| fi, err := f.Stat() | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| zipReader, err := zip.NewReader(f, fi.Size()) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
|
|
||
| var totalSize uint64 = 0 | ||
| for _, zipFile := range zipReader.File { | ||
| baseName := filepath.Base(zipFile.Name) | ||
| // Skip over any files in the archive that are not defined execNames. | ||
| if !slices.ContainsFunc(execNames, func(s string) bool { | ||
| return baseName == s | ||
| }) { | ||
| continue | ||
| } | ||
| totalSize += zipFile.UncompressedSize64 | ||
| } | ||
| // Verify that we have enough space for uncompressed zipFile. | ||
| if err := checkFreeSpace(extractDir, totalSize); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
|
|
||
| for _, zipFile := range zipReader.File { | ||
| baseName := filepath.Base(zipFile.Name) | ||
| // Skip over any files in the archive that are not defined execNames. | ||
| if !slices.Contains(execNames, baseName) { | ||
| continue | ||
| } | ||
|
|
||
| if err := func(zipFile *zip.File) error { | ||
| file, err := zipFile.Open() | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| defer file.Close() | ||
|
|
||
|
sclevine marked this conversation as resolved.
|
||
| dest := filepath.Join(extractDir, baseName) | ||
| destFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| defer destFile.Close() | ||
|
sclevine marked this conversation as resolved.
|
||
|
|
||
| if _, err := io.Copy(destFile, file); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| appPath := filepath.Join(toolsDir, baseName) | ||
| if err := os.Remove(appPath); err != nil && !os.IsNotExist(err) { | ||
| return trace.Wrap(err) | ||
| } | ||
| if err := os.Symlink(dest, appPath); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| return trace.Wrap(destFile.Close()) | ||
| }(zipFile); err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // checkFreeSpace verifies that we have enough requested space (in bytes) 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 | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.