package config import ( "fmt" "os" "time" "github.com/spf13/cobra" ) // Config holds all runtime configuration for ha-sync. type Config struct { Src string Dest string Pair string Direction string DBDSN string LockTTL int LogDir string LogRetainDays int MtimeThreshold time.Duration DeleteMissing bool Workers int DryRun bool Verbose bool KubeNamespace string Excludes []string // glob patterns passed via --exclude flags SrcHost string DestHost string } // NewRootCmd returns a configured cobra root command with all flags bound to a // Config. Call FromFlags(cmd) after Execute() to obtain a validated *Config. func NewRootCmd() *cobra.Command { cfg := &Config{} cmd := &cobra.Command{ Use: "ha-sync", Short: "Bidirectional HA file-sync with leader-election and audit logging", Long: `ha-sync copies files from a source directory to a destination directory, ensuring only one instance runs per sync pair at a time via a Kubernetes Lease. Each run is recorded in MySQL with per-file operation details. Use --dry-run to preview what would change without writing any files. Examples: # Sync /data/media from Dell to HP (production) ha-sync --src=/data/media --dest=/mnt/hp/media --pair=media --direction=dell-to-hp # Dry-run first to preview changes ha-sync --src=/data/media --dest=/mnt/hp/media --pair=media --direction=dell-to-hp --dry-run # Exclude temporary and lock files ha-sync --src=/data/infra --dest=/mnt/hp/infra --pair=infra --direction=dell-to-hp \ --exclude='*.sock' --exclude='*.lock' --exclude='*.pid'`, } f := cmd.Flags() f.StringVar(&cfg.Src, "src", "", "Source directory path (required)") f.StringVar(&cfg.Dest, "dest", "", "Destination directory path (required)") f.StringVar(&cfg.Pair, "pair", "", "Unique name for this sync pair; used as the Kubernetes Lease name ha-sync- (required)") f.StringVar(&cfg.Direction, "direction", "fwd", "Label stored in the audit log identifying sync direction (e.g. dell-to-hp, hp-to-dell)") f.StringVar(&cfg.DBDSN, "db-dsn", "", "MySQL DSN for audit logging. Defaults to env HA_SYNC_DB_DSN (format: user:pass@tcp(host:port)/db?parseTime=true)") f.IntVar(&cfg.LockTTL, "lock-ttl", 3600, "Kubernetes Lease TTL in seconds; a crashed pod releases the lock after this duration") f.StringVar(&cfg.LogDir, "log-dir", "/var/log/ha-sync", "Directory for file-based operation logs") f.IntVar(&cfg.LogRetainDays, "log-retain-days", 10, "Number of days to retain log files before rotation") f.DurationVar(&cfg.MtimeThreshold, "mtime-threshold", 2*time.Second, "Minimum mtime difference required to treat a same-size file as changed (avoids NFS clock skew false positives)") f.BoolVar(&cfg.DeleteMissing, "delete-missing", false, "Delete files in dest that no longer exist in src (use with caution)") f.IntVar(&cfg.Workers, "workers", 4, "Number of concurrent file-copy workers") f.BoolVar(&cfg.DryRun, "dry-run", false, "Preview what would be synced without writing any files; results are recorded in the DB with dry_run=true") f.BoolVar(&cfg.Verbose, "verbose", false, "Log every file operation (create/update/skip); default logs only errors and summary") f.StringVar(&cfg.KubeNamespace, "kube-namespace", "infrastructure", "Kubernetes namespace where the Lease object is managed") f.StringArrayVar(&cfg.Excludes, "exclude", nil, "Glob pattern to skip (may be repeated). Matched against the file's base name and its path relative to src. Supports * and ? wildcards. Example: --exclude='*.sock' --exclude='tmp/*'") f.StringVar(&cfg.SrcHost, "src-host", "dell", "Label for the source host (stored in audit log, e.g. dell, hp)") f.StringVar(&cfg.DestHost, "dest-host", "hp", "Label for the destination host (stored in audit log, e.g. dell, hp)") return cmd } // FromFlags reads flag values from cmd and returns a validated Config. // It returns an error if any required field is missing. func FromFlags(cmd *cobra.Command) (*Config, error) { f := cmd.Flags() src, _ := f.GetString("src") dest, _ := f.GetString("dest") pair, _ := f.GetString("pair") direction, _ := f.GetString("direction") dbDSN, _ := f.GetString("db-dsn") lockTTL, _ := f.GetInt("lock-ttl") logDir, _ := f.GetString("log-dir") logRetainDays, _ := f.GetInt("log-retain-days") mtimeThreshold, _ := f.GetDuration("mtime-threshold") deleteMissing, _ := f.GetBool("delete-missing") workers, _ := f.GetInt("workers") dryRun, _ := f.GetBool("dry-run") verbose, _ := f.GetBool("verbose") kubeNamespace, _ := f.GetString("kube-namespace") excludes, _ := f.GetStringArray("exclude") srcHost, _ := f.GetString("src-host") destHost, _ := f.GetString("dest-host") if src == "" { return nil, fmt.Errorf("--src is required") } if dest == "" { return nil, fmt.Errorf("--dest is required") } if pair == "" { return nil, fmt.Errorf("--pair is required") } // Fall back to env if the flag was left at its default empty value. if dbDSN == "" { dbDSN = os.Getenv("HA_SYNC_DB_DSN") } if dbDSN == "" { return nil, fmt.Errorf("--db-dsn or env HA_SYNC_DB_DSN is required") } return &Config{ Src: src, Dest: dest, Pair: pair, Direction: direction, DBDSN: dbDSN, LockTTL: lockTTL, LogDir: logDir, LogRetainDays: logRetainDays, MtimeThreshold: mtimeThreshold, DeleteMissing: deleteMissing, Workers: workers, DryRun: dryRun, Verbose: verbose, KubeNamespace: kubeNamespace, Excludes: excludes, SrcHost: srcHost, DestHost: destHost, }, nil }