summaryrefslogtreecommitdiffstats
path: root/cmd/ship/root.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/ship/root.go')
-rw-r--r--cmd/ship/root.go138
1 files changed, 134 insertions, 4 deletions
diff --git a/cmd/ship/root.go b/cmd/ship/root.go
index 81d33d1..d63772b 100644
--- a/cmd/ship/root.go
+++ b/cmd/ship/root.go
@@ -27,6 +27,8 @@ func runDeploy(cmd *cobra.Command, args []string) error {
27 envFile, _ := flags.GetString("env-file") 27 envFile, _ := flags.GetString("env-file")
28 binaryArgs, _ := flags.GetString("args") 28 binaryArgs, _ := flags.GetString("args")
29 files, _ := flags.GetStringArray("file") 29 files, _ := flags.GetStringArray("file")
30 memory, _ := flags.GetString("memory")
31 cpu, _ := flags.GetString("cpu")
30 32
31 // Get host from flag or state default 33 // Get host from flag or state default
32 host := hostFlag 34 host := hostFlag
@@ -39,7 +41,7 @@ func runDeploy(cmd *cobra.Command, args []string) error {
39 } 41 }
40 42
41 // If no flags provided, show help 43 // If no flags provided, show help
42 if domain == "" && binary == "" && !static { 44 if domain == "" && binary == "" && !static && name == "" {
43 return cmd.Help() 45 return cmd.Help()
44 } 46 }
45 47
@@ -54,6 +56,11 @@ func runDeploy(cmd *cobra.Command, args []string) error {
54 } 56 }
55 hostState := st.GetHost(host) 57 hostState := st.GetHost(host)
56 58
59 // Config update mode: --name provided without --binary or --static
60 if name != "" && binary == "" && !static {
61 return updateAppConfig(host, name, envVars, envFile, binaryArgs, memory, cpu)
62 }
63
57 if domain == "" && hostState.BaseDomain == "" { 64 if domain == "" && hostState.BaseDomain == "" {
58 return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')") 65 return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')")
59 } 66 }
@@ -84,10 +91,10 @@ func runDeploy(cmd *cobra.Command, args []string) error {
84 if static { 91 if static {
85 return deployStatic(host, combinedDomains, inferredName, dir) 92 return deployStatic(host, combinedDomains, inferredName, dir)
86 } 93 }
87 return deployApp(host, combinedDomains, inferredName, binary, port, envVars, envFile, binaryArgs, files) 94 return deployApp(host, combinedDomains, inferredName, binary, port, envVars, envFile, binaryArgs, files, memory, cpu)
88} 95}
89 96
90func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string) error { 97func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string, memory, cpu string) error {
91 if binaryPath == "" { 98 if binaryPath == "" {
92 return fmt.Errorf("--binary is required") 99 return fmt.Errorf("--binary is required")
93 } 100 }
@@ -130,6 +137,12 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars
130 if len(files) == 0 && len(existingApp.Files) > 0 { 137 if len(files) == 0 && len(existingApp.Files) > 0 {
131 files = existingApp.Files 138 files = existingApp.Files
132 } 139 }
140 if memory == "" && existingApp.Memory != "" {
141 memory = existingApp.Memory
142 }
143 if cpu == "" && existingApp.CPU != "" {
144 cpu = existingApp.CPU
145 }
133 } 146 }
134 147
135 for _, e := range envVars { 148 for _, e := range envVars {
@@ -235,6 +248,8 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars
235 "Port": strconv.Itoa(port), 248 "Port": strconv.Itoa(port),
236 "EnvFile": envFilePath, 249 "EnvFile": envFilePath,
237 "Args": args, 250 "Args": args,
251 "Memory": memory,
252 "CPU": cpu,
238 }) 253 })
239 if err != nil { 254 if err != nil {
240 return fmt.Errorf("error generating systemd unit: %w", err) 255 return fmt.Errorf("error generating systemd unit: %w", err)
@@ -284,6 +299,8 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars
284 Env: env, 299 Env: env,
285 Args: args, 300 Args: args,
286 Files: files, 301 Files: files,
302 Memory: memory,
303 CPU: cpu,
287 }) 304 })
288 if err := st.Save(); err != nil { 305 if err := st.Save(); err != nil {
289 return fmt.Errorf("error saving state: %w", err) 306 return fmt.Errorf("error saving state: %w", err)
@@ -297,8 +314,121 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars
297 return nil 314 return nil
298} 315}
299 316
300func deployStatic(host, domain, name, dir string) error { 317func updateAppConfig(host, name string, envVars []string, envFile, args, memory, cpu string) error {
318 st, err := state.Load()
319 if err != nil {
320 return fmt.Errorf("error loading state: %w", err)
321 }
322
323 existingApp, err := st.GetApp(host, name)
324 if err != nil {
325 return fmt.Errorf("app %s not found (use --binary to deploy a new app)", name)
326 }
327
328 if existingApp.Type != "app" {
329 return fmt.Errorf("%s is a static site, not an app", name)
330 }
331
332 fmt.Printf("Updating config: %s\n", name)
333
334 // Merge with existing values
335 if args == "" {
336 args = existingApp.Args
337 }
338 if memory == "" {
339 memory = existingApp.Memory
340 }
341 if cpu == "" {
342 cpu = existingApp.CPU
343 }
344
345 // Merge env vars
346 env := make(map[string]string)
347 for k, v := range existingApp.Env {
348 env[k] = v
349 }
350 for _, e := range envVars {
351 parts := strings.SplitN(e, "=", 2)
352 if len(parts) == 2 {
353 env[parts[0]] = parts[1]
354 }
355 }
356 if envFile != "" {
357 fileEnv, err := parseEnvFile(envFile)
358 if err != nil {
359 return fmt.Errorf("error reading env file: %w", err)
360 }
361 for k, v := range fileEnv {
362 env[k] = v
363 }
364 }
365 env["PORT"] = strconv.Itoa(existingApp.Port)
301 366
367 client, err := ssh.Connect(host)
368 if err != nil {
369 return fmt.Errorf("error connecting to VPS: %w", err)
370 }
371 defer client.Close()
372
373 // Update env file
374 fmt.Println("-> Updating environment file...")
375 envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", name)
376 envContent := ""
377 for k, v := range env {
378 envContent += fmt.Sprintf("%s=%s\n", k, v)
379 }
380 if err := client.WriteSudoFile(envFilePath, envContent); err != nil {
381 return fmt.Errorf("error creating env file: %w", err)
382 }
383
384 // Regenerate systemd unit
385 fmt.Println("-> Updating systemd service...")
386 workDir := fmt.Sprintf("/var/lib/%s", name)
387 binaryDest := fmt.Sprintf("/usr/local/bin/%s", name)
388 serviceContent, err := templates.SystemdService(map[string]string{
389 "Name": name,
390 "User": name,
391 "WorkDir": workDir,
392 "BinaryPath": binaryDest,
393 "Port": strconv.Itoa(existingApp.Port),
394 "EnvFile": envFilePath,
395 "Args": args,
396 "Memory": memory,
397 "CPU": cpu,
398 })
399 if err != nil {
400 return fmt.Errorf("error generating systemd unit: %w", err)
401 }
402
403 servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", name)
404 if err := client.WriteSudoFile(servicePath, serviceContent); err != nil {
405 return fmt.Errorf("error creating systemd unit: %w", err)
406 }
407
408 fmt.Println("-> Reloading systemd...")
409 if _, err := client.RunSudo("systemctl daemon-reload"); err != nil {
410 return fmt.Errorf("error reloading systemd: %w", err)
411 }
412
413 fmt.Println("-> Restarting service...")
414 if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil {
415 return fmt.Errorf("error restarting service: %w", err)
416 }
417
418 // Update state
419 existingApp.Args = args
420 existingApp.Memory = memory
421 existingApp.CPU = cpu
422 existingApp.Env = env
423 if err := st.Save(); err != nil {
424 return fmt.Errorf("error saving state: %w", err)
425 }
426
427 fmt.Printf("\n Config updated successfully!\n")
428 return nil
429}
430
431func deployStatic(host, domain, name, dir string) error {
302 if _, err := os.Stat(dir); err != nil { 432 if _, err := os.Stat(dir); err != nil {
303 return fmt.Errorf("directory not found: %s", dir) 433 return fmt.Errorf("directory not found: %s", dir)
304 } 434 }