f61ef2aa9e
internal/backup/backup.go: Backup (tar.gz + HMAC-SHA256 signature, excludes /run/orca + sockets + WAL/SHM), VerifySignature, Restore (signature verify + extract + Force flag). internal/cli/backup.go: orca backup --out + orca restore --in --force. Tests: round-trip, signature mismatch, exclusion, force-refuse, force-overwrite. ---ci--- project: orca phase: 04 milestone: v0.11 status: execute ---/ci---
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
// Package cli: backup.go implements the `orca backup` and `orca restore`
|
|
// subcommands (P04, v0.11 milestone).
|
|
//
|
|
// orca backup --out <path> — create a signed tar.gz of ORCA_HOME
|
|
// orca restore --in <path> — restore a verified backup
|
|
//
|
|
// `backup` reads the master key at paths.MasterKeyPath() and backs up
|
|
// paths.Root() (ORCA_HOME). The tarball + HMAC-SHA256 signature are
|
|
// written to --out and --out+".sig". `restore` verifies the signature
|
|
// before extracting; with --force it overwrites a non-empty target.
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/backup"
|
|
"git.cloudinit.dev/coreci/orca/internal/paths"
|
|
"git.cloudinit.dev/coreci/orca/internal/secrets"
|
|
)
|
|
|
|
var (
|
|
backupOutPath string
|
|
restoreInPath string
|
|
restoreTargetDir string
|
|
restoreForce bool
|
|
)
|
|
|
|
var backupCmd = &cobra.Command{
|
|
Use: "backup",
|
|
Short: "Create a signed tar.gz backup of ORCA_HOME",
|
|
Long: `Create a signed tar.gz backup of ORCA_HOME (P04).
|
|
|
|
Walks ` + "`ORCA_HOME`" + ` recursively, excludes /run/orca/*, *.sock,
|
|
*.db-wal, *.db-shm, packs the rest into a tar.gz, and computes an
|
|
HMAC-SHA256 signature using the cluster master key. The tarball is
|
|
written to --out; the hex-encoded signature to --out + ".sig".`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
mk, err := secrets.LoadMasterKey(paths.MasterKeyPath())
|
|
if err != nil {
|
|
return fmt.Errorf("load master key: %w", err)
|
|
}
|
|
out := backupOutPath
|
|
if out == "" {
|
|
ts := time.Now().UTC().Format("20060102-150405")
|
|
out = fmt.Sprintf("orca-backup-%s.tar.gz", ts)
|
|
}
|
|
opts := backup.BackupOptions{
|
|
SourceDir: paths.Root(),
|
|
OutputPath: out,
|
|
MasterKey: mk,
|
|
}
|
|
if err := backup.Backup(opts); err != nil {
|
|
return err
|
|
}
|
|
if jsonOutput {
|
|
return printJSON(map[string]string{
|
|
"path": out,
|
|
"sig": out + ".sig",
|
|
})
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "✓ Backup written: %s (sig: %s)\n", out, out+".sig")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var restoreCmd = &cobra.Command{
|
|
Use: "restore",
|
|
Short: "Restore ORCA_HOME from a verified signed backup",
|
|
Long: `Restore ORCA_HOME from a verified signed backup (P04).
|
|
|
|
Verifies the HMAC-SHA256 signature on --in (using the cluster master
|
|
key) before extracting. With --force, overwrites a non-empty target;
|
|
without it, refuses to clobber an existing ORCA_HOME.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if restoreInPath == "" {
|
|
return fmt.Errorf("--in is required")
|
|
}
|
|
mk, err := secrets.LoadMasterKey(paths.MasterKeyPath())
|
|
if err != nil {
|
|
return fmt.Errorf("load master key: %w", err)
|
|
}
|
|
target := restoreTargetDir
|
|
if target == "" {
|
|
target = paths.Root()
|
|
}
|
|
opts := backup.RestoreOptions{
|
|
InputPath: restoreInPath,
|
|
TargetDir: target,
|
|
MasterKey: mk,
|
|
Force: restoreForce,
|
|
}
|
|
if err := backup.Restore(opts); err != nil {
|
|
return err
|
|
}
|
|
if jsonOutput {
|
|
return printJSON(map[string]string{
|
|
"path": restoreInPath,
|
|
"target": target,
|
|
})
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "✓ Restored to: %s\n", target)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
backupCmd.Flags().StringVar(&backupOutPath, "out", "", "output tarball path (default: orca-backup-<timestamp>.tar.gz in CWD)")
|
|
restoreCmd.Flags().StringVar(&restoreInPath, "in", "", "input tarball path (required)")
|
|
restoreCmd.Flags().StringVar(&restoreTargetDir, "target", "", "restore target dir (default: ORCA_HOME)")
|
|
restoreCmd.Flags().BoolVar(&restoreForce, "force", false, "overwrite a non-empty target directory")
|
|
rootCmd.AddCommand(backupCmd)
|
|
rootCmd.AddCommand(restoreCmd)
|
|
}
|