diff options
| author | bndw <ben@bdw.to> | 2026-01-23 20:54:46 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-01-23 20:54:46 -0800 |
| commit | 98b9af372025595e8a4255538e2836e019311474 (patch) | |
| tree | 0a26fa5a8a19ea8565da6d63e1f19c21fc170d12 /cmd/deploy/manage.go | |
| parent | 7fcb9dfa87310e91b527829ece9989decb6fda64 (diff) | |
Add deploy command and fix static site naming
Static sites now default to using the domain as the name instead of
the source directory basename, preventing conflicts when multiple
sites use the same directory name (e.g., dist).
Also fixes .gitignore to not exclude cmd/deploy/ directory.
Diffstat (limited to 'cmd/deploy/manage.go')
| -rw-r--r-- | cmd/deploy/manage.go | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/cmd/deploy/manage.go b/cmd/deploy/manage.go new file mode 100644 index 0000000..3cee1f4 --- /dev/null +++ b/cmd/deploy/manage.go | |||
| @@ -0,0 +1,327 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "flag" | ||
| 5 | "fmt" | ||
| 6 | "os" | ||
| 7 | |||
| 8 | "github.com/bdw/deploy/internal/config" | ||
| 9 | "github.com/bdw/deploy/internal/ssh" | ||
| 10 | "github.com/bdw/deploy/internal/state" | ||
| 11 | ) | ||
| 12 | |||
| 13 | func runRemove(args []string) { | ||
| 14 | fs := flag.NewFlagSet("remove", flag.ExitOnError) | ||
| 15 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 16 | fs.Parse(args) | ||
| 17 | |||
| 18 | if len(fs.Args()) == 0 { | ||
| 19 | fmt.Fprintf(os.Stderr, "Error: app name is required\n") | ||
| 20 | fmt.Fprintf(os.Stderr, "Usage: deploy remove <app-name> --host user@vps-ip\n") | ||
| 21 | os.Exit(1) | ||
| 22 | } | ||
| 23 | |||
| 24 | name := fs.Args()[0] | ||
| 25 | |||
| 26 | // Get host from flag or config | ||
| 27 | if *host == "" { | ||
| 28 | cfg, err := config.Load() | ||
| 29 | if err != nil { | ||
| 30 | fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) | ||
| 31 | os.Exit(1) | ||
| 32 | } | ||
| 33 | *host = cfg.Host | ||
| 34 | } | ||
| 35 | |||
| 36 | if *host == "" { | ||
| 37 | fmt.Fprintf(os.Stderr, "Error: --host is required\n") | ||
| 38 | fs.Usage() | ||
| 39 | os.Exit(1) | ||
| 40 | } | ||
| 41 | |||
| 42 | // Load state | ||
| 43 | st, err := state.Load() | ||
| 44 | if err != nil { | ||
| 45 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 46 | os.Exit(1) | ||
| 47 | } | ||
| 48 | |||
| 49 | // Get app info | ||
| 50 | app, err := st.GetApp(*host, name) | ||
| 51 | if err != nil { | ||
| 52 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| 53 | os.Exit(1) | ||
| 54 | } | ||
| 55 | |||
| 56 | fmt.Printf("Removing deployment: %s\n", name) | ||
| 57 | |||
| 58 | // Connect to VPS | ||
| 59 | client, err := ssh.Connect(*host) | ||
| 60 | if err != nil { | ||
| 61 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 62 | os.Exit(1) | ||
| 63 | } | ||
| 64 | defer client.Close() | ||
| 65 | |||
| 66 | if app.Type == "app" { | ||
| 67 | // Stop and disable service | ||
| 68 | fmt.Println("→ Stopping service...") | ||
| 69 | client.RunSudo(fmt.Sprintf("systemctl stop %s", name)) | ||
| 70 | client.RunSudo(fmt.Sprintf("systemctl disable %s", name)) | ||
| 71 | |||
| 72 | // Remove systemd unit | ||
| 73 | client.RunSudo(fmt.Sprintf("rm -f /etc/systemd/system/%s.service", name)) | ||
| 74 | client.RunSudo("systemctl daemon-reload") | ||
| 75 | |||
| 76 | // Remove binary | ||
| 77 | client.RunSudo(fmt.Sprintf("rm -f /usr/local/bin/%s", name)) | ||
| 78 | |||
| 79 | // Remove working directory | ||
| 80 | client.RunSudo(fmt.Sprintf("rm -rf /var/lib/%s", name)) | ||
| 81 | |||
| 82 | // Remove env file | ||
| 83 | client.RunSudo(fmt.Sprintf("rm -f /etc/deploy/env/%s.env", name)) | ||
| 84 | |||
| 85 | // Remove user | ||
| 86 | client.RunSudo(fmt.Sprintf("userdel %s", name)) | ||
| 87 | } else { | ||
| 88 | // Remove static site files | ||
| 89 | fmt.Println("→ Removing files...") | ||
| 90 | client.RunSudo(fmt.Sprintf("rm -rf /var/www/%s", name)) | ||
| 91 | } | ||
| 92 | |||
| 93 | // Remove Caddy config | ||
| 94 | fmt.Println("→ Removing Caddy config...") | ||
| 95 | client.RunSudo(fmt.Sprintf("rm -f /etc/caddy/sites-enabled/%s.caddy", name)) | ||
| 96 | |||
| 97 | // Reload Caddy | ||
| 98 | fmt.Println("→ Reloading Caddy...") | ||
| 99 | if _, err := client.RunSudo("systemctl reload caddy"); err != nil { | ||
| 100 | fmt.Fprintf(os.Stderr, "Warning: Error reloading Caddy: %v\n", err) | ||
| 101 | } | ||
| 102 | |||
| 103 | // Update state | ||
| 104 | if err := st.RemoveApp(*host, name); err != nil { | ||
| 105 | fmt.Fprintf(os.Stderr, "Error updating state: %v\n", err) | ||
| 106 | os.Exit(1) | ||
| 107 | } | ||
| 108 | if err := st.Save(); err != nil { | ||
| 109 | fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err) | ||
| 110 | os.Exit(1) | ||
| 111 | } | ||
| 112 | |||
| 113 | fmt.Printf("✓ Deployment removed successfully\n") | ||
| 114 | } | ||
| 115 | |||
| 116 | func runLogs(args []string) { | ||
| 117 | fs := flag.NewFlagSet("logs", flag.ExitOnError) | ||
| 118 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 119 | follow := fs.Bool("f", false, "Follow logs") | ||
| 120 | lines := fs.Int("n", 50, "Number of lines to show") | ||
| 121 | fs.Parse(args) | ||
| 122 | |||
| 123 | if len(fs.Args()) == 0 { | ||
| 124 | fmt.Fprintf(os.Stderr, "Error: app name is required\n") | ||
| 125 | fmt.Fprintf(os.Stderr, "Usage: deploy logs <app-name> --host user@vps-ip\n") | ||
| 126 | os.Exit(1) | ||
| 127 | } | ||
| 128 | |||
| 129 | name := fs.Args()[0] | ||
| 130 | |||
| 131 | // Get host from flag or config | ||
| 132 | if *host == "" { | ||
| 133 | cfg, err := config.Load() | ||
| 134 | if err != nil { | ||
| 135 | fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) | ||
| 136 | os.Exit(1) | ||
| 137 | } | ||
| 138 | *host = cfg.Host | ||
| 139 | } | ||
| 140 | |||
| 141 | if *host == "" { | ||
| 142 | fmt.Fprintf(os.Stderr, "Error: --host is required\n") | ||
| 143 | fs.Usage() | ||
| 144 | os.Exit(1) | ||
| 145 | } | ||
| 146 | |||
| 147 | // Load state to verify app exists | ||
| 148 | st, err := state.Load() | ||
| 149 | if err != nil { | ||
| 150 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 151 | os.Exit(1) | ||
| 152 | } | ||
| 153 | |||
| 154 | app, err := st.GetApp(*host, name) | ||
| 155 | if err != nil { | ||
| 156 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| 157 | os.Exit(1) | ||
| 158 | } | ||
| 159 | |||
| 160 | if app.Type != "app" { | ||
| 161 | fmt.Fprintf(os.Stderr, "Error: logs are only available for apps, not static sites\n") | ||
| 162 | os.Exit(1) | ||
| 163 | } | ||
| 164 | |||
| 165 | // Connect to VPS | ||
| 166 | client, err := ssh.Connect(*host) | ||
| 167 | if err != nil { | ||
| 168 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 169 | os.Exit(1) | ||
| 170 | } | ||
| 171 | defer client.Close() | ||
| 172 | |||
| 173 | // Build journalctl command | ||
| 174 | cmd := fmt.Sprintf("journalctl -u %s -n %d", name, *lines) | ||
| 175 | if *follow { | ||
| 176 | cmd += " -f" | ||
| 177 | } | ||
| 178 | |||
| 179 | // Run command | ||
| 180 | if *follow { | ||
| 181 | // Stream output for follow mode (no sudo needed for journalctl) | ||
| 182 | if err := client.RunStream(cmd); err != nil { | ||
| 183 | fmt.Fprintf(os.Stderr, "Error fetching logs: %v\n", err) | ||
| 184 | os.Exit(1) | ||
| 185 | } | ||
| 186 | } else { | ||
| 187 | // Buffer output for non-follow mode (no sudo needed for journalctl) | ||
| 188 | output, err := client.Run(cmd) | ||
| 189 | if err != nil { | ||
| 190 | fmt.Fprintf(os.Stderr, "Error fetching logs: %v\n", err) | ||
| 191 | os.Exit(1) | ||
| 192 | } | ||
| 193 | fmt.Print(output) | ||
| 194 | } | ||
| 195 | } | ||
| 196 | |||
| 197 | func runStatus(args []string) { | ||
| 198 | fs := flag.NewFlagSet("status", flag.ExitOnError) | ||
| 199 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 200 | fs.Parse(args) | ||
| 201 | |||
| 202 | if len(fs.Args()) == 0 { | ||
| 203 | fmt.Fprintf(os.Stderr, "Error: app name is required\n") | ||
| 204 | fmt.Fprintf(os.Stderr, "Usage: deploy status <app-name> --host user@vps-ip\n") | ||
| 205 | os.Exit(1) | ||
| 206 | } | ||
| 207 | |||
| 208 | name := fs.Args()[0] | ||
| 209 | |||
| 210 | // Get host from flag or config | ||
| 211 | if *host == "" { | ||
| 212 | cfg, err := config.Load() | ||
| 213 | if err != nil { | ||
| 214 | fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) | ||
| 215 | os.Exit(1) | ||
| 216 | } | ||
| 217 | *host = cfg.Host | ||
| 218 | } | ||
| 219 | |||
| 220 | if *host == "" { | ||
| 221 | fmt.Fprintf(os.Stderr, "Error: --host is required\n") | ||
| 222 | fs.Usage() | ||
| 223 | os.Exit(1) | ||
| 224 | } | ||
| 225 | |||
| 226 | // Load state to verify app exists | ||
| 227 | st, err := state.Load() | ||
| 228 | if err != nil { | ||
| 229 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 230 | os.Exit(1) | ||
| 231 | } | ||
| 232 | |||
| 233 | app, err := st.GetApp(*host, name) | ||
| 234 | if err != nil { | ||
| 235 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| 236 | os.Exit(1) | ||
| 237 | } | ||
| 238 | |||
| 239 | if app.Type != "app" { | ||
| 240 | fmt.Fprintf(os.Stderr, "Error: status is only available for apps, not static sites\n") | ||
| 241 | os.Exit(1) | ||
| 242 | } | ||
| 243 | |||
| 244 | // Connect to VPS | ||
| 245 | client, err := ssh.Connect(*host) | ||
| 246 | if err != nil { | ||
| 247 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 248 | os.Exit(1) | ||
| 249 | } | ||
| 250 | defer client.Close() | ||
| 251 | |||
| 252 | // Get status | ||
| 253 | output, err := client.RunSudo(fmt.Sprintf("systemctl status %s", name)) | ||
| 254 | if err != nil { | ||
| 255 | // systemctl status returns non-zero for non-active services | ||
| 256 | // but we still want to show the output | ||
| 257 | fmt.Print(output) | ||
| 258 | return | ||
| 259 | } | ||
| 260 | |||
| 261 | fmt.Print(output) | ||
| 262 | } | ||
| 263 | |||
| 264 | func runRestart(args []string) { | ||
| 265 | fs := flag.NewFlagSet("restart", flag.ExitOnError) | ||
| 266 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 267 | fs.Parse(args) | ||
| 268 | |||
| 269 | if len(fs.Args()) == 0 { | ||
| 270 | fmt.Fprintf(os.Stderr, "Error: app name is required\n") | ||
| 271 | fmt.Fprintf(os.Stderr, "Usage: deploy restart <app-name> --host user@vps-ip\n") | ||
| 272 | os.Exit(1) | ||
| 273 | } | ||
| 274 | |||
| 275 | name := fs.Args()[0] | ||
| 276 | |||
| 277 | // Get host from flag or config | ||
| 278 | if *host == "" { | ||
| 279 | cfg, err := config.Load() | ||
| 280 | if err != nil { | ||
| 281 | fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) | ||
| 282 | os.Exit(1) | ||
| 283 | } | ||
| 284 | *host = cfg.Host | ||
| 285 | } | ||
| 286 | |||
| 287 | if *host == "" { | ||
| 288 | fmt.Fprintf(os.Stderr, "Error: --host is required\n") | ||
| 289 | fs.Usage() | ||
| 290 | os.Exit(1) | ||
| 291 | } | ||
| 292 | |||
| 293 | // Load state to verify app exists | ||
| 294 | st, err := state.Load() | ||
| 295 | if err != nil { | ||
| 296 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 297 | os.Exit(1) | ||
| 298 | } | ||
| 299 | |||
| 300 | app, err := st.GetApp(*host, name) | ||
| 301 | if err != nil { | ||
| 302 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| 303 | os.Exit(1) | ||
| 304 | } | ||
| 305 | |||
| 306 | if app.Type != "app" { | ||
| 307 | fmt.Fprintf(os.Stderr, "Error: restart is only available for apps, not static sites\n") | ||
| 308 | os.Exit(1) | ||
| 309 | } | ||
| 310 | |||
| 311 | // Connect to VPS | ||
| 312 | client, err := ssh.Connect(*host) | ||
| 313 | if err != nil { | ||
| 314 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 315 | os.Exit(1) | ||
| 316 | } | ||
| 317 | defer client.Close() | ||
| 318 | |||
| 319 | // Restart service | ||
| 320 | fmt.Printf("Restarting %s...\n", name) | ||
| 321 | if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { | ||
| 322 | fmt.Fprintf(os.Stderr, "Error restarting service: %v\n", err) | ||
| 323 | os.Exit(1) | ||
| 324 | } | ||
| 325 | |||
| 326 | fmt.Println("✓ Service restarted successfully") | ||
| 327 | } | ||
