orama/pkg/cli/sandbox/destroy.go

123 lines
3.1 KiB
Go

package sandbox
import (
"bufio"
"fmt"
"os"
"strings"
"sync"
)
// Destroy tears down a sandbox cluster.
func Destroy(name string, force bool) error {
cfg, err := LoadConfig()
if err != nil {
return err
}
// Resolve sandbox name
state, err := resolveSandbox(name)
if err != nil {
return err
}
// Confirm destruction
if !force {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("Destroy sandbox %q? This deletes %d servers. [y/N]: ", state.Name, len(state.Servers))
choice, _ := reader.ReadString('\n')
choice = strings.TrimSpace(strings.ToLower(choice))
if choice != "y" && choice != "yes" {
fmt.Println("Aborted.")
return nil
}
}
state.Status = StatusDestroying
SaveState(state) // best-effort status update
client := NewHetznerClient(cfg.HetznerAPIToken)
// Step 1: Unassign floating IPs from nameserver nodes
fmt.Println("Unassigning floating IPs...")
for _, srv := range state.NameserverNodes() {
if srv.FloatingIP == "" {
continue
}
// Find the floating IP ID from config
for _, fip := range cfg.FloatingIPs {
if fip.IP == srv.FloatingIP {
if err := client.UnassignFloatingIP(fip.ID); err != nil {
fmt.Fprintf(os.Stderr, " Warning: could not unassign floating IP %s: %v\n", fip.IP, err)
} else {
fmt.Printf(" Unassigned %s from %s\n", fip.IP, srv.Name)
}
break
}
}
}
// Step 2: Delete all servers in parallel
fmt.Printf("Deleting %d servers...\n", len(state.Servers))
var wg sync.WaitGroup
var mu sync.Mutex
var failed []string
for _, srv := range state.Servers {
wg.Add(1)
go func(srv ServerState) {
defer wg.Done()
if err := client.DeleteServer(srv.ID); err != nil {
// Treat 404 as already deleted (idempotent)
if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") {
fmt.Printf(" %s (ID %d): already deleted\n", srv.Name, srv.ID)
} else {
mu.Lock()
failed = append(failed, fmt.Sprintf("%s (ID %d): %v", srv.Name, srv.ID, err))
mu.Unlock()
fmt.Fprintf(os.Stderr, " Warning: failed to delete %s: %v\n", srv.Name, err)
}
} else {
fmt.Printf(" Deleted %s (ID %d)\n", srv.Name, srv.ID)
}
}(srv)
}
wg.Wait()
if len(failed) > 0 {
fmt.Fprintf(os.Stderr, "\nFailed to delete %d server(s):\n", len(failed))
for _, f := range failed {
fmt.Fprintf(os.Stderr, " %s\n", f)
}
fmt.Fprintf(os.Stderr, "\nManual cleanup: delete servers at https://console.hetzner.cloud\n")
state.Status = StatusError
SaveState(state)
return fmt.Errorf("failed to delete %d server(s)", len(failed))
}
// Step 3: Remove state file
if err := DeleteState(state.Name); err != nil {
return fmt.Errorf("delete state: %w", err)
}
fmt.Printf("\nSandbox %q destroyed (%d servers deleted)\n", state.Name, len(state.Servers))
return nil
}
// resolveSandbox finds a sandbox by name or returns the active one.
func resolveSandbox(name string) (*SandboxState, error) {
if name != "" {
return LoadState(name)
}
// Find the active sandbox
active, err := FindActiveSandbox()
if err != nil {
return nil, err
}
if active == nil {
return nil, fmt.Errorf("no active sandbox found, specify --name")
}
return active, nil
}