From 6b2c04728cd914f27ae62c1df0bf5df24ac9a628 Mon Sep 17 00:00:00 2001 From: Clawd Date: Tue, 17 Feb 2026 07:54:26 -0800 Subject: Remove v1 code, simplify state to just base_domain - Delete all v1 commands (deploy, init, list, status, remove, etc.) - Delete v1 env/ and host/ subcommand directories - Simplify state.go: remove NextPort, Apps, AllocatePort, etc. - Local state now only tracks default_host + base_domain per host - Ports and deploys are tracked on the server (/etc/ship/ports/) - host init now creates minimal state.json --- cmd/ship/deploy.go | 664 ----------------------------------------------------- 1 file changed, 664 deletions(-) delete mode 100644 cmd/ship/deploy.go (limited to 'cmd/ship/deploy.go') diff --git a/cmd/ship/deploy.go b/cmd/ship/deploy.go deleted file mode 100644 index 414ade5..0000000 --- a/cmd/ship/deploy.go +++ /dev/null @@ -1,664 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/bdw/ship/internal/ssh" - "github.com/bdw/ship/internal/state" - "github.com/bdw/ship/internal/templates" - "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 - Dir string // for static sites - Port int - Args string - Files []string - Memory string - CPU string - Env map[string]string // merged env vars - IsUpdate bool -} - -func runDeploy(cmd *cobra.Command, args []string) error { - flags := cmd.Flags() - - // Parse CLI flags - 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") - argsFlag, _ := flags.GetString("args") - files, _ := flags.GetStringArray("file") - memory, _ := flags.GetString("memory") - cpu, _ := flags.GetString("cpu") - - // Get host from flag or state default - host := hostFlag - if host == "" { - st, err := state.Load() - if err != nil { - return fmt.Errorf("error loading state: %w", err) - } - host = st.GetDefaultHost() - } - - // If no flags provided, show help - if domain == "" && binary == "" && !static && name == "" { - return cmd.Help() - } - - if host == "" { - return fmt.Errorf("--host is required") - } - - // Load state once - this will be used throughout - st, err := state.Load() - if err != nil { - return fmt.Errorf("error loading state: %w", err) - } - hostState := st.GetHost(host) - - // Config update mode: --name provided without --binary or --static - if name != "" && binary == "" && !static { - existingApp, err := st.GetApp(host, name) - if err != nil { - return fmt.Errorf("app %s not found (use --binary to deploy a new app)", name) - } - - // Build merged config starting from existing app - opts := DeployOptions{ - Host: host, - Name: name, - Port: existingApp.Port, - Args: existingApp.Args, - Files: existingApp.Files, - Memory: existingApp.Memory, - CPU: existingApp.CPU, - Env: make(map[string]string), - } - for k, v := range existingApp.Env { - opts.Env[k] = v - } - - // Override with CLI flags if provided - if argsFlag != "" { - opts.Args = argsFlag - } - if len(files) > 0 { - opts.Files = files - } - if memory != "" { - opts.Memory = memory - } - if cpu != "" { - opts.CPU = cpu - } - - // Merge env vars (CLI overrides existing) - for _, e := range envVars { - parts := strings.SplitN(e, "=", 2) - if len(parts) == 2 { - opts.Env[parts[0]] = parts[1] - } - } - if envFile != "" { - fileEnv, err := parseEnvFile(envFile) - if err != nil { - return fmt.Errorf("error reading env file: %w", err) - } - for k, v := range fileEnv { - opts.Env[k] = v - } - } - - return updateAppConfig(st, opts) - } - - // Infer name early so we can use it for subdomain generation and existing app lookup - if name == "" { - if static { - name = domain - if name == "" && hostState.BaseDomain != "" { - name = filepath.Base(dir) - } - } else { - name = filepath.Base(binary) - } - } - if err := validateName(name); err != nil { - return err - } - - // Check if this is an update to an existing app/site - existingApp, _ := st.GetApp(host, name) - isUpdate := existingApp != nil - - // For new deployments, require domain or base domain - if !isUpdate && domain == "" && hostState.BaseDomain == "" { - return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')") - } - - // Build merged config, starting from existing app if updating - opts := DeployOptions{ - Host: host, - Name: name, - Binary: binary, - Dir: dir, - IsUpdate: isUpdate, - } - - // Merge domain: auto-subdomain + (user-provided or existing custom domain) - var domains []string - if hostState.BaseDomain != "" { - domains = append(domains, name+"."+hostState.BaseDomain) - } - if domain != "" { - domains = append(domains, domain) - } else if isUpdate && existingApp.Domain != "" { - for _, d := range strings.Split(existingApp.Domain, ",") { - d = strings.TrimSpace(d) - if d != "" && (hostState.BaseDomain == "" || !strings.HasSuffix(d, "."+hostState.BaseDomain)) { - domains = append(domains, d) - } - } - } - opts.Domain = strings.Join(domains, ", ") - - // For apps, merge all config fields - if !static { - // Start with existing values if updating - if isUpdate { - opts.Port = existingApp.Port - opts.Args = existingApp.Args - opts.Files = existingApp.Files - opts.Memory = existingApp.Memory - opts.CPU = existingApp.CPU - opts.Env = make(map[string]string) - for k, v := range existingApp.Env { - opts.Env[k] = v - } - } else { - opts.Port = port - opts.Env = make(map[string]string) - } - - // Override with CLI flags if provided - if argsFlag != "" { - opts.Args = argsFlag - } - if len(files) > 0 { - opts.Files = files - } - if memory != "" { - opts.Memory = memory - } - if cpu != "" { - opts.CPU = cpu - } - - // Merge env vars (CLI overrides existing) - for _, e := range envVars { - parts := strings.SplitN(e, "=", 2) - if len(parts) == 2 { - opts.Env[parts[0]] = parts[1] - } - } - if envFile != "" { - fileEnv, err := parseEnvFile(envFile) - if err != nil { - return fmt.Errorf("error reading env file: %w", err) - } - for k, v := range fileEnv { - opts.Env[k] = v - } - } - } - - if static { - return deployStatic(st, opts) - } - return deployApp(st, opts) -} - -func deployApp(st *state.State, opts DeployOptions) error { - if opts.Binary == "" { - return fmt.Errorf("--binary is required") - } - - if _, err := os.Stat(opts.Binary); err != nil { - return fmt.Errorf("binary not found: %s", opts.Binary) - } - - fmt.Printf("Deploying app: %s\n", opts.Name) - fmt.Printf(" Domain(s): %s\n", opts.Domain) - fmt.Printf(" Binary: %s\n", opts.Binary) - - // Allocate port for new apps - port := opts.Port - if opts.IsUpdate { - fmt.Printf(" Updating existing deployment (port %d)\n", port) - } else { - if port == 0 { - port = st.AllocatePort(opts.Host) - } - fmt.Printf(" Allocated port: %d\n", port) - } - - // Add PORT to env - opts.Env["PORT"] = strconv.Itoa(port) - - 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", 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", opts.Name)) - - fmt.Println("-> Setting up directories...") - 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", 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", opts.Name) - if _, err := client.RunSudo(fmt.Sprintf("mv %s %s", remoteTmpPath, binaryDest)); err != nil { - return fmt.Errorf("error moving binary: %w", err) - } - if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", binaryDest)); err != nil { - return fmt.Errorf("error making binary executable: %w", err) - } - - if len(opts.Files) > 0 { - fmt.Println("-> Uploading config files...") - for _, file := range opts.Files { - if _, err := os.Stat(file); err != nil { - return fmt.Errorf("config file not found: %s", file) - } - - remotePath := fmt.Sprintf("%s/%s", workDir, filepath.Base(file)) - fileTmpPath := fmt.Sprintf("/tmp/%s_%s", opts.Name, filepath.Base(file)) - - 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", 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", opts.Name, opts.Name, remotePath)); err != nil { - return fmt.Errorf("error setting config file ownership %s: %w", file, err) - } - - fmt.Printf(" Uploaded: %s\n", file) - } - } - - fmt.Println("-> Creating environment file...") - envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) - envContent := "" - for k, v := range opts.Env { - envContent += fmt.Sprintf("%s=%s\n", k, v) - } - if err := client.WriteSudoFile(envFilePath, envContent); err != nil { - return fmt.Errorf("error creating env file: %w", err) - } - 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", opts.Name, opts.Name, envFilePath)); err != nil { - return fmt.Errorf("error setting env file ownership: %w", err) - } - - // Create local .ship directory for deployment configs if they don't exist - // (handles both initial deployment and migration of existing deployments) - if _, err := os.Stat(".ship/service"); os.IsNotExist(err) { - fmt.Println("-> Creating local .ship directory...") - if err := os.MkdirAll(".ship", 0755); err != nil { - return fmt.Errorf("error creating .ship directory: %w", err) - } - - fmt.Println("-> Generating systemd service...") - serviceContent, err := templates.SystemdService(map[string]string{ - "Name": opts.Name, - "User": opts.Name, - "WorkDir": workDir, - "BinaryPath": binaryDest, - "Port": strconv.Itoa(port), - "EnvFile": envFilePath, - "Args": opts.Args, - "Memory": opts.Memory, - "CPU": opts.CPU, - }) - if err != nil { - return fmt.Errorf("error generating systemd unit: %w", err) - } - if err := os.WriteFile(".ship/service", []byte(serviceContent), 0644); err != nil { - return fmt.Errorf("error writing .ship/service: %w", err) - } - } - - if _, err := os.Stat(".ship/Caddyfile"); os.IsNotExist(err) { - fmt.Println("-> Generating Caddyfile...") - caddyContent, err := templates.AppCaddy(map[string]string{ - "Domain": opts.Domain, - "Port": strconv.Itoa(port), - }) - if err != nil { - return fmt.Errorf("error generating Caddy config: %w", err) - } - if err := os.WriteFile(".ship/Caddyfile", []byte(caddyContent), 0644); err != nil { - return fmt.Errorf("error writing .ship/Caddyfile: %w", err) - } - } - - // Upload systemd service from .ship/service - fmt.Println("-> Installing systemd service...") - serviceContent, err := os.ReadFile(".ship/service") - if err != nil { - return fmt.Errorf("error reading .ship/service: %w", err) - } - servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name) - if err := client.WriteSudoFile(servicePath, string(serviceContent)); err != nil { - return fmt.Errorf("error installing systemd unit: %w", err) - } - - // Upload Caddyfile from .ship/Caddyfile - fmt.Println("-> Installing Caddy config...") - caddyContent, err := os.ReadFile(".ship/Caddyfile") - if err != nil { - return fmt.Errorf("error reading .ship/Caddyfile: %w", err) - } - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name) - if err := client.WriteSudoFile(caddyPath, string(caddyContent)); err != nil { - return fmt.Errorf("error installing Caddy config: %w", err) - } - - fmt.Println("-> Reloading systemd...") - if _, err := client.RunSudo("systemctl daemon-reload"); err != nil { - return fmt.Errorf("error reloading systemd: %w", err) - } - - fmt.Println("-> Starting service...") - 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", opts.Name)); err != nil { - return fmt.Errorf("error starting service: %w", err) - } - - fmt.Println("-> Reloading Caddy...") - if _, err := client.RunSudo("systemctl reload caddy"); err != nil { - return fmt.Errorf("error reloading Caddy: %w", err) - } - - st.AddApp(opts.Host, opts.Name, &state.App{ - Type: "app", - Domain: opts.Domain, - Port: port, - Env: opts.Env, - Args: opts.Args, - Files: opts.Files, - Memory: opts.Memory, - CPU: opts.CPU, - }) - if err := st.Save(); err != nil { - return fmt.Errorf("error saving state: %w", err) - } - - fmt.Printf("\n App deployed successfully!\n") - // Show first domain in the URL message - 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(st *state.State, opts DeployOptions) error { - 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)", opts.Name) - } - - if existingApp.Type != "app" && existingApp.Type != "git-app" { - return fmt.Errorf("%s is a static site, not an app", opts.Name) - } - - fmt.Printf("Updating config: %s\n", opts.Name) - - // Add PORT to env - opts.Env["PORT"] = strconv.Itoa(existingApp.Port) - - client, err := ssh.Connect(opts.Host) - if err != nil { - return fmt.Errorf("error connecting to VPS: %w", err) - } - defer client.Close() - - // Update env file - fmt.Println("-> Updating environment file...") - envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) - envContent := "" - for k, v := range opts.Env { - envContent += fmt.Sprintf("%s=%s\n", k, v) - } - if err := client.WriteSudoFile(envFilePath, envContent); err != nil { - return fmt.Errorf("error creating env file: %w", err) - } - - // For git-app, the systemd unit comes from .ship/service in the repo, - // so we only update the env file and restart. - if existingApp.Type != "git-app" { - // Regenerate systemd unit to .ship/service (resource flags are being updated) - fmt.Println("-> Updating systemd service...") - 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": opts.Name, - "User": opts.Name, - "WorkDir": workDir, - "BinaryPath": binaryDest, - "Port": strconv.Itoa(existingApp.Port), - "EnvFile": envFilePath, - "Args": opts.Args, - "Memory": opts.Memory, - "CPU": opts.CPU, - }) - if err != nil { - return fmt.Errorf("error generating systemd unit: %w", err) - } - - // Write to local .ship/service - if err := os.MkdirAll(".ship", 0755); err != nil { - return fmt.Errorf("error creating .ship directory: %w", err) - } - if err := os.WriteFile(".ship/service", []byte(serviceContent), 0644); err != nil { - return fmt.Errorf("error writing .ship/service: %w", err) - } - - // Upload to server - servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", opts.Name) - if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { - return fmt.Errorf("error installing systemd unit: %w", err) - } - - fmt.Println("-> Reloading systemd...") - if _, err := client.RunSudo("systemctl daemon-reload"); err != nil { - return fmt.Errorf("error reloading systemd: %w", err) - } - } - - fmt.Println("-> Restarting service...") - if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", opts.Name)); err != nil { - return fmt.Errorf("error restarting service: %w", err) - } - - // Update state - existingApp.Args = opts.Args - existingApp.Memory = opts.Memory - existingApp.CPU = opts.CPU - existingApp.Env = opts.Env - if err := st.Save(); err != nil { - return fmt.Errorf("error saving state: %w", err) - } - - fmt.Printf("\n Config updated successfully!\n") - return nil -} - -func deployStatic(st *state.State, opts DeployOptions) error { - if _, err := os.Stat(opts.Dir); err != nil { - return fmt.Errorf("directory not found: %s", opts.Dir) - } - - fmt.Printf("Deploying static site: %s\n", opts.Name) - fmt.Printf(" Domain(s): %s\n", opts.Domain) - fmt.Printf(" Directory: %s\n", opts.Dir) - - if opts.IsUpdate { - fmt.Println(" Updating existing deployment") - } - - client, err := ssh.Connect(opts.Host) - if err != nil { - return fmt.Errorf("error connecting to VPS: %w", err) - } - defer client.Close() - - remoteDir := fmt.Sprintf("/var/www/%s", opts.Name) - fmt.Println("-> Creating remote directory...") - if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", remoteDir)); err != nil { - return fmt.Errorf("error creating remote directory: %w", err) - } - - currentUser, err := client.Run("whoami") - if err != nil { - return fmt.Errorf("error getting current user: %w", err) - } - currentUser = strings.TrimSpace(currentUser) - - if _, err := client.RunSudo(fmt.Sprintf("chown -R %s:%s %s", currentUser, currentUser, remoteDir)); err != nil { - return fmt.Errorf("error setting temporary ownership: %w", err) - } - - fmt.Println("-> Uploading files...") - if err := client.UploadDir(opts.Dir, remoteDir); err != nil { - return fmt.Errorf("error uploading files: %w", err) - } - - fmt.Println("-> Setting permissions...") - if _, err := client.RunSudo(fmt.Sprintf("chown -R www-data:www-data %s", remoteDir)); err != nil { - return fmt.Errorf("error setting ownership: %w", err) - } - if _, err := client.RunSudo(fmt.Sprintf("chmod -R 755 %s", remoteDir)); err != nil { - return fmt.Errorf("error setting directory permissions: %w", err) - } - if _, err := client.RunSudo(fmt.Sprintf("find %s -type f -exec chmod 644 {} \\;", remoteDir)); err != nil { - return fmt.Errorf("error setting file permissions: %w", err) - } - - // Create local .ship directory and Caddyfile for static sites if it doesn't exist - // (handles both initial deployment and migration of existing deployments) - shipDir := filepath.Join(opts.Dir, ".ship") - caddyfilePath := filepath.Join(shipDir, "Caddyfile") - - if _, err := os.Stat(caddyfilePath); os.IsNotExist(err) { - fmt.Println("-> Creating local .ship directory...") - if err := os.MkdirAll(shipDir, 0755); err != nil { - return fmt.Errorf("error creating .ship directory: %w", err) - } - - fmt.Println("-> Generating Caddyfile...") - caddyContent, err := templates.StaticCaddy(map[string]string{ - "Domain": opts.Domain, - "RootDir": remoteDir, - }) - if err != nil { - return fmt.Errorf("error generating Caddy config: %w", err) - } - if err := os.WriteFile(caddyfilePath, []byte(caddyContent), 0644); err != nil { - return fmt.Errorf("error writing .ship/Caddyfile: %w", err) - } - } - - // Upload Caddyfile from .ship/Caddyfile - fmt.Println("-> Installing Caddy config...") - caddyContent, err := os.ReadFile(caddyfilePath) - if err != nil { - return fmt.Errorf("error reading .ship/Caddyfile: %w", err) - } - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name) - if err := client.WriteSudoFile(caddyPath, string(caddyContent)); err != nil { - return fmt.Errorf("error installing Caddy config: %w", err) - } - - fmt.Println("-> Reloading Caddy...") - if _, err := client.RunSudo("systemctl reload caddy"); err != nil { - return fmt.Errorf("error reloading Caddy: %w", err) - } - - st.AddApp(opts.Host, opts.Name, &state.App{ - Type: "static", - Domain: opts.Domain, - }) - if err := st.Save(); err != nil { - return fmt.Errorf("error saving state: %w", err) - } - - fmt.Printf("\n Static site deployed successfully!\n") - 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 parseEnvFile(path string) (map[string]string, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - env := make(map[string]string) - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - env[parts[0]] = parts[1] - } - } - - return env, scanner.Err() -} -- cgit v1.2.3