From 778bef5ee6941056e06326d1eaaa6956d7307a85 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sat, 18 Apr 2026 14:40:17 -0700 Subject: Remove Go implementation — ship is skills-only now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skills/ directory fully replaces the old Go CLI. Drop all Go source, build files, planning docs, and the stale SECURITY.md (which described the old git-user push-deploy model that no longer exists). Trim .gitignore to match the new tree. --- cmd/ship/deploy_impl.go | 394 ------------------------------------------------ 1 file changed, 394 deletions(-) delete mode 100644 cmd/ship/deploy_impl.go (limited to 'cmd/ship/deploy_impl.go') diff --git a/cmd/ship/deploy_impl.go b/cmd/ship/deploy_impl.go deleted file mode 100644 index bfec9d3..0000000 --- a/cmd/ship/deploy_impl.go +++ /dev/null @@ -1,394 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/bdw/ship/internal/output" - "github.com/bdw/ship/internal/ssh" - "github.com/bdw/ship/internal/templates" -) - -// deployStaticV2 deploys a static site -// 1. rsync path to /var/www// -// 2. Generate and upload Caddyfile -// 3. Reload Caddy -func deployStaticV2(ctx *deployContext) *output.ErrorResponse { - client, err := ssh.Connect(ctx.SSHHost) - if err != nil { - return output.Err(output.ErrSSHConnectFailed, err.Error()) - } - defer client.Close() - - name := ctx.Name - remotePath := fmt.Sprintf("/var/www/%s", name) - - // Create directory and set ownership for upload - user, _ := client.Run("whoami") - user = strings.TrimSpace(user) - if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", remotePath)); err != nil { - return output.Err(output.ErrUploadFailed, "failed to create directory: "+err.Error()) - } - if _, err := client.RunSudo(fmt.Sprintf("chown -R %s:%s %s", user, user, remotePath)); err != nil { - return output.Err(output.ErrUploadFailed, "failed to set directory ownership: "+err.Error()) - } - - // Upload files using rsync - if err := client.UploadDir(ctx.Path, remotePath); err != nil { - return output.Err(output.ErrUploadFailed, err.Error()) - } - - // Set ownership back to www-data - if _, err := client.RunSudo(fmt.Sprintf("chown -R www-data:www-data %s", remotePath)); err != nil { - // Non-fatal, continue - } - - // Generate Caddyfile only if it doesn't exist (preserve manual edits) - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) - caddyExists, _ := client.Run(fmt.Sprintf("test -f %s && echo exists", caddyPath)) - if strings.TrimSpace(caddyExists) != "exists" { - caddyfile, err := templates.StaticCaddy(map[string]string{ - "Domain": ctx.URL[8:], // Strip https:// - "RootDir": remotePath, - "Name": name, - }) - if err != nil { - return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) - } - - if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { - return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) - } - } - - // Reload Caddy - if _, err := client.RunSudo("systemctl reload caddy"); err != nil { - return output.Err(output.ErrCaddyFailed, "failed to reload Caddy: "+err.Error()) - } - - return nil -} - -// deployDockerV2 deploys a Docker-based app -// 1. Allocate port -// 2. rsync path to /var/lib//src/ -// 3. docker build -// 4. Generate systemd unit and env file -// 5. Generate Caddyfile -// 6. Start service, reload Caddy -func deployDockerV2(ctx *deployContext) *output.ErrorResponse { - client, err := ssh.Connect(ctx.SSHHost) - if err != nil { - return output.Err(output.ErrSSHConnectFailed, err.Error()) - } - defer client.Close() - - name := ctx.Name - - // Allocate port on server - port, err := allocatePort(client, name) - if err != nil { - return output.Err(output.ErrServiceFailed, "failed to allocate port: "+err.Error()) - } - - srcPath := fmt.Sprintf("/var/lib/%s/src", name) - dataPath := fmt.Sprintf("/var/lib/%s/data", name) - - // Create directories - if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s %s", srcPath, dataPath)); err != nil { - return output.Err(output.ErrUploadFailed, "failed to create directories: "+err.Error()) - } - - // Set ownership for upload - user, _ := client.Run("whoami") - user = strings.TrimSpace(user) - if _, err := client.RunSudo(fmt.Sprintf("chown -R %s:%s %s", user, user, srcPath)); err != nil { - return output.Err(output.ErrUploadFailed, "failed to set directory ownership: "+err.Error()) - } - - // Upload source - if err := client.UploadDir(ctx.Path, srcPath); err != nil { - return output.Err(output.ErrUploadFailed, err.Error()) - } - - // Docker build - buildCmd := fmt.Sprintf("docker build -t %s:latest %s", name, srcPath) - if _, err := client.RunSudo(buildCmd); err != nil { - return output.Err(output.ErrBuildFailed, err.Error()) - } - - // Determine container port - containerPort := ctx.Opts.ContainerPort - if containerPort == 0 { - containerPort = 80 - } - - // Generate and write env file - // Use containerPort so the app listens on the correct port inside the container - envContent := fmt.Sprintf("PORT=%d\nSHIP_NAME=%s\nSHIP_URL=%s\n", containerPort, name, ctx.URL) - for _, e := range ctx.Opts.Env { - envContent += e + "\n" - } - envPath := fmt.Sprintf("/etc/ship/env/%s.env", name) - if _, err := client.RunSudo("mkdir -p /etc/ship/env"); err != nil { - // Continue, directory might exist - } - if err := client.WriteSudoFile(envPath, envContent); err != nil { - return output.Err(output.ErrServiceFailed, "failed to write env file: "+err.Error()) - } - - // Generate systemd unit - service, err := templates.DockerService(map[string]string{ - "Name": name, - "Port": strconv.Itoa(port), - "ContainerPort": strconv.Itoa(containerPort), - }) - if err != nil { - return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error()) - } - - servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", name) - if err := client.WriteSudoFile(servicePath, service); err != nil { - return output.Err(output.ErrServiceFailed, "failed to write systemd unit: "+err.Error()) - } - - // Generate Caddyfile only if it doesn't exist (preserve manual edits) - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) - caddyExists, _ := client.Run(fmt.Sprintf("test -f %s && echo exists", caddyPath)) - if strings.TrimSpace(caddyExists) != "exists" { - caddyfile, err := templates.AppCaddy(map[string]string{ - "Domain": ctx.URL[8:], // Strip https:// - "Port": strconv.Itoa(port), - }) - if err != nil { - return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) - } - - if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { - return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) - } - } - - // Reload systemd and start service - if _, err := client.RunSudo("systemctl daemon-reload"); err != nil { - return output.Err(output.ErrServiceFailed, "failed to reload systemd: "+err.Error()) - } - if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { - return output.Err(output.ErrServiceFailed, "failed to start service: "+err.Error()) - } - - // Reload Caddy - if _, err := client.RunSudo("systemctl reload caddy"); err != nil { - return output.Err(output.ErrCaddyFailed, "failed to reload Caddy: "+err.Error()) - } - - return nil -} - -// deployBinaryV2 deploys a pre-built binary -// 1. Allocate port -// 2. scp binary to /usr/local/bin/ -// 3. Create user for service -// 4. Generate systemd unit and env file -// 5. Generate Caddyfile -// 6. Start service, reload Caddy -func deployBinaryV2(ctx *deployContext) *output.ErrorResponse { - client, err := ssh.Connect(ctx.SSHHost) - if err != nil { - return output.Err(output.ErrSSHConnectFailed, err.Error()) - } - defer client.Close() - - name := ctx.Name - - // Allocate port on server - port, err := allocatePort(client, name) - if err != nil { - return output.Err(output.ErrServiceFailed, "failed to allocate port: "+err.Error()) - } - - binaryPath := fmt.Sprintf("/usr/local/bin/%s", name) - workDir := fmt.Sprintf("/var/lib/%s", name) - - // Upload binary - if err := client.Upload(ctx.Path, "/tmp/"+name); err != nil { - return output.Err(output.ErrUploadFailed, err.Error()) - } - - // Move to final location and set permissions - if _, err := client.RunSudo(fmt.Sprintf("mv /tmp/%s %s && chmod +x %s", name, binaryPath, binaryPath)); err != nil { - return output.Err(output.ErrUploadFailed, "failed to install binary: "+err.Error()) - } - - // Create work directory - if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", workDir)); err != nil { - return output.Err(output.ErrServiceFailed, "failed to create work directory: "+err.Error()) - } - - // Create service user (ignore error if exists) - client.RunSudo(fmt.Sprintf("useradd -r -s /bin/false %s 2>/dev/null || true", name)) - client.RunSudo(fmt.Sprintf("chown -R %s:%s %s", name, name, workDir)) - - // Generate and write env file - envContent := fmt.Sprintf("PORT=%d\nSHIP_NAME=%s\nSHIP_URL=%s\n", port, name, ctx.URL) - for _, e := range ctx.Opts.Env { - envContent += e + "\n" - } - envPath := fmt.Sprintf("/etc/ship/env/%s.env", name) - if _, err := client.RunSudo("mkdir -p /etc/ship/env"); err != nil { - // Continue - } - if err := client.WriteSudoFile(envPath, envContent); err != nil { - return output.Err(output.ErrServiceFailed, "failed to write env file: "+err.Error()) - } - - // Generate systemd unit - service, err := templates.SystemdService(map[string]string{ - "Name": name, - "User": name, - "WorkDir": workDir, - "EnvFile": envPath, - "BinaryPath": binaryPath, - "Args": "", - }) - if err != nil { - return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error()) - } - - servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", name) - if err := client.WriteSudoFile(servicePath, service); err != nil { - return output.Err(output.ErrServiceFailed, "failed to write systemd unit: "+err.Error()) - } - - // Generate Caddyfile only if it doesn't exist (preserve manual edits) - caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) - caddyExists, _ := client.Run(fmt.Sprintf("test -f %s && echo exists", caddyPath)) - if strings.TrimSpace(caddyExists) != "exists" { - caddyfile, err := templates.AppCaddy(map[string]string{ - "Domain": ctx.URL[8:], // Strip https:// - "Port": strconv.Itoa(port), - }) - if err != nil { - return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) - } - - if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { - return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) - } - } - - // Reload systemd and start service - if _, err := client.RunSudo("systemctl daemon-reload"); err != nil { - return output.Err(output.ErrServiceFailed, "failed to reload systemd: "+err.Error()) - } - if _, err := client.RunSudo(fmt.Sprintf("systemctl enable --now %s", name)); err != nil { - return output.Err(output.ErrServiceFailed, "failed to start service: "+err.Error()) - } - - // Reload Caddy - if _, err := client.RunSudo("systemctl reload caddy"); err != nil { - return output.Err(output.ErrCaddyFailed, "failed to reload Caddy: "+err.Error()) - } - - return nil -} - -// allocatePort allocates or retrieves a port for a service -// Uses atomic increment on /etc/ship/next_port to avoid collisions -func allocatePort(client *ssh.Client, name string) (int, error) { - portFile := fmt.Sprintf("/etc/ship/ports/%s", name) - - // Try to read existing port for this app - out, err := client.Run(fmt.Sprintf("cat %s 2>/dev/null || echo ''", portFile)) - if err == nil && out != "" { - out = strings.TrimSpace(out) - if port, err := strconv.Atoi(out); err == nil && port > 0 { - return port, nil - } - } - - // Allocate new port atomically using flock - // Scans existing port files to avoid collisions even if next_port is stale - allocScript := `flock -x /etc/ship/.port.lock sh -c 'mkdir -p /etc/ship/ports; NEXT=$(cat /etc/ship/next_port 2>/dev/null || echo 9000); MAX=8999; for f in /etc/ship/ports/*; do [ -f "$f" ] && P=$(cat "$f" 2>/dev/null) && [ "$P" -gt "$MAX" ] 2>/dev/null && MAX=$P; done; PORT=$(( NEXT > MAX ? NEXT : MAX + 1 )); echo $((PORT + 1)) > /etc/ship/next_port; echo $PORT'` - out, err = client.RunSudo(allocScript) - if err != nil { - return 0, fmt.Errorf("failed to allocate port: %w", err) - } - - port, err := strconv.Atoi(strings.TrimSpace(out)) - if err != nil { - return 0, fmt.Errorf("invalid port allocated: %s", out) - } - - // Write port allocation for this app - if err := client.WriteSudoFile(portFile, strconv.Itoa(port)); err != nil { - return 0, err - } - - return port, nil -} - -// setTTLV2 sets auto-expiry for a deploy -func setTTLV2(ctx *deployContext, ttl time.Duration) error { - client, err := ssh.Connect(ctx.SSHHost) - if err != nil { - return err - } - defer client.Close() - - expires := time.Now().Add(ttl).Unix() - ttlPath := fmt.Sprintf("/etc/ship/ttl/%s", ctx.Name) - - if _, err := client.RunSudo("mkdir -p /etc/ship/ttl"); err != nil { - return err - } - - return client.WriteSudoFile(ttlPath, strconv.FormatInt(expires, 10)) -} - -// runHealthCheck verifies the deploy is responding -func runHealthCheck(url, endpoint string) (*output.HealthResult, *output.ErrorResponse) { - fullURL := url + endpoint - - // Wait for app to start - time.Sleep(2 * time.Second) - - var lastErr error - var lastStatus int - - for i := 0; i < 15; i++ { - start := time.Now() - resp, err := http.Get(fullURL) - latency := time.Since(start).Milliseconds() - - if err != nil { - lastErr = err - time.Sleep(2 * time.Second) - continue - } - resp.Body.Close() - lastStatus = resp.StatusCode - - if resp.StatusCode >= 200 && resp.StatusCode < 400 { - return &output.HealthResult{ - Endpoint: endpoint, - Status: resp.StatusCode, - LatencyMs: latency, - }, nil - } - - time.Sleep(2 * time.Second) - } - - msg := fmt.Sprintf("health check failed after 30s: ") - if lastErr != nil { - msg += lastErr.Error() - } else { - msg += fmt.Sprintf("status %d", lastStatus) - } - - return nil, output.Err(output.ErrHealthCheckFailed, msg) -} -- cgit v1.2.3