diff options
| -rw-r--r-- | cmd/ship/deploy_impl_v2.go | 45 | ||||
| -rw-r--r-- | cmd/ship/deploy_v2.go | 17 | ||||
| -rw-r--r-- | cmd/ship/root_v2.go | 2 | ||||
| -rw-r--r-- | internal/templates/templates.go | 2 | ||||
| -rwxr-xr-x | ship-new | bin | 12401121 -> 12400369 bytes |
5 files changed, 40 insertions, 26 deletions
diff --git a/cmd/ship/deploy_impl_v2.go b/cmd/ship/deploy_impl_v2.go index e2989b2..9ff674e 100644 --- a/cmd/ship/deploy_impl_v2.go +++ b/cmd/ship/deploy_impl_v2.go | |||
| @@ -132,9 +132,14 @@ func deployDockerV2(ctx *deployContext) *output.ErrorResponse { | |||
| 132 | } | 132 | } |
| 133 | 133 | ||
| 134 | // Generate systemd unit | 134 | // Generate systemd unit |
| 135 | containerPort := ctx.Opts.ContainerPort | ||
| 136 | if containerPort == 0 { | ||
| 137 | containerPort = 80 | ||
| 138 | } | ||
| 135 | service, err := templates.DockerService(map[string]string{ | 139 | service, err := templates.DockerService(map[string]string{ |
| 136 | "Name": name, | 140 | "Name": name, |
| 137 | "Port": strconv.Itoa(port), | 141 | "Port": strconv.Itoa(port), |
| 142 | "ContainerPort": strconv.Itoa(containerPort), | ||
| 138 | }) | 143 | }) |
| 139 | if err != nil { | 144 | if err != nil { |
| 140 | return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error()) | 145 | return output.Err(output.ErrServiceFailed, "failed to generate systemd unit: "+err.Error()) |
| @@ -281,38 +286,44 @@ func deployBinaryV2(ctx *deployContext) *output.ErrorResponse { | |||
| 281 | } | 286 | } |
| 282 | 287 | ||
| 283 | // allocatePort allocates or retrieves a port for a service | 288 | // allocatePort allocates or retrieves a port for a service |
| 289 | // Uses atomic increment on /etc/ship/next_port to avoid collisions | ||
| 284 | func allocatePort(client *ssh.Client, name string) (int, error) { | 290 | func allocatePort(client *ssh.Client, name string) (int, error) { |
| 285 | portFile := fmt.Sprintf("/etc/ship/ports/%s", name) | 291 | portFile := fmt.Sprintf("/etc/ship/ports/%s", name) |
| 286 | 292 | ||
| 287 | // Try to read existing port | 293 | // Try to read existing port for this app |
| 288 | out, err := client.Run(fmt.Sprintf("cat %s 2>/dev/null || echo ''", portFile)) | 294 | out, err := client.Run(fmt.Sprintf("cat %s 2>/dev/null || echo ''", portFile)) |
| 289 | if err == nil && out != "" { | 295 | if err == nil && out != "" { |
| 290 | port, err := strconv.Atoi(out[:len(out)-1]) // Strip newline | 296 | out = strings.TrimSpace(out) |
| 291 | if err == nil && port > 0 { | 297 | if port, err := strconv.Atoi(out); err == nil && port > 0 { |
| 292 | return port, nil | 298 | return port, nil |
| 293 | } | 299 | } |
| 294 | } | 300 | } |
| 295 | 301 | ||
| 296 | // Allocate new port | 302 | // Allocate new port atomically using flock |
| 297 | // Find highest used port and increment | 303 | // 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") | 304 | allocScript := ` |
| 305 | flock -x /etc/ship/.port.lock sh -c ' | ||
| 306 | mkdir -p /etc/ship/ports | ||
| 307 | PORT=$(cat /etc/ship/next_port 2>/dev/null || echo 9000) | ||
| 308 | echo $((PORT + 1)) > /etc/ship/next_port | ||
| 309 | echo $PORT | ||
| 310 | '` | ||
| 311 | out, err = client.RunSudo(allocScript) | ||
| 299 | if err != nil { | 312 | if err != nil { |
| 300 | out = "" | 313 | return 0, fmt.Errorf("failed to allocate port: %w", err) |
| 301 | } | 314 | } |
| 302 | 315 | ||
| 303 | nextPort := 9000 | 316 | port, err := strconv.Atoi(strings.TrimSpace(out)) |
| 304 | if out != "" { | 317 | if err != nil { |
| 305 | if lastPort, err := strconv.Atoi(out[:len(out)-1]); err == nil { | 318 | return 0, fmt.Errorf("invalid port allocated: %s", out) |
| 306 | nextPort = lastPort + 1 | ||
| 307 | } | ||
| 308 | } | 319 | } |
| 309 | 320 | ||
| 310 | // Write port allocation | 321 | // Write port allocation for this app |
| 311 | if err := client.WriteSudoFile(portFile, strconv.Itoa(nextPort)); err != nil { | 322 | if err := client.WriteSudoFile(portFile, strconv.Itoa(port)); err != nil { |
| 312 | return 0, err | 323 | return 0, err |
| 313 | } | 324 | } |
| 314 | 325 | ||
| 315 | return nextPort, nil | 326 | return port, nil |
| 316 | } | 327 | } |
| 317 | 328 | ||
| 318 | // setTTLV2 sets auto-expiry for a deploy | 329 | // setTTLV2 sets auto-expiry for a deploy |
diff --git a/cmd/ship/deploy_v2.go b/cmd/ship/deploy_v2.go index b636709..7d498b2 100644 --- a/cmd/ship/deploy_v2.go +++ b/cmd/ship/deploy_v2.go | |||
| @@ -143,14 +143,15 @@ func deployV2(path string, opts deployV2Options) { | |||
| 143 | } | 143 | } |
| 144 | 144 | ||
| 145 | type deployV2Options struct { | 145 | type deployV2Options struct { |
| 146 | Name string | 146 | Name string |
| 147 | Host string | 147 | Host string |
| 148 | Domain string | 148 | Domain string |
| 149 | Health string | 149 | Health string |
| 150 | TTL string | 150 | TTL string |
| 151 | Env []string | 151 | Env []string |
| 152 | EnvFile string | 152 | EnvFile string |
| 153 | Pretty bool | 153 | ContainerPort int // Port the container listens on (default 80 for Docker) |
| 154 | Pretty bool | ||
| 154 | } | 155 | } |
| 155 | 156 | ||
| 156 | // deployContext holds all info needed for a deploy | 157 | // deployContext holds all info needed for a deploy |
diff --git a/cmd/ship/root_v2.go b/cmd/ship/root_v2.go index 4101d4e..aa81d1e 100644 --- a/cmd/ship/root_v2.go +++ b/cmd/ship/root_v2.go | |||
| @@ -43,6 +43,7 @@ func initV2() { | |||
| 43 | rootV2Cmd.Flags().String("ttl", "", "Auto-delete after duration (e.g., 1h, 7d)") | 43 | rootV2Cmd.Flags().String("ttl", "", "Auto-delete after duration (e.g., 1h, 7d)") |
| 44 | rootV2Cmd.Flags().StringArray("env", nil, "Environment variable (KEY=VALUE)") | 44 | rootV2Cmd.Flags().StringArray("env", nil, "Environment variable (KEY=VALUE)") |
| 45 | rootV2Cmd.Flags().String("env-file", "", "Path to .env file") | 45 | rootV2Cmd.Flags().String("env-file", "", "Path to .env file") |
| 46 | rootV2Cmd.Flags().Int("container-port", 80, "Port the container listens on (Docker only)") | ||
| 46 | 47 | ||
| 47 | // Check for SHIP_PRETTY env var | 48 | // Check for SHIP_PRETTY env var |
| 48 | if os.Getenv("SHIP_PRETTY") == "1" { | 49 | if os.Getenv("SHIP_PRETTY") == "1" { |
| @@ -78,6 +79,7 @@ func runDeployV2(cmd *cobra.Command, args []string) error { | |||
| 78 | opts.TTL, _ = cmd.Flags().GetString("ttl") | 79 | opts.TTL, _ = cmd.Flags().GetString("ttl") |
| 79 | opts.Env, _ = cmd.Flags().GetStringArray("env") | 80 | opts.Env, _ = cmd.Flags().GetStringArray("env") |
| 80 | opts.EnvFile, _ = cmd.Flags().GetString("env-file") | 81 | opts.EnvFile, _ = cmd.Flags().GetString("env-file") |
| 82 | opts.ContainerPort, _ = cmd.Flags().GetInt("container-port") | ||
| 81 | 83 | ||
| 82 | // deployV2 handles all output and exits | 84 | // deployV2 handles all output and exits |
| 83 | deployV2(path, opts) | 85 | deployV2(path, opts) |
diff --git a/internal/templates/templates.go b/internal/templates/templates.go index f1b9a9e..2163f47 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go | |||
| @@ -216,7 +216,7 @@ Requires=docker.service | |||
| 216 | Type=simple | 216 | Type=simple |
| 217 | ExecStartPre=-/usr/bin/docker rm -f {{.Name}} | 217 | ExecStartPre=-/usr/bin/docker rm -f {{.Name}} |
| 218 | ExecStart=/usr/bin/docker run --rm --name {{.Name}} \ | 218 | ExecStart=/usr/bin/docker run --rm --name {{.Name}} \ |
| 219 | -p 127.0.0.1:{{.Port}}:80 \ | 219 | -p 127.0.0.1:{{.Port}}:{{.ContainerPort}} \ |
| 220 | --env-file /etc/ship/env/{{.Name}}.env \ | 220 | --env-file /etc/ship/env/{{.Name}}.env \ |
| 221 | -v /var/lib/{{.Name}}/data:/data \ | 221 | -v /var/lib/{{.Name}}/data:/data \ |
| 222 | {{.Name}}:latest | 222 | {{.Name}}:latest |
| Binary files differ | |||
