From 1704c7d9dd88ebdae78217ea9b1a5941dc10f998 Mon Sep 17 00:00:00 2001 From: bndw Date: Sun, 25 Jan 2026 08:16:54 -0800 Subject: Refactor deploy functions to use DeployOptions struct --- cmd/ship/root.go | 191 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 107 insertions(+), 84 deletions(-) diff --git a/cmd/ship/root.go b/cmd/ship/root.go index d63772b..c46491f 100644 --- a/cmd/ship/root.go +++ b/cmd/ship/root.go @@ -14,38 +14,54 @@ import ( "github.com/spf13/cobra" ) +// DeployOptions contains all options for deploying or updating an app +type DeployOptions struct { + Host string + Domain string + Name string + Binary string + Port int + EnvVars []string + EnvFile string + Args string + Files []string + Memory string + CPU string +} + func runDeploy(cmd *cobra.Command, args []string) error { flags := cmd.Flags() - binary, _ := flags.GetString("binary") + opts := DeployOptions{} + opts.Binary, _ = flags.GetString("binary") static, _ := flags.GetBool("static") dir, _ := flags.GetString("dir") - domain, _ := flags.GetString("domain") - name, _ := flags.GetString("name") - port, _ := flags.GetInt("port") - envVars, _ := flags.GetStringArray("env") - envFile, _ := flags.GetString("env-file") - binaryArgs, _ := flags.GetString("args") - files, _ := flags.GetStringArray("file") - memory, _ := flags.GetString("memory") - cpu, _ := flags.GetString("cpu") + opts.Domain, _ = flags.GetString("domain") + opts.Name, _ = flags.GetString("name") + opts.Port, _ = flags.GetInt("port") + opts.EnvVars, _ = flags.GetStringArray("env") + opts.EnvFile, _ = flags.GetString("env-file") + opts.Args, _ = flags.GetString("args") + opts.Files, _ = flags.GetStringArray("file") + opts.Memory, _ = flags.GetString("memory") + opts.CPU, _ = flags.GetString("cpu") // Get host from flag or state default - host := hostFlag - if host == "" { + opts.Host = hostFlag + if opts.Host == "" { st, err := state.Load() if err != nil { return fmt.Errorf("error loading state: %w", err) } - host = st.GetDefaultHost() + opts.Host = st.GetDefaultHost() } // If no flags provided, show help - if domain == "" && binary == "" && !static && name == "" { + if opts.Domain == "" && opts.Binary == "" && !static && opts.Name == "" { return cmd.Help() } - if host == "" { + if opts.Host == "" { return fmt.Errorf("--host is required") } @@ -54,78 +70,82 @@ func runDeploy(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("error loading state: %w", err) } - hostState := st.GetHost(host) + hostState := st.GetHost(opts.Host) // Config update mode: --name provided without --binary or --static - if name != "" && binary == "" && !static { - return updateAppConfig(host, name, envVars, envFile, binaryArgs, memory, cpu) + if opts.Name != "" && opts.Binary == "" && !static { + return updateAppConfig(opts) } - if domain == "" && hostState.BaseDomain == "" { + if opts.Domain == "" && hostState.BaseDomain == "" { return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')") } // Infer name early so we can use it for subdomain generation - inferredName := name - if inferredName == "" { + if opts.Name == "" { if static { - inferredName = domain - if inferredName == "" && hostState.BaseDomain != "" { - inferredName = filepath.Base(dir) + opts.Name = opts.Domain + if opts.Name == "" && hostState.BaseDomain != "" { + opts.Name = filepath.Base(dir) } } else { - inferredName = filepath.Base(binary) + opts.Name = filepath.Base(opts.Binary) } } // Generate subdomain if base domain configured var domains []string if hostState.BaseDomain != "" { - domains = append(domains, inferredName+"."+hostState.BaseDomain) + domains = append(domains, opts.Name+"."+hostState.BaseDomain) } - if domain != "" { - domains = append(domains, domain) + if opts.Domain != "" { + domains = append(domains, opts.Domain) } - combinedDomains := strings.Join(domains, ", ") + opts.Domain = strings.Join(domains, ", ") if static { - return deployStatic(host, combinedDomains, inferredName, dir) + return deployStatic(opts.Host, opts.Domain, opts.Name, dir) } - return deployApp(host, combinedDomains, inferredName, binary, port, envVars, envFile, binaryArgs, files, memory, cpu) + return deployApp(opts) } -func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string, memory, cpu string) error { - if binaryPath == "" { +func deployApp(opts DeployOptions) error { + if opts.Binary == "" { return fmt.Errorf("--binary is required") } - if _, err := os.Stat(binaryPath); err != nil { - return fmt.Errorf("binary not found: %s", binaryPath) + if _, err := os.Stat(opts.Binary); err != nil { + return fmt.Errorf("binary not found: %s", opts.Binary) } - fmt.Printf("Deploying app: %s\n", name) - fmt.Printf(" Domain(s): %s\n", domain) - fmt.Printf(" Binary: %s\n", binaryPath) + fmt.Printf("Deploying app: %s\n", opts.Name) + fmt.Printf(" Domain(s): %s\n", opts.Domain) + fmt.Printf(" Binary: %s\n", opts.Binary) st, err := state.Load() if err != nil { return fmt.Errorf("error loading state: %w", err) } - existingApp, _ := st.GetApp(host, name) + existingApp, _ := st.GetApp(opts.Host, opts.Name) var port int if existingApp != nil { port = existingApp.Port fmt.Printf(" Updating existing deployment (port %d)\n", port) } else { - if portOverride > 0 { - port = portOverride + if opts.Port > 0 { + port = opts.Port } else { - port = st.AllocatePort(host) + port = st.AllocatePort(opts.Host) } fmt.Printf(" Allocated port: %d\n", port) } + // Merge with existing config + args := opts.Args + files := opts.Files + memory := opts.Memory + cpu := opts.CPU env := make(map[string]string) if existingApp != nil { for k, v := range existingApp.Env { @@ -145,15 +165,15 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars } } - for _, e := range envVars { + for _, e := range opts.EnvVars { parts := strings.SplitN(e, "=", 2) if len(parts) == 2 { env[parts[0]] = parts[1] } } - if envFile != "" { - fileEnv, err := parseEnvFile(envFile) + if opts.EnvFile != "" { + fileEnv, err := parseEnvFile(opts.EnvFile) if err != nil { return fmt.Errorf("error reading env file: %w", err) } @@ -164,32 +184,32 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars env["PORT"] = strconv.Itoa(port) - client, err := ssh.Connect(host) + client, err := ssh.Connect(opts.Host) if err != nil { return fmt.Errorf("error connecting to VPS: %w", err) } defer client.Close() fmt.Println("-> Uploading binary...") - remoteTmpPath := fmt.Sprintf("/tmp/%s", name) - if err := client.Upload(binaryPath, remoteTmpPath); err != nil { + remoteTmpPath := fmt.Sprintf("/tmp/%s", opts.Name) + if err := client.Upload(opts.Binary, remoteTmpPath); err != nil { return fmt.Errorf("error uploading binary: %w", err) } fmt.Println("-> Creating system user...") - client.RunSudo(fmt.Sprintf("useradd -r -s /bin/false %s", name)) + client.RunSudo(fmt.Sprintf("useradd -r -s /bin/false %s", opts.Name)) fmt.Println("-> Setting up directories...") - workDir := fmt.Sprintf("/var/lib/%s", name) + workDir := fmt.Sprintf("/var/lib/%s", opts.Name) if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", workDir)); err != nil { return fmt.Errorf("error creating work directory: %w", err) } - if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, workDir)); err != nil { + if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", opts.Name, opts.Name, workDir)); err != nil { return fmt.Errorf("error setting work directory ownership: %w", err) } fmt.Println("-> Installing binary...") - binaryDest := fmt.Sprintf("/usr/local/bin/%s", name) + binaryDest := fmt.Sprintf("/usr/local/bin/%s", opts.Name) if _, err := client.RunSudo(fmt.Sprintf("mv %s %s", remoteTmpPath, binaryDest)); err != nil { return fmt.Errorf("error moving binary: %w", err) } @@ -205,17 +225,17 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars } remotePath := fmt.Sprintf("%s/%s", workDir, filepath.Base(file)) - remoteTmpPath := fmt.Sprintf("/tmp/%s_%s", name, filepath.Base(file)) + fileTmpPath := fmt.Sprintf("/tmp/%s_%s", opts.Name, filepath.Base(file)) - if err := client.Upload(file, remoteTmpPath); err != nil { + if err := client.Upload(file, fileTmpPath); err != nil { return fmt.Errorf("error uploading config file %s: %w", file, err) } - if _, err := client.RunSudo(fmt.Sprintf("mv %s %s", remoteTmpPath, remotePath)); err != nil { + if _, err := client.RunSudo(fmt.Sprintf("mv %s %s", fileTmpPath, remotePath)); err != nil { return fmt.Errorf("error moving config file %s: %w", file, err) } - if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, remotePath)); err != nil { + if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", opts.Name, opts.Name, remotePath)); err != nil { return fmt.Errorf("error setting config file ownership %s: %w", file, err) } @@ -224,7 +244,7 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars } fmt.Println("-> Creating environment file...") - envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", name) + envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) envContent := "" for k, v := range env { envContent += fmt.Sprintf("%s=%s\n", k, v) @@ -235,14 +255,14 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars if _, err := client.RunSudo(fmt.Sprintf("chmod 600 %s", envFilePath)); err != nil { return fmt.Errorf("error setting env file permissions: %w", err) } - if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, envFilePath)); err != nil { + if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", opts.Name, opts.Name, envFilePath)); err != nil { return fmt.Errorf("error setting env file ownership: %w", err) } fmt.Println("-> Creating systemd service...") serviceContent, err := templates.SystemdService(map[string]string{ - "Name": name, - "User": name, + "Name": opts.Name, + "User": opts.Name, "WorkDir": workDir, "BinaryPath": binaryDest, "Port": strconv.Itoa(port), @@ -255,21 +275,21 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars return fmt.Errorf("error generating systemd unit: %w", err) } - servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", name) + servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name) if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { return fmt.Errorf("error creating systemd unit: %w", err) } fmt.Println("-> Configuring Caddy...") caddyContent, err := templates.AppCaddy(map[string]string{ - "Domain": domain, + "Domain": opts.Domain, "Port": strconv.Itoa(port), }) if err != nil { return fmt.Errorf("error generating Caddy config: %w", err) } - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) + caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name) if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { return fmt.Errorf("error creating Caddy config: %w", err) } @@ -280,10 +300,10 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars } fmt.Println("-> Starting service...") - if _, err := client.RunSudo(fmt.Sprintf("systemctl enable %s", name)); err != nil { + if _, err := client.RunSudo(fmt.Sprintf("systemctl enable %s", opts.Name)); err != nil { return fmt.Errorf("error enabling service: %w", err) } - if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { + if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", opts.Name)); err != nil { return fmt.Errorf("error starting service: %w", err) } @@ -292,9 +312,9 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars return fmt.Errorf("error reloading Caddy: %w", err) } - st.AddApp(host, name, &state.App{ + st.AddApp(opts.Host, opts.Name, &state.App{ Type: "app", - Domain: domain, + Domain: opts.Domain, Port: port, Env: env, Args: args, @@ -308,36 +328,39 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars fmt.Printf("\n App deployed successfully!\n") // Show first domain in the URL message - primaryDomain := strings.Split(domain, ",")[0] + primaryDomain := strings.Split(opts.Domain, ",")[0] primaryDomain = strings.TrimSpace(primaryDomain) fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain) return nil } -func updateAppConfig(host, name string, envVars []string, envFile, args, memory, cpu string) error { +func updateAppConfig(opts DeployOptions) error { st, err := state.Load() if err != nil { return fmt.Errorf("error loading state: %w", err) } - existingApp, err := st.GetApp(host, name) + existingApp, err := st.GetApp(opts.Host, opts.Name) if err != nil { - return fmt.Errorf("app %s not found (use --binary to deploy a new app)", name) + return fmt.Errorf("app %s not found (use --binary to deploy a new app)", opts.Name) } if existingApp.Type != "app" { - return fmt.Errorf("%s is a static site, not an app", name) + return fmt.Errorf("%s is a static site, not an app", opts.Name) } - fmt.Printf("Updating config: %s\n", name) + fmt.Printf("Updating config: %s\n", opts.Name) // Merge with existing values + args := opts.Args if args == "" { args = existingApp.Args } + memory := opts.Memory if memory == "" { memory = existingApp.Memory } + cpu := opts.CPU if cpu == "" { cpu = existingApp.CPU } @@ -347,14 +370,14 @@ func updateAppConfig(host, name string, envVars []string, envFile, args, memory, for k, v := range existingApp.Env { env[k] = v } - for _, e := range envVars { + for _, e := range opts.EnvVars { parts := strings.SplitN(e, "=", 2) if len(parts) == 2 { env[parts[0]] = parts[1] } } - if envFile != "" { - fileEnv, err := parseEnvFile(envFile) + if opts.EnvFile != "" { + fileEnv, err := parseEnvFile(opts.EnvFile) if err != nil { return fmt.Errorf("error reading env file: %w", err) } @@ -364,7 +387,7 @@ func updateAppConfig(host, name string, envVars []string, envFile, args, memory, } env["PORT"] = strconv.Itoa(existingApp.Port) - client, err := ssh.Connect(host) + client, err := ssh.Connect(opts.Host) if err != nil { return fmt.Errorf("error connecting to VPS: %w", err) } @@ -372,7 +395,7 @@ func updateAppConfig(host, name string, envVars []string, envFile, args, memory, // Update env file fmt.Println("-> Updating environment file...") - envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", name) + envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) envContent := "" for k, v := range env { envContent += fmt.Sprintf("%s=%s\n", k, v) @@ -383,11 +406,11 @@ func updateAppConfig(host, name string, envVars []string, envFile, args, memory, // Regenerate systemd unit fmt.Println("-> Updating systemd service...") - workDir := fmt.Sprintf("/var/lib/%s", name) - binaryDest := fmt.Sprintf("/usr/local/bin/%s", name) + workDir := fmt.Sprintf("/var/lib/%s", opts.Name) + binaryDest := fmt.Sprintf("/usr/local/bin/%s", opts.Name) serviceContent, err := templates.SystemdService(map[string]string{ - "Name": name, - "User": name, + "Name": opts.Name, + "User": opts.Name, "WorkDir": workDir, "BinaryPath": binaryDest, "Port": strconv.Itoa(existingApp.Port), @@ -400,7 +423,7 @@ func updateAppConfig(host, name string, envVars []string, envFile, args, memory, return fmt.Errorf("error generating systemd unit: %w", err) } - servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", name) + servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name) if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { return fmt.Errorf("error creating systemd unit: %w", err) } @@ -411,7 +434,7 @@ func updateAppConfig(host, name string, envVars []string, envFile, args, memory, } fmt.Println("-> Restarting service...") - if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { + if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", opts.Name)); err != nil { return fmt.Errorf("error restarting service: %w", err) } -- cgit v1.2.3