Initial commit: Disaster recovery CLI tool
A Go-based CLI tool for recovering servers from backups to new cloud VMs. Features: - Multi-cloud support: Exoscale, Cloudscale, Hetzner Cloud - Backup sources: Local filesystem, Hetzner Storage Box - 6-stage restore pipeline with /etc whitelist protection - DNS migration with safety checks and auto-rollback - Dry-run by default, requires --yes to execute - Cloud-init for SSH key injection Packages: - cmd/recover-server: CLI commands (recover, migrate-dns, list, cleanup) - internal/providers: Cloud provider implementations - internal/backup: Backup source implementations - internal/restore: 6-stage restore pipeline - internal/dns: Exoscale DNS management - internal/ui: Prompts, progress, dry-run display - internal/config: Environment and host configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
108
internal/ui/dryrun.go
Normal file
108
internal/ui/dryrun.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DryRun tracks dry-run mode operations
|
||||
type DryRun struct {
|
||||
Enabled bool
|
||||
Operations []DryRunOp
|
||||
}
|
||||
|
||||
// DryRunOp represents a single operation that would be performed
|
||||
type DryRunOp struct {
|
||||
Component string // e.g., "VM", "DNS", "Restore"
|
||||
Action string // e.g., "Create", "Update", "Delete"
|
||||
Description string
|
||||
}
|
||||
|
||||
// NewDryRun creates a dry-run tracker
|
||||
func NewDryRun(enabled bool) *DryRun {
|
||||
return &DryRun{
|
||||
Enabled: enabled,
|
||||
Operations: make([]DryRunOp, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddOperation records an operation
|
||||
func (d *DryRun) AddOperation(component, action, description string) {
|
||||
d.Operations = append(d.Operations, DryRunOp{
|
||||
Component: component,
|
||||
Action: action,
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
// Print displays all recorded operations
|
||||
func (d *DryRun) Print() {
|
||||
if !d.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n" + HeaderLine("DRY RUN - No changes will be made"))
|
||||
fmt.Println()
|
||||
|
||||
if len(d.Operations) == 0 {
|
||||
fmt.Println("No operations would be performed.")
|
||||
return
|
||||
}
|
||||
|
||||
for i, op := range d.Operations {
|
||||
fmt.Printf("%d. [%s] %s: %s\n", i+1, op.Component, op.Action, op.Description)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("To execute these operations, run with --yes flag")
|
||||
}
|
||||
|
||||
// HeaderLine creates a formatted header
|
||||
func HeaderLine(title string) string {
|
||||
return fmt.Sprintf("=== %s ===", title)
|
||||
}
|
||||
|
||||
// TablePrint prints data as a simple table
|
||||
func TablePrint(headers []string, rows [][]string) {
|
||||
// Calculate column widths
|
||||
widths := make([]int, len(headers))
|
||||
for i, h := range headers {
|
||||
widths[i] = len(h)
|
||||
}
|
||||
for _, row := range rows {
|
||||
for i, cell := range row {
|
||||
if i < len(widths) && len(cell) > widths[i] {
|
||||
widths[i] = len(cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print header
|
||||
for i, h := range headers {
|
||||
fmt.Printf("%-*s ", widths[i], h)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Print separator
|
||||
for i := range headers {
|
||||
fmt.Printf("%s ", repeat("-", widths[i]))
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Print rows
|
||||
for _, row := range rows {
|
||||
for i, cell := range row {
|
||||
if i < len(widths) {
|
||||
fmt.Printf("%-*s ", widths[i], cell)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func repeat(s string, n int) string {
|
||||
result := ""
|
||||
for i := 0; i < n; i++ {
|
||||
result += s
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user