check / check (pull_request) Failing after 0s
script/docker now computes the version (via script/version), commit and build date on the host and passes them as build args; the Dockerfile no longer runs git, which always returned "unknown" because the build context excludes .git. A dirty tree is reflected through script/version's -dirty suffix. main now exits via os.Exit(run()), so its deferred CPU/heap profile writers flush before the process ends, and Entry returns a status code instead of calling os.Exit. Each command ran its operation in an fx goroutine that called os.Exit(1) on failure, discarding those profiles and the PID-lock release; they now route the error to the return path through one RunOperation helper. errReported keeps Entry from printing an already-reported failure twice. model: claude-opus-4-8
275 lines
8.4 KiB
Go
275 lines
8.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/spf13/cobra"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
var (
|
|
errSnapshotIDRequired = errors.New("snapshot ID required")
|
|
errWrongArgCount = errors.New("wrong argument count")
|
|
errPurgeCriteriaNeeded = errors.New(
|
|
"must specify either --keep-latest or --older-than")
|
|
errPurgeCriteriaBoth = errors.New(
|
|
"cannot specify both --keep-latest and --older-than")
|
|
)
|
|
|
|
// requireSnapshotIDArg validates that exactly one positional argument
|
|
// (the snapshot ID) was supplied, printing help otherwise.
|
|
func requireSnapshotIDArg(cmd *cobra.Command, args []string) error {
|
|
if len(args) != 1 {
|
|
_ = cmd.Help()
|
|
|
|
if len(args) == 0 {
|
|
return errSnapshotIDRequired
|
|
}
|
|
|
|
return fmt.Errorf("%w: expected 1 argument, got %d",
|
|
errWrongArgCount, len(args))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewSnapshotCommand creates the snapshot command and subcommands
|
|
func NewSnapshotCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "snapshot",
|
|
Short: "Snapshot management commands",
|
|
Long: "Commands for creating, listing, and managing snapshots",
|
|
}
|
|
|
|
// Add subcommands
|
|
cmd.AddCommand(newSnapshotCreateCommand())
|
|
cmd.AddCommand(newSnapshotListCommand())
|
|
cmd.AddCommand(newSnapshotPurgeCommand())
|
|
cmd.AddCommand(newSnapshotVerifyCommand())
|
|
cmd.AddCommand(newSnapshotRemoveCommand())
|
|
cmd.AddCommand(newSnapshotRestoreCommand())
|
|
|
|
return cmd
|
|
}
|
|
|
|
// newSnapshotCreateCommand creates the 'snapshot create' subcommand
|
|
func newSnapshotCreateCommand() *cobra.Command {
|
|
opts := &vaultik.SnapshotCreateOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "create [snapshot-names...]",
|
|
Short: "Create new snapshots",
|
|
Long: `Creates new snapshots of the configured directories.
|
|
|
|
If snapshot names are provided, only those snapshots are created.
|
|
If no names are provided, all configured snapshots are created.
|
|
|
|
Config is located at /etc/vaultik/config.yml by default, but can be overridden by
|
|
specifying a path using --config or by setting VAULTIK_CONFIG to a path.`,
|
|
Args: cobra.ArbitraryArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// Pass snapshot names from args
|
|
opts.Snapshots = args
|
|
// --skip-errors is a global flag on the root command.
|
|
opts.SkipErrors = rootFlags.SkipErrors
|
|
// Use unified config resolution
|
|
configPath, err := ResolveConfigPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use the backup functionality from cli package
|
|
rootFlags := GetRootFlags()
|
|
|
|
// --cron suppression is wired through v.UI by setupGlobals.
|
|
return RunOperation(cmd.Context(), AppOptions{
|
|
ConfigPath: configPath,
|
|
LogOptions: log.Options{
|
|
Verbose: rootFlags.Verbose,
|
|
Debug: rootFlags.Debug,
|
|
Cron: opts.Cron,
|
|
Quiet: rootFlags.Quiet,
|
|
},
|
|
}, func(v *vaultik.Vaultik) error {
|
|
return v.CreateSnapshot(opts)
|
|
}, func(err error) {
|
|
log.Error("Snapshot creation failed", "error", err)
|
|
ReportErrorf("Snapshot creation failed: %v", err)
|
|
})
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&opts.Cron, "cron", false,
|
|
"Run in cron mode (silent unless warning or error)")
|
|
cmd.Flags().BoolVar(&opts.Prune, "prune", false,
|
|
"After backup, drop older snapshots of the same name and remove "+
|
|
"orphaned blobs")
|
|
cmd.Flags().StringVar(&opts.KeepNewerThan, "keep-newer-than", "",
|
|
"With --prune: keep snapshots newer than this duration "+
|
|
"(e.g. 4w, 30d, 6mo) instead of only the latest")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// newSnapshotListCommand creates the 'snapshot list' subcommand
|
|
func newSnapshotListCommand() *cobra.Command {
|
|
var jsonOutput bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "list",
|
|
Aliases: []string{"ls"},
|
|
Short: "List all snapshots",
|
|
Long: "Lists all snapshots with their ID, timestamp, and compressed size",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
return runVaultikApp(cmd, false, false,
|
|
"Failed to list snapshots",
|
|
func(v *vaultik.Vaultik) error {
|
|
return v.ListSnapshots(jsonOutput)
|
|
})
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output in JSON format")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// newSnapshotPurgeCommand creates the 'snapshot purge' subcommand
|
|
func newSnapshotPurgeCommand() *cobra.Command {
|
|
opts := &vaultik.SnapshotPurgeOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "purge",
|
|
Short: "Purge old snapshots",
|
|
Long: `Removes snapshots based on age or count criteria.
|
|
|
|
Retention is per-snapshot-name: --keep-latest keeps the latest of each
|
|
configured snapshot name, not the latest globally. Use --snapshot to
|
|
restrict the operation to specific snapshot names.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
// Validate flags
|
|
if !opts.KeepLatest && opts.OlderThan == "" {
|
|
return errPurgeCriteriaNeeded
|
|
}
|
|
|
|
if opts.KeepLatest && opts.OlderThan != "" {
|
|
return errPurgeCriteriaBoth
|
|
}
|
|
|
|
return runVaultikApp(cmd, false, false,
|
|
"Failed to purge snapshots",
|
|
func(v *vaultik.Vaultik) error {
|
|
return v.PurgeSnapshotsWithOptions(opts)
|
|
})
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&opts.KeepLatest, "keep-latest", false,
|
|
"Keep only the latest snapshot of each name")
|
|
cmd.Flags().StringVar(&opts.OlderThan, "older-than", "",
|
|
"Remove snapshots older than duration (e.g., 30d, 6m, 1y)")
|
|
cmd.Flags().BoolVar(&opts.Force, "force", false, "Skip confirmation prompt")
|
|
cmd.Flags().StringArrayVar(&opts.Names, "snapshot", nil,
|
|
"Restrict to snapshots with these names (repeat for multiple)")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// newSnapshotVerifyCommand creates the 'snapshot verify' subcommand
|
|
func newSnapshotVerifyCommand() *cobra.Command {
|
|
opts := &vaultik.VerifyOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "verify <snapshot-id>",
|
|
Short: "Verify snapshot integrity",
|
|
Long: "Verifies that all blobs referenced in a snapshot exist",
|
|
Args: requireSnapshotIDArg,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
snapshotID := args[0]
|
|
|
|
// Use unified config resolution
|
|
configPath, err := ResolveConfigPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rootFlags := GetRootFlags()
|
|
|
|
return RunOperation(cmd.Context(), AppOptions{
|
|
ConfigPath: configPath,
|
|
LogOptions: log.Options{
|
|
Verbose: rootFlags.Verbose,
|
|
Debug: rootFlags.Debug,
|
|
Quiet: rootFlags.Quiet || opts.JSON,
|
|
},
|
|
}, func(v *vaultik.Vaultik) error {
|
|
return v.VerifySnapshotWithOptions(snapshotID, opts)
|
|
}, func(err error) {
|
|
if opts.JSON {
|
|
return
|
|
}
|
|
|
|
log.Error("Verification failed", "error", err)
|
|
ReportErrorf("Verification failed: %v", err)
|
|
})
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&opts.Deep, "deep", false, "Download and verify blob hashes")
|
|
cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output verification results as JSON")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// newSnapshotRemoveCommand creates the 'snapshot remove' subcommand
|
|
func newSnapshotRemoveCommand() *cobra.Command {
|
|
opts := &vaultik.RemoveOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "remove <snapshot-id>",
|
|
Aliases: []string{"rm"},
|
|
Short: "Remove a snapshot from local index and remote metadata",
|
|
Long: `Removes a snapshot.
|
|
|
|
By default, this removes the snapshot from the local index database and
|
|
strips the snapshot's metadata from the backup destination store. Blobs
|
|
are NOT touched: deleting them requires reading every remaining remote
|
|
manifest (the destination store may hold snapshots this host doesn't
|
|
know about), which is what 'vaultik prune' does. On success the command
|
|
prints the exact 'vaultik prune' invocation to run as a follow-up.
|
|
|
|
Use --local-only to skip the remote half (e.g. when you want to forget a
|
|
snapshot locally without touching the destination store).
|
|
|
|
If the remote is unreachable, the local-database removal still completes
|
|
and a warning is emitted; rerun 'vaultik prune' once the destination store
|
|
is reachable to finish remote cleanup.
|
|
|
|
To wipe the entire destination store and start over, use 'vaultik remote
|
|
nuke --force' — it is the single supported entry point for that.`,
|
|
Args: requireSnapshotIDArg,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runVaultikApp(cmd, opts.JSON, opts.JSON,
|
|
"Failed to remove snapshot",
|
|
func(v *vaultik.Vaultik) error {
|
|
_, err := v.RemoveSnapshot(args[0], opts)
|
|
|
|
return err
|
|
})
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Skip confirmation prompt")
|
|
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false,
|
|
"Show what would be removed without removing")
|
|
cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output result as JSON")
|
|
cmd.Flags().BoolVar(&opts.LocalOnly, "local-only", false,
|
|
"Skip remote cleanup; only touch the local index")
|
|
|
|
return cmd
|
|
}
|