diff options
| author | bndw <ben@bdw.to> | 2026-01-23 21:39:19 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-01-23 21:39:19 -0800 |
| commit | 57eb67df265a7a6bb544cde83a3be5eadf53fdf2 (patch) | |
| tree | 55bfcf7f1d084ab7bd8a918dabfe2d4a772b5361 | |
| parent | 1694ba1b8ad68e2d2ad21c1442c29b6f3f2c1632 (diff) | |
Add VPS management commands: vps, vps-update, vps-ssh
- vps: Show server health (uptime, load, memory, disk, services)
- vps-update: Run apt update && upgrade with streaming output
- vps-ssh: Open interactive SSH session to default/specified host
| -rw-r--r-- | cmd/deploy/main.go | 15 | ||||
| -rw-r--r-- | cmd/deploy/vps.go | 229 |
2 files changed, 244 insertions, 0 deletions
diff --git a/cmd/deploy/main.go b/cmd/deploy/main.go index e86eff7..51439dd 100644 --- a/cmd/deploy/main.go +++ b/cmd/deploy/main.go | |||
| @@ -30,6 +30,12 @@ func main() { | |||
| 30 | runEnv(os.Args[2:]) | 30 | runEnv(os.Args[2:]) |
| 31 | case "webui": | 31 | case "webui": |
| 32 | runWebUI(os.Args[2:]) | 32 | runWebUI(os.Args[2:]) |
| 33 | case "vps": | ||
| 34 | runVPS(os.Args[2:]) | ||
| 35 | case "vps-update": | ||
| 36 | runUpdate(os.Args[2:]) | ||
| 37 | case "vps-ssh": | ||
| 38 | runSSH(os.Args[2:]) | ||
| 33 | case "help", "--help", "-h": | 39 | case "help", "--help", "-h": |
| 34 | printUsage() | 40 | printUsage() |
| 35 | default: | 41 | default: |
| @@ -53,6 +59,9 @@ COMMANDS: | |||
| 53 | status Check status of a deployment | 59 | status Check status of a deployment |
| 54 | restart Restart a deployment | 60 | restart Restart a deployment |
| 55 | env Manage environment variables | 61 | env Manage environment variables |
| 62 | vps Show VPS health (uptime, disk, memory, load) | ||
| 63 | vps-update Update VPS packages (apt update && upgrade) | ||
| 64 | vps-ssh Open an interactive SSH session | ||
| 56 | webui Launch web UI to manage deployments | 65 | webui Launch web UI to manage deployments |
| 57 | 66 | ||
| 58 | FLAGS: | 67 | FLAGS: |
| @@ -73,6 +82,12 @@ EXAMPLES: | |||
| 73 | 82 | ||
| 74 | # View logs | 83 | # View logs |
| 75 | deploy logs myapp | 84 | deploy logs myapp |
| 85 | |||
| 86 | # Check VPS health | ||
| 87 | deploy vps | ||
| 88 | |||
| 89 | # Update VPS packages | ||
| 90 | deploy vps-update | ||
| 76 | ` | 91 | ` |
| 77 | fmt.Fprint(os.Stderr, usage) | 92 | fmt.Fprint(os.Stderr, usage) |
| 78 | } | 93 | } |
diff --git a/cmd/deploy/vps.go b/cmd/deploy/vps.go new file mode 100644 index 0000000..bd6278b --- /dev/null +++ b/cmd/deploy/vps.go | |||
| @@ -0,0 +1,229 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "flag" | ||
| 5 | "fmt" | ||
| 6 | "os" | ||
| 7 | "os/exec" | ||
| 8 | |||
| 9 | "github.com/bdw/deploy/internal/ssh" | ||
| 10 | "github.com/bdw/deploy/internal/state" | ||
| 11 | ) | ||
| 12 | |||
| 13 | func runVPS(args []string) { | ||
| 14 | fs := flag.NewFlagSet("vps", flag.ExitOnError) | ||
| 15 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 16 | fs.Parse(args) | ||
| 17 | |||
| 18 | // Load state | ||
| 19 | st, err := state.Load() | ||
| 20 | if err != nil { | ||
| 21 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 22 | os.Exit(1) | ||
| 23 | } | ||
| 24 | |||
| 25 | // Get host from flag or state default | ||
| 26 | if *host == "" { | ||
| 27 | *host = st.GetDefaultHost() | ||
| 28 | } | ||
| 29 | |||
| 30 | if *host == "" { | ||
| 31 | fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") | ||
| 32 | fs.Usage() | ||
| 33 | os.Exit(1) | ||
| 34 | } | ||
| 35 | |||
| 36 | fmt.Printf("Connecting to %s...\n\n", *host) | ||
| 37 | |||
| 38 | // Connect to VPS | ||
| 39 | client, err := ssh.Connect(*host) | ||
| 40 | if err != nil { | ||
| 41 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 42 | os.Exit(1) | ||
| 43 | } | ||
| 44 | defer client.Close() | ||
| 45 | |||
| 46 | // Uptime | ||
| 47 | fmt.Println("UPTIME") | ||
| 48 | if output, err := client.Run("uptime -p"); err == nil { | ||
| 49 | fmt.Printf(" %s", output) | ||
| 50 | } | ||
| 51 | if output, err := client.Run("uptime -s"); err == nil { | ||
| 52 | fmt.Printf(" Since: %s", output) | ||
| 53 | } | ||
| 54 | fmt.Println() | ||
| 55 | |||
| 56 | // Load average | ||
| 57 | fmt.Println("LOAD") | ||
| 58 | if output, err := client.Run("cat /proc/loadavg | awk '{print $1, $2, $3}'"); err == nil { | ||
| 59 | fmt.Printf(" 1m, 5m, 15m: %s", output) | ||
| 60 | } | ||
| 61 | fmt.Println() | ||
| 62 | |||
| 63 | // Memory | ||
| 64 | fmt.Println("MEMORY") | ||
| 65 | if output, err := client.Run("free -h | awk 'NR==2 {print \" Used: \" $3 \" / \" $2}'"); err == nil { | ||
| 66 | fmt.Print(output) | ||
| 67 | } | ||
| 68 | if output, err := client.Run("free -h | awk 'NR==2 {printf \" Available: %s\\n\", $7}'"); err == nil { | ||
| 69 | fmt.Print(output) | ||
| 70 | } | ||
| 71 | fmt.Println() | ||
| 72 | |||
| 73 | // Disk | ||
| 74 | fmt.Println("DISK") | ||
| 75 | if output, err := client.Run("df -h / | awk 'NR==2 {print \" Used: \" $3 \" / \" $2 \" (\" $5 \")\"}'"); err == nil { | ||
| 76 | fmt.Print(output) | ||
| 77 | } | ||
| 78 | if output, err := client.Run("df -h / | awk 'NR==2 {print \" Available: \" $4}'"); err == nil { | ||
| 79 | fmt.Print(output) | ||
| 80 | } | ||
| 81 | fmt.Println() | ||
| 82 | |||
| 83 | // Updates available | ||
| 84 | fmt.Println("UPDATES") | ||
| 85 | if output, err := client.Run("[ -f /var/lib/update-notifier/updates-available ] && cat /var/lib/update-notifier/updates-available | head -2 || echo ' (update info not available)'"); err == nil { | ||
| 86 | fmt.Print(output) | ||
| 87 | } | ||
| 88 | fmt.Println() | ||
| 89 | |||
| 90 | // Services | ||
| 91 | fmt.Println("SERVICES") | ||
| 92 | if output, err := client.Run("systemctl is-active caddy 2>/dev/null && echo ' Caddy: active' || echo ' Caddy: inactive'"); err == nil { | ||
| 93 | // Parse the output | ||
| 94 | if output == "active\n" { | ||
| 95 | fmt.Println(" Caddy: active") | ||
| 96 | } else { | ||
| 97 | fmt.Println(" Caddy: inactive") | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | // Count deployed apps that are running | ||
| 102 | hostState := st.GetHost(*host) | ||
| 103 | if hostState != nil && len(hostState.Apps) > 0 { | ||
| 104 | activeCount := 0 | ||
| 105 | for name, app := range hostState.Apps { | ||
| 106 | if app.Type == "app" { | ||
| 107 | if output, err := client.Run(fmt.Sprintf("systemctl is-active %s 2>/dev/null", name)); err == nil && output == "active\n" { | ||
| 108 | activeCount++ | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | appCount := 0 | ||
| 113 | for _, app := range hostState.Apps { | ||
| 114 | if app.Type == "app" { | ||
| 115 | appCount++ | ||
| 116 | } | ||
| 117 | } | ||
| 118 | fmt.Printf(" Deployed apps: %d/%d active\n", activeCount, appCount) | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 122 | func runUpdate(args []string) { | ||
| 123 | fs := flag.NewFlagSet("vps-update", flag.ExitOnError) | ||
| 124 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 125 | yes := fs.Bool("y", false, "Skip confirmation prompt") | ||
| 126 | fs.Parse(args) | ||
| 127 | |||
| 128 | // Load state | ||
| 129 | st, err := state.Load() | ||
| 130 | if err != nil { | ||
| 131 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 132 | os.Exit(1) | ||
| 133 | } | ||
| 134 | |||
| 135 | // Get host from flag or state default | ||
| 136 | if *host == "" { | ||
| 137 | *host = st.GetDefaultHost() | ||
| 138 | } | ||
| 139 | |||
| 140 | if *host == "" { | ||
| 141 | fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") | ||
| 142 | fs.Usage() | ||
| 143 | os.Exit(1) | ||
| 144 | } | ||
| 145 | |||
| 146 | // Confirm unless -y flag is set | ||
| 147 | if !*yes { | ||
| 148 | fmt.Printf("This will run apt update && apt upgrade on %s\n", *host) | ||
| 149 | fmt.Print("Continue? [y/N]: ") | ||
| 150 | var response string | ||
| 151 | fmt.Scanln(&response) | ||
| 152 | if response != "y" && response != "Y" { | ||
| 153 | fmt.Println("Aborted.") | ||
| 154 | return | ||
| 155 | } | ||
| 156 | } | ||
| 157 | |||
| 158 | fmt.Printf("Connecting to %s...\n", *host) | ||
| 159 | |||
| 160 | // Connect to VPS | ||
| 161 | client, err := ssh.Connect(*host) | ||
| 162 | if err != nil { | ||
| 163 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 164 | os.Exit(1) | ||
| 165 | } | ||
| 166 | defer client.Close() | ||
| 167 | |||
| 168 | // Run apt update | ||
| 169 | fmt.Println("\nā Running apt update...") | ||
| 170 | if err := client.RunSudoStream("apt update"); err != nil { | ||
| 171 | fmt.Fprintf(os.Stderr, "Error running apt update: %v\n", err) | ||
| 172 | os.Exit(1) | ||
| 173 | } | ||
| 174 | |||
| 175 | // Run apt upgrade | ||
| 176 | fmt.Println("\nā Running apt upgrade...") | ||
| 177 | if err := client.RunSudoStream("DEBIAN_FRONTEND=noninteractive apt upgrade -y"); err != nil { | ||
| 178 | fmt.Fprintf(os.Stderr, "Error running apt upgrade: %v\n", err) | ||
| 179 | os.Exit(1) | ||
| 180 | } | ||
| 181 | |||
| 182 | // Check if reboot is required | ||
| 183 | fmt.Println() | ||
| 184 | if output, err := client.Run("[ -f /var/run/reboot-required ] && echo 'yes' || echo 'no'"); err == nil { | ||
| 185 | if output == "yes\n" { | ||
| 186 | fmt.Println("Note: A reboot is required to complete the update.") | ||
| 187 | } | ||
| 188 | } | ||
| 189 | |||
| 190 | fmt.Println("ā Update complete") | ||
| 191 | } | ||
| 192 | |||
| 193 | func runSSH(args []string) { | ||
| 194 | fs := flag.NewFlagSet("vps-ssh", flag.ExitOnError) | ||
| 195 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 196 | fs.Parse(args) | ||
| 197 | |||
| 198 | // Load state | ||
| 199 | st, err := state.Load() | ||
| 200 | if err != nil { | ||
| 201 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 202 | os.Exit(1) | ||
| 203 | } | ||
| 204 | |||
| 205 | // Get host from flag or state default | ||
| 206 | if *host == "" { | ||
| 207 | *host = st.GetDefaultHost() | ||
| 208 | } | ||
| 209 | |||
| 210 | if *host == "" { | ||
| 211 | fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") | ||
| 212 | fs.Usage() | ||
| 213 | os.Exit(1) | ||
| 214 | } | ||
| 215 | |||
| 216 | // Launch interactive SSH session | ||
| 217 | cmd := exec.Command("ssh", *host) | ||
| 218 | cmd.Stdin = os.Stdin | ||
| 219 | cmd.Stdout = os.Stdout | ||
| 220 | cmd.Stderr = os.Stderr | ||
| 221 | |||
| 222 | if err := cmd.Run(); err != nil { | ||
| 223 | if exitErr, ok := err.(*exec.ExitError); ok { | ||
| 224 | os.Exit(exitErr.ExitCode()) | ||
| 225 | } | ||
| 226 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| 227 | os.Exit(1) | ||
| 228 | } | ||
| 229 | } | ||
