summaryrefslogtreecommitdiffstats
path: root/cmd/ship/deploy_impl_v2.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/ship/deploy_impl_v2.go')
-rw-r--r--cmd/ship/deploy_impl_v2.go123
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
284func allocatePort(client *ssh.Client, name string) (int, error) { 298func 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 := `
313flock -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