check / check (push) Waiting to run
snapshot verify --help now says --deep needs the age private key, from VAULTIK_AGE_SECRET_KEY or age_secret_key, as snapshot restore does. Both now name the config key as well as the environment variable. The help of snapshot create, list, purge, remove, prune, remote info and remote nuke says which access to the destination it needs. config.example.yml gains age_secret_key, and no longer tells the reader to pipe age-keygen into grep, which discards the private key. Only help strings and comments change. Model: fable-5-1
180 lines
5.8 KiB
Go
180 lines
5.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/adrg/xdg"
|
|
"github.com/spf13/cobra"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
)
|
|
|
|
// errConfigNotFound is wrapped by all config-resolution failures.
|
|
var errConfigNotFound = errors.New("config file not found")
|
|
|
|
// RootFlags holds global flags that apply to all commands.
|
|
// These flags are defined on the root command and inherited by all subcommands.
|
|
type RootFlags struct {
|
|
ConfigPath string
|
|
Verbose bool
|
|
Debug bool
|
|
Quiet bool
|
|
SkipErrors bool
|
|
}
|
|
|
|
//nolint:gochecknoglobals // cobra persistent flags bind to package state
|
|
var rootFlags RootFlags
|
|
|
|
// NewRootCommand creates the root cobra command for the vaultik CLI.
|
|
// It sets up the command structure, global flags, and adds all subcommands.
|
|
// This is the main entry point for the CLI command hierarchy.
|
|
func NewRootCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "vaultik",
|
|
Short: "Secure incremental backup tool with asymmetric encryption",
|
|
Long: `vaultik is an incremental backup tool that encrypts data to age public
|
|
keys and uploads it to an S3-compatible object store, a local directory or
|
|
an rclone remote. The backed-up machine needs no private key: only
|
|
'snapshot restore' and 'snapshot verify --deep' need one.`,
|
|
SilenceUsage: true,
|
|
// Bare 'vaultik' (no subcommand): print help. The banner is
|
|
// printed once at process startup by Entry, before cobra
|
|
// parses arguments, so it appears even when cobra rejects
|
|
// args (e.g. "requires at least 2 arg(s)") and on --help.
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
_ = cmd.Help()
|
|
},
|
|
}
|
|
|
|
// Add global flags
|
|
cmd.PersistentFlags().StringVar(&rootFlags.ConfigPath, "config", "",
|
|
"Path to config file (default: $VAULTIK_CONFIG, then the platform "+
|
|
"config directory, then /etc/vaultik/config.yml)")
|
|
cmd.PersistentFlags().BoolVarP(&rootFlags.Verbose, "verbose", "v", false,
|
|
"Enable verbose output")
|
|
cmd.PersistentFlags().BoolVar(&rootFlags.Debug, "debug", false,
|
|
"Enable debug output")
|
|
cmd.PersistentFlags().BoolVarP(&rootFlags.Quiet, "quiet", "q", false,
|
|
"Suppress status output (warnings, errors, JSON documents, "+
|
|
"'config get' values and confirmation prompts are still printed)")
|
|
cmd.PersistentFlags().BoolVar(&rootFlags.SkipErrors, "skip-errors", false,
|
|
"Skip files that cannot be read when creating a snapshot, or "+
|
|
"that cannot be restored when restoring, instead of aborting "+
|
|
"(packing and storage errors while creating a snapshot still "+
|
|
"abort)")
|
|
|
|
// Add subcommands
|
|
cmd.AddCommand(
|
|
NewConfigCommand(),
|
|
NewPruneCommand(),
|
|
NewSnapshotCommand(),
|
|
NewInfoCommand(),
|
|
NewVersionCommand(),
|
|
NewRemoteCommand(),
|
|
NewDatabaseCommand(),
|
|
)
|
|
|
|
// cobra adds the completion command only when Execute runs, and with
|
|
// no Run, so it skipped its own cobra.NoArgs and a mistyped shell name
|
|
// printed help and exited 0. Adding it here lets it have a Run. Its
|
|
// scripts go to os.Stdout as it is now: a later SetOut does not
|
|
// redirect them.
|
|
cmd.InitDefaultCompletionCmd()
|
|
|
|
for _, sub := range cmd.Commands() {
|
|
if sub.Name() == "completion" {
|
|
sub.Run = func(completion *cobra.Command, _ []string) {
|
|
_ = completion.Help()
|
|
}
|
|
}
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
// GetRootFlags returns the global flags that were parsed from the command line.
|
|
// This allows subcommands to access global flag values like verbosity and config path.
|
|
func GetRootFlags() RootFlags {
|
|
return rootFlags
|
|
}
|
|
|
|
// commandUI returns a UI writer for a command's stdout, in quiet mode
|
|
// when the global --quiet flag is set. This is how the pure-cli
|
|
// commands (version, config, database) reach internal/ui: color follows
|
|
// the writer (a TTY gets color, a captured test buffer does not), and
|
|
// --quiet silences the same message classes it silences everywhere else.
|
|
func commandUI(cmd *cobra.Command) *ui.Writer {
|
|
w := ui.New(cmd.OutOrStdout())
|
|
if GetRootFlags().Quiet {
|
|
w.SetQuiet(true)
|
|
}
|
|
|
|
return w
|
|
}
|
|
|
|
// ResolveConfigPath resolves the config file path from flags, environment, or default.
|
|
// Search order: --config flag, VAULTIK_CONFIG env, XDG config dir,
|
|
// /etc/vaultik/config.yml.
|
|
// Explicit paths from --config and $VAULTIK_CONFIG are checked for existence
|
|
// so the user gets a clear error instead of a downstream YAML parser failure.
|
|
func ResolveConfigPath() (string, error) {
|
|
if path := rootFlags.ConfigPath; path != "" {
|
|
_, err := os.Stat(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"%w: from --config: %s (run 'vaultik config init --config %s' to create it)",
|
|
errConfigNotFound, path, path)
|
|
}
|
|
|
|
return path, nil
|
|
}
|
|
|
|
if path := os.Getenv("VAULTIK_CONFIG"); path != "" {
|
|
_, err := os.Stat(path) //nolint:gosec // G703: path is operator-supplied by design
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"%w: from $VAULTIK_CONFIG: %s (unset VAULTIK_CONFIG, point it at "+
|
|
"an existing file, or run 'vaultik config init')",
|
|
errConfigNotFound, path)
|
|
}
|
|
|
|
return path, nil
|
|
}
|
|
|
|
for _, path := range defaultConfigPaths() {
|
|
_, err := os.Stat(path)
|
|
if err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf(
|
|
"%w: searched %s (run 'vaultik config init' to create the default "+
|
|
"config, or pass --config <path>)",
|
|
errConfigNotFound, strings.Join(defaultConfigPaths(), " or "))
|
|
}
|
|
|
|
// defaultConfigPaths returns the ordered list of config paths to search.
|
|
// On macOS: ~/Library/Application Support/vaultik/config.yml
|
|
// On Linux: ~/.config/vaultik/config.yml
|
|
// Fallback: /etc/vaultik/config.yml
|
|
func defaultConfigPaths() []string {
|
|
return []string{
|
|
filepath.Join(xdg.ConfigHome, "vaultik", "config.yml"),
|
|
"/etc/vaultik/config.yml",
|
|
}
|
|
}
|
|
|
|
// DefaultConfigPath returns the platform-appropriate default config path.
|
|
// Used by the init command and in help text.
|
|
func DefaultConfigPath() string {
|
|
if os.Getuid() == 0 {
|
|
return "/etc/vaultik/config.yml"
|
|
}
|
|
|
|
return filepath.Join(xdg.ConfigHome, "vaultik", "config.yml")
|
|
}
|