mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
# Overview This pull request resolves #31165, implementing command-line tooling to migrate GitOps YAML files following the [changes introduced in the upcoming 4.74 release](https://github.com/fleetdm/fleet/pull/32237/files#diff-8769f6e90e8bdf15faad8f390fdf3ffb6fd2238b7d6087d83518c21464109119R7). Aligning with the recommended steps in the `README`; [this is an example of the first step](https://github.com/Illbjorn/fleet/pull/3/files) (`gitops-migrate format`) and [this is an example of the second step](https://github.com/Illbjorn/fleet/pull/4/files) (`gitops-migrate migrate`). --------- Signed-off-by: Illbjorn <am@hades.so> Co-authored-by: Ian Littman <iansltx@gmail.com>
253 lines
6.4 KiB
Go
253 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/cmd/gitops-migrate/log"
|
|
)
|
|
|
|
// backup creates a backup of the path provided via 'from', to a gzipped tarball
|
|
// in the directory specified by 'to'.
|
|
func backup(ctx context.Context, from string, to string) (string, error) {
|
|
// Resolve and validate the backup archive output path.
|
|
output, err := resolveBackupTarget(to)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
log.Info(
|
|
"Performing Fleet GitOps file backup.",
|
|
"Source", from,
|
|
"Destination", output,
|
|
)
|
|
|
|
// Get a writable handle to the output archive file.
|
|
//
|
|
//nolint:gosec,G304 // 'output' is a trusted input.
|
|
f, err := os.OpenFile(output, fileFlagsOverwrite, fileModeUserRW)
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"failed to get a writable handle to archive file(%s): %w",
|
|
output, err,
|
|
)
|
|
}
|
|
|
|
// Init the gzip writer.
|
|
gz := gzip.NewWriter(f)
|
|
|
|
// Init the tar writer.
|
|
tw := tar.NewWriter(gz)
|
|
|
|
// Defer stream closure, in reverse order.
|
|
//
|
|
// This is a verbose chunk of code, but it's important to check each error
|
|
// here as the 'gzip.Writer' and 'tar.Writer' closures actually write critical
|
|
// trailer data to the file stream.
|
|
defer func() {
|
|
var errs error
|
|
|
|
// Close the output archive tar writer.
|
|
err = tw.Close()
|
|
if err != nil {
|
|
log.Errorf("Failed to close the backup archive tar writer: %s.", err)
|
|
}
|
|
errs = errors.Join(errs, err)
|
|
|
|
// Close the output archive gzip writer.
|
|
err = gz.Close()
|
|
if err != nil {
|
|
log.Errorf("Failed to close the backup archive gzip writer: %s.", err)
|
|
}
|
|
errs = errors.Join(errs, err)
|
|
|
|
// Close the output archive file stream.
|
|
err := f.Close()
|
|
if err != nil {
|
|
log.Errorf("Failed to close the backup archive file stream: %s.", err)
|
|
}
|
|
errs = errors.Join(errs, err)
|
|
|
|
// We want a good, clean backup before we proceed with hijacking all the
|
|
// targeted GitOps files. So, if we encounter any errors at all, close up
|
|
// shop.
|
|
if errs != nil {
|
|
log.Fatal("Errors encountered in backup archive stream close, exiting.")
|
|
}
|
|
}()
|
|
|
|
// Enumerate the file system, writing files to the tarball.
|
|
//
|
|
//nolint:wrapcheck // Not an external package error.
|
|
for file, err := range fsEnum(from) {
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"encountered erorr in directory enumeration: %w",
|
|
err,
|
|
)
|
|
}
|
|
|
|
// Init the tar header for this file.
|
|
header, err := tar.FileInfoHeader(file.Stats, file.Path)
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"failed to create tar header for file(%s): %w",
|
|
file.Path, err,
|
|
)
|
|
}
|
|
|
|
// Construct the relative file path.
|
|
filePathRelative := strings.TrimPrefix(file.Path, from)
|
|
filePathRelative = strings.TrimPrefix(
|
|
filePathRelative,
|
|
string(os.PathSeparator),
|
|
)
|
|
|
|
// Fix up the tar header.
|
|
//
|
|
// See 'tar.FileInfoHeader' docs for more on why we need to set this twice.
|
|
header.Name = filePathRelative
|
|
// If the item is a directory, zero the size.
|
|
if file.Stats.IsDir() {
|
|
header.Size = 0
|
|
}
|
|
|
|
// Write the tar header.
|
|
err = tw.WriteHeader(header)
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"failed to write tar header to tar stream: %w", err,
|
|
)
|
|
}
|
|
// If the item is a directory, no further to-do here.
|
|
if file.Stats.IsDir() {
|
|
continue
|
|
}
|
|
|
|
log.Debug(
|
|
"Compressing file.",
|
|
"File Path", file.Path,
|
|
"Archive Path", filePathRelative,
|
|
)
|
|
|
|
// Get a readable handle to the file.
|
|
//
|
|
//nolint:gosec,G304 // 'path' is a trusted input.
|
|
f, err := os.Open(file.Path)
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"failed to get a readable handle to file [%s] while performing backup: %w",
|
|
file.Path, err,
|
|
)
|
|
}
|
|
|
|
// Write the file content to the tar stream.
|
|
n, err := io.Copy(tw, f)
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"failed to write file [%s] to tar stream during backup: %w",
|
|
file.Path, err,
|
|
)
|
|
}
|
|
|
|
// Ensure we wrote the content length we expect.
|
|
if n != file.Stats.Size() {
|
|
return "", fmt.Errorf(
|
|
"encountered no error during backup, however the stat'd "+
|
|
"file size(%d) didn't match the size we actually wrote(%d)",
|
|
file.Stats.Size(), n,
|
|
)
|
|
}
|
|
|
|
// Close the file stream.
|
|
err = f.Close()
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"failed to close input file stream(%s) during backup: %w",
|
|
file.Path, err,
|
|
)
|
|
}
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
// resolveBackupTarget evaluates the type of path 'path' points to.
|
|
//
|
|
// If 'path' is a FILE, the parent directory is created if it doesn't exist and
|
|
// the path is returned unchanged.
|
|
//
|
|
// If 'path' is a DIRECTORY, the directory is created if it doesn't exist, a
|
|
// random file name is generated and concatenated to the end of 'path',
|
|
// finally returning the result.
|
|
func resolveBackupTarget(path string) (string, error) {
|
|
// Resolve the absolute file path.
|
|
path, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"failed to identify absolute file path from [%s]: %w",
|
|
path, err,
|
|
)
|
|
}
|
|
|
|
// Attempt to identify if 'path' is a file or directory path by the presence
|
|
// of a file extension.
|
|
if filepath.Ext(path) == "" {
|
|
// 'path' is a directory.
|
|
return resolveBackupDirPath(path)
|
|
}
|
|
|
|
// 'path' is a file.
|
|
return resolveBackupFilePath(path)
|
|
}
|
|
|
|
func resolveBackupDirPath(path string) (string, error) {
|
|
// Create 'path' if it doesn't exist.
|
|
err := os.MkdirAll(path, fileModeUserRWX)
|
|
if err != nil && !errors.Is(err, os.ErrExist) {
|
|
return "", fmt.Errorf(
|
|
"failed to create all or part of the provided path(%s): %w",
|
|
path, err,
|
|
)
|
|
}
|
|
|
|
now := time.Now()
|
|
// Generate a timestamped file name.
|
|
fileName := fmt.Sprintf(
|
|
"fleet-gitops-backup-%d-%d-%d_%d-%d-%d.tar.gz",
|
|
now.Month(), now.Day(), now.Year(), now.Hour(), now.Minute(), now.Second(),
|
|
)
|
|
|
|
// Concatenate the file name to the directory path and return.
|
|
return filepath.Join(path, fileName), nil
|
|
}
|
|
|
|
func resolveBackupFilePath(path string) (string, error) {
|
|
// Create 'path' if it doesn't exist.
|
|
err := os.MkdirAll(filepath.Dir(path), fileModeUserRWX)
|
|
if err != nil && !errors.Is(err, os.ErrExist) {
|
|
return "", fmt.Errorf(
|
|
"failed to create all or part of the provided path(%s): %w",
|
|
path, err,
|
|
)
|
|
}
|
|
|
|
// Return the file path, unchanged.
|
|
return path, nil
|
|
}
|
|
|
|
func mkBackupDir() (string, error) {
|
|
path, err := os.MkdirTemp(os.TempDir(), "fleet-gitops-*")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temp backup directory: %w", err)
|
|
}
|
|
return path, nil
|
|
}
|