also looking at this
fix(deploy): allocate site ports per box, so a second attach cannot collide
#170
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.
| @@ -384,6 +384,49 @@ regenerates the route config and restarts the gateway — so new | ||
| 384 | 384 | server-app/server-static sites appear automatically. Leaving `proxy` unset |
| 385 | 385 | keeps the prior behavior (no gateway installed; you run your own). |
| 386 | 386 | |
| 387 | ### Ports on a shared box | |
| 388 | ||
| 389 | Each 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 | |
| 391 | what lets several independent projects share one box: routes are keyed by host, so | |
| 392 | they compose. | |
| 393 | ||
| 394 | Ports do not compose. Every `server-app` route resolves to `from: | |
| 395 | 'localhost:<port>'` on one shared loopback namespace, and apps generated from the | |
| 396 | same template declare the same ports. `validateDeploymentConfig` only ever sees | |
| 397 | one project's `sites`, so a second project attaching to an occupied box passes | |
| 398 | validation and then fails at `systemctl start` with a bind error naming neither | |
| 399 | side. | |
| 400 | ||
| 401 | The fragments already answer this, since every one of them records its upstream | |
| 402 | port. `site-ports.ts` reads them: | |
| 403 | ||
| 404 | ```typescript | |
| 405 | import { allocateSitePorts, buildHostSitePortsScript, occupiedHostPorts, parseHostSiteFragments } from './deploy/site-ports' | |
| 406 | import { validateDeploymentConfig } from './deploy/site-target' | |
| 407 | ||
| 408 | // `stdout` is the output of buildHostSitePortsScript() run on the box. | |
| 409 | const occupiedPorts = occupiedHostPorts(parseHostSiteFragments(stdout), { ignoreSlug: config.project.slug }) | |
| 410 | ||
| 411 | // Reports a collision at plan time, naming the project that holds the port. | |
| 412 | const { errors } = validateDeploymentConfig(config, { occupiedPorts }) | |
| 413 | ||
| 414 | // Or allocate around it, so no app has to hand-pick a globally unique port. | |
| 415 | const { allocations } = allocateSitePorts(config, occupiedPorts) | |
| 416 | ``` | |
| 417 | ||
| 418 | Two 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 | ||
| 427 | Omitting `occupiedPorts` validates exactly as before, so this is additive for any | |
| 428 | single-project box. | |
| 429 | ||
| 387 | 430 | ## Preset Configuration |
| 388 | 431 | |
| 389 | 432 | ### Static Site Preset |