feat(P04): backup/restore — signed tarball (HMAC-SHA256)
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---
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user