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>
109 lines
2.2 KiB
Go
109 lines
2.2 KiB
Go
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
|
|
}
|