diff options
Diffstat (limited to 'cmd/ship/deploy_impl_v2.go')
| -rw-r--r-- | cmd/ship/deploy_impl_v2.go | 123 |
1 files changed, 71 insertions, 52 deletions
diff --git a/cmd/ship/deploy_impl_v2.go b/cmd/ship/deploy_impl_v2.go index e2989b2..5b68dc3 100644 --- a/cmd/ship/deploy_impl_v2.go +++ b/cmd/ship/deploy_impl_v2.go | |||
| @@ -46,20 +46,22 @@ func deployStaticV2(ctx *deployContext) *output.ErrorResponse { | |||
| 46 | // Non-fatal, continue | 46 | // Non-fatal, continue |
| 47 | } | 47 | } |
| 48 | 48 | ||
| 49 | // Generate Caddyfile | 49 | // Generate Caddyfile only if it doesn't exist (preserve manual edits) |
| 50 | caddyfile, err := templates.StaticCaddy(map[string]string{ | ||
| 51 | "Domain": ctx.URL[8:], // Strip https:// | ||
| 52 | "RootDir": remotePath, | ||
| 53 | "Name": name, | ||
| 54 | }) | ||
| 55 | if err != nil { | ||
| 56 | return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) | ||
| 57 | } | ||
| 58 | |||
| 59 | // Upload Caddyfile | ||
| 60 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) | 50 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) |
| 61 | if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { | 51 | caddyExists, _ := client.Run(fmt.Sprintf("test -f %s && echo exists", caddyPath)) |
| 62 | return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) | 52 | if strings.TrimSpace(caddyExists) != "exists" { |
| 53 | caddyfile, err := templates.StaticCaddy(map[string]string{ | ||
| 54 | "Domain": ctx.URL[8:], // Strip https:// | ||
| 55 | "RootDir": remotePath, | ||
| 56 | "Name": name, | ||
| 57 | }) | ||
| 58 | if err != nil { | ||
| 59 | return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) | ||
| 60 | } | ||
| 61 | |||
| 62 | if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { | ||
| 63 | return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) | ||
| 64 | } | ||
| 63 | } | 65 | } |
| 64 | 66 | ||
| 65 | // Reload Caddy | 67 | // Reload Caddy |
| @@ -132,9 +134,14 @@ func deployDockerV2(ctx *deployContext) *output.ErrorResponse { | |||
| 132 | } | 134 | } |
| 133 | 135 | ||
| 134 | // Generate systemd unit | 136 | // Generate systemd unit |
| 137 | containerPort := ctx.Opts.ContainerPort | ||
| 138 | if containerPort == 0 { | ||
| 139 | containerPort = 80 | ||
| 140 | } | ||
| 135 | service, err := templates.DockerService(map[string]string{ | 141 | service, err := templates.DockerService(map[string]string{ |
| 136 | "Name": name, | 142 | "Name": name, |
| 137 | "Port": strconv.Itoa(port), | 143 | "Port": strconv.Itoa(port), |
| 144 | "ContainerPort": strconv.Itoa(containerPort), | ||
| 138 | }) | 145 | }) |
| 139 | if err != nil { | 146 | if err != nil { |
| 140 | return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error()) | 147 | return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error()) |
| @@ -145,18 +152,21 @@ func deployDockerV2(ctx *deployContext) *output.ErrorResponse { | |||
| 145 | return output.Err(output.ErrServiceFailed, "failed to write systemd unit: "+err.Error()) | 152 | return output.Err(output.ErrServiceFailed, "failed to write systemd unit: "+err.Error()) |
| 146 | } | 153 | } |
| 147 | 154 | ||
| 148 | // Generate Caddyfile | 155 | // Generate Caddyfile only if it doesn't exist (preserve manual edits) |
| 149 | caddyfile, err := templates.AppCaddy(map[string]string{ | ||
| 150 | "Domain": ctx.URL[8:], // Strip https:// | ||
| 151 | "Port": strconv.Itoa(port), | ||
| 152 | }) | ||
| 153 | if err != nil { | ||
| 154 | return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) | ||
| 155 | } | ||
| 156 | |||
| 157 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) | 156 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) |
| 158 | if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { | 157 | caddyExists, _ := client.Run(fmt.Sprintf("test -f %s && echo exists", caddyPath)) |
| 159 | return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) | 158 | if strings.TrimSpace(caddyExists) != "exists" { |
| 159 | caddyfile, err := templates.AppCaddy(map[string]string{ | ||
| 160 | "Domain": ctx.URL[8:], // Strip https:// | ||
| 161 | "Port": strconv.Itoa(port), | ||
| 162 | }) | ||
| 163 | if err != nil { | ||
| 164 | return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) | ||
| 165 | } | ||
| 166 | |||
| 167 | if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { | ||
| 168 | return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) | ||
| 169 | } | ||
| 160 | } | 170 | } |
| 161 | 171 | ||
| 162 | // Reload systemd and start service | 172 | // Reload systemd and start service |
| @@ -250,18 +260,21 @@ func deployBinaryV2(ctx *deployContext) *output.ErrorResponse { | |||
| 250 | return output.Err(output.ErrServiceFailed, "failed to write systemd unit: "+err.Error()) | 260 | return output.Err(output.ErrServiceFailed, "failed to write systemd unit: "+err.Error()) |
| 251 | } | 261 | } |
| 252 | 262 | ||
| 253 | // Generate Caddyfile | 263 | // Generate Caddyfile only if it doesn't exist (preserve manual edits) |
| 254 | caddyfile, err := templates.AppCaddy(map[string]string{ | ||
| 255 | "Domain": ctx.URL[8:], // Strip https:// | ||
| 256 | "Port": strconv.Itoa(port), | ||
| 257 | }) | ||
| 258 | if err != nil { | ||
| 259 | return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) | ||
| 260 | } | ||
| 261 | |||
| 262 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) | 264 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) |
| 263 | if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { | 265 | caddyExists, _ := client.Run(fmt.Sprintf("test -f %s && echo exists", caddyPath)) |
| 264 | return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) | 266 | if strings.TrimSpace(caddyExists) != "exists" { |
| 267 | caddyfile, err := templates.AppCaddy(map[string]string{ | ||
| 268 | "Domain": ctx.URL[8:], // Strip https:// | ||
| 269 | "Port": strconv.Itoa(port), | ||
| 270 | }) | ||
| 271 | if err != nil { | ||
| 272 | return output.Err(output.ErrCaddyFailed, "failed to generate Caddyfile: "+err.Error()) | ||
| 273 | } | ||
| 274 | |||
| 275 | if err := client.WriteSudoFile(caddyPath, caddyfile); err != nil { | ||
| 276 | return output.Err(output.ErrCaddyFailed, "failed to write Caddyfile: "+err.Error()) | ||
| 277 | } | ||
| 265 | } | 278 | } |
| 266 | 279 | ||
| 267 | // Reload systemd and start service | 280 | // Reload systemd and start service |
| @@ -281,38 +294,44 @@ func deployBinaryV2(ctx *deployContext) *output.ErrorResponse { | |||
| 281 | } | 294 | } |
| 282 | 295 | ||
| 283 | // allocatePort allocates or retrieves a port for a service | 296 | // allocatePort allocates or retrieves a port for a service |
| 297 | // Uses atomic increment on /etc/ship/next_port to avoid collisions | ||
| 284 | func allocatePort(client *ssh.Client, name string) (int, error) { | 298 | func allocatePort(client *ssh.Client, name string) (int, error) { |
| 285 | portFile := fmt.Sprintf("/etc/ship/ports/%s", name) | 299 | portFile := fmt.Sprintf("/etc/ship/ports/%s", name) |
| 286 | 300 | ||
| 287 | // Try to read existing port | 301 | // Try to read existing port for this app |
| 288 | out, err := client.Run(fmt.Sprintf("cat %s 2>/dev/null || echo ''", portFile)) | 302 | out, err := client.Run(fmt.Sprintf("cat %s 2>/dev/null || echo ''", portFile)) |
| 289 | if err == nil && out != "" { | 303 | if err == nil && out != "" { |
| 290 | port, err := strconv.Atoi(out[:len(out)-1]) // Strip newline | 304 | out = strings.TrimSpace(out) |
| 291 | if err == nil && port > 0 { | 305 | if port, err := strconv.Atoi(out); err == nil && port > 0 { |
| 292 | return port, nil | 306 | return port, nil |
| 293 | } | 307 | } |
| 294 | } | 308 | } |
| 295 | 309 | ||
| 296 | // Allocate new port | 310 | // Allocate new port atomically using flock |
| 297 | // Find highest used port and increment | 311 | // This reads next_port, increments it, and writes back while holding a lock |
| 298 | out, err = client.RunSudo("mkdir -p /etc/ship/ports && ls /etc/ship/ports/ 2>/dev/null | xargs -I{} cat /etc/ship/ports/{} 2>/dev/null | sort -n | tail -1") | 312 | allocScript := ` |
| 313 | flock -x /etc/ship/.port.lock sh -c ' | ||
| 314 | mkdir -p /etc/ship/ports | ||
| 315 | PORT=$(cat /etc/ship/next_port 2>/dev/null || echo 9000) | ||
| 316 | echo $((PORT + 1)) > /etc/ship/next_port | ||
| 317 | echo $PORT | ||
| 318 | '` | ||
| 319 | out, err = client.RunSudo(allocScript) | ||
| 299 | if err != nil { | 320 | if err != nil { |
| 300 | out = "" | 321 | return 0, fmt.Errorf("failed to allocate port: %w", err) |
| 301 | } | 322 | } |
| 302 | 323 | ||
| 303 | nextPort := 9000 | 324 | port, err := strconv.Atoi(strings.TrimSpace(out)) |
| 304 | if out != "" { | 325 | if err != nil { |
| 305 | if lastPort, err := strconv.Atoi(out[:len(out)-1]); err == nil { | 326 | return 0, fmt.Errorf("invalid port allocated: %s", out) |
| 306 | nextPort = lastPort + 1 | ||
| 307 | } | ||
| 308 | } | 327 | } |
| 309 | 328 | ||
| 310 | // Write port allocation | 329 | // Write port allocation for this app |
| 311 | if err := client.WriteSudoFile(portFile, strconv.Itoa(nextPort)); err != nil { | 330 | if err := client.WriteSudoFile(portFile, strconv.Itoa(port)); err != nil { |
| 312 | return 0, err | 331 | return 0, err |
| 313 | } | 332 | } |
| 314 | 333 | ||
| 315 | return nextPort, nil | 334 | return port, nil |
| 316 | } | 335 | } |
| 317 | 336 | ||
| 318 | // setTTLV2 sets auto-expiry for a deploy | 337 | // setTTLV2 sets auto-expiry for a deploy |
