Skip to content
170 changes: 170 additions & 0 deletions integration/helpers/archive.go
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.
Comment thread
sclevine marked this conversation as resolved.
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)
}
}
}()

Comment thread
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())
Comment thread
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())
}
17 changes: 17 additions & 0 deletions lib/utils/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

@rosstimothy rosstimothy Oct 10, 2024

Choose a reason for hiding this comment

The 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 make lint-go locally:

lib/utils/disk.go:56:5: SA4003: no value of type uint32 is less than 0 (staticcheck)
	if stat.Bsize < 0 {
	  ^

Copy link
Copy Markdown
Contributor Author

@vapopov vapopov Oct 10, 2024

Choose a reason for hiding this comment

The 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 unix package and replaced with syscall

ztypes_linux_arm.go:

type Statfs_t struct {
	Type    int32
	Bsize   int32
...
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
Expand Down
18 changes: 17 additions & 1 deletion lib/utils/disk_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,29 @@

package utils

import "github.com/gravitational/trace"
import (
"github.com/gravitational/trace"
"golang.org/x/sys/windows"
)

// PercentUsed is not supported on Windows.
func PercentUsed(path string) (float64, error) {
return 0.0, trace.NotImplemented("disk usage not supported on Windows")
}

// FreeDiskWithReserve returns the available disk space (in bytes) on the disk at dir, minus `reservedFreeDisk`.
func FreeDiskWithReserve(dir string, reservedFreeDisk uint64) (uint64, error) {
var avail uint64
err := windows.GetDiskFreeSpaceEx(windows.StringToUTF16Ptr(dir), &avail, nil, nil)
if err != nil {
return 0, trace.Wrap(err)
}
if reservedFreeDisk > avail {
return 0, trace.Errorf("no free space left")
}
return avail - reservedFreeDisk, nil
}

// CanUserWriteTo is not supported on Windows.
func CanUserWriteTo(path string) (bool, error) {
return false, trace.NotImplemented("path permission checking is not supported on Windows")
Expand Down
158 changes: 158 additions & 0 deletions lib/utils/packaging/unarchive.go
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
Comment thread
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()

Comment thread
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()
Comment thread
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
}
Loading