ReviewOS

also looking at this

stacks/ts-cloud

fix(deploy): allocate site ports per box, so a second attach cannot collide

#170
Merged glennmichael123 wants to merge fix/attach-port-allocation into main
4 files +643 -1

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

docs/config.mdmodified+43-0
Changes to docs/config.md
@@ -384,6 +384,49 @@ regenerates the route config and restarts the gateway — so new
384384server-app/server-static sites appear automatically. Leaving `proxy` unset
385385keeps the prior behavior (no gateway installed; you run your own).
386386
387### Ports on a shared box
388
389Each project's deploy writes only its own route fragment,
390`/etc/rpx/sites.d/<slug>.json`, and the gateway merges them at startup. That is
391what lets several independent projects share one box: routes are keyed by host, so
392they compose.
393
394Ports do not compose. Every `server-app` route resolves to `from:
395'localhost:<port>'` on one shared loopback namespace, and apps generated from the
396same template declare the same ports. `validateDeploymentConfig` only ever sees
397one project's `sites`, so a second project attaching to an occupied box passes
398validation and then fails at `systemctl start` with a bind error naming neither
399side.
400
401The fragments already answer this, since every one of them records its upstream
402port. `site-ports.ts` reads them:
403
404```typescript
405import { allocateSitePorts, buildHostSitePortsScript, occupiedHostPorts, parseHostSiteFragments } from './deploy/site-ports'
406import { validateDeploymentConfig } from './deploy/site-target'
407
408// `stdout` is the output of buildHostSitePortsScript() run on the box.
409const occupiedPorts = occupiedHostPorts(parseHostSiteFragments(stdout), { ignoreSlug: config.project.slug })
410
411// Reports a collision at plan time, naming the project that holds the port.
412const { errors } = validateDeploymentConfig(config, { occupiedPorts })
413
414// Or allocate around it, so no app has to hand-pick a globally unique port.
415const { allocations } = allocateSitePorts(config, occupiedPorts)
416```
417
418Two details matter:
419
420- **`ignoreSlug` is required.** This project's own fragment is already on the box
421 from its last deploy, so counting it makes every redeploy conflict with itself.
422- **A declared port is kept whenever it is free.** A box with no co-tenants
423 allocates nothing and ports stay stable across deploys; only a site that
424 actually collides moves, and it moves to the next free port rather than
425 somewhere arbitrary.
426
427Omitting `occupiedPorts` validates exactly as before, so this is additive for any
428single-project box.
429
387430## Preset Configuration
388431
389432### Static Site Preset