summaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/ship/main.go2
-rw-r--r--cmd/ship/root.go138
2 files changed, 136 insertions, 4 deletions
diff --git a/cmd/ship/main.go b/cmd/ship/main.go
index a6984ec..47dd579 100644
--- a/cmd/ship/main.go
+++ b/cmd/ship/main.go
@@ -65,6 +65,8 @@ func init() {
65 rootCmd.Flags().String("env-file", "", "Path to .env file") 65 rootCmd.Flags().String("env-file", "", "Path to .env file")
66 rootCmd.Flags().String("args", "", "Arguments to pass to binary") 66 rootCmd.Flags().String("args", "", "Arguments to pass to binary")
67 rootCmd.Flags().StringArray("file", nil, "Config file to upload to working directory (can be specified multiple times)") 67 rootCmd.Flags().StringArray("file", nil, "Config file to upload to working directory (can be specified multiple times)")
68 rootCmd.Flags().String("memory", "", "Memory limit (e.g., 512M, 1G)")
69 rootCmd.Flags().String("cpu", "", "CPU limit (e.g., 50%, 200% for 2 cores)")
68 70
69 // Add subcommands 71 // Add subcommands
70 rootCmd.AddCommand(listCmd) 72 rootCmd.AddCommand(listCmd)
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 }