also looking at this
fix(deploy): allocate site ports per box, so a second attach cannot collide
#170
4 files
+643
-1
| @@ -125,6 +125,30 @@ export interface DeploymentValidationResult { | ||
| 125 | 125 | warnings: string[] |
| 126 | 126 | } |
| 127 | 127 | |
| 128 | export interface ValidateDeploymentOptions { | |
| 129 | /** | |
| 130 | * Ports this box already serves for OTHER projects, as port -> owning slug. | |
| 131 | * | |
| 132 | * Validation is otherwise blind to co-tenants: it only ever sees one project's | |
| 133 | * `sites`, so two apps attached to the same server can both declare the | |
| 134 | * template's default port and both pass. The box does not reject the second | |
| 135 | * one either - ts-cloud's units do not set exclusive binding, so both bind and | |
| 136 | * the kernel load-balances, leaving each domain serving the other project's | |
| 137 | * site about half the time with nothing logged as an error. Supplying this | |
| 138 | * turns that into a plan-time error naming the project holding the port. | |
| 139 | * | |
| 140 | * Build it with `occupiedHostPorts()` from `./site-ports`, passing the | |
| 141 | * deploying project's own slug as `ignoreSlug` - its fragment is already on the | |
| 142 | * box from the last deploy, and counting it would make every redeploy conflict | |
| 143 | * with itself. | |
| 144 | * | |
| 145 | * Omitted means "no co-tenant information", which validates exactly as before. | |
| 146 | * | |
| 147 | * @see https://github.com/stacksjs/ts-cloud/issues/168 | |
| 148 | */ | |
| 149 | occupiedPorts?: ReadonlyMap<number, string> | |
| 150 | } | |
| 151 | ||
| 128 | 152 | /** |
| 129 | 153 | * Validate the per-site deployment configuration up front, turning what used to |
| 130 | 154 | * be silent runtime failures (e.g. a `start` site with no compute server) into |
| @@ -133,7 +157,10 @@ export interface DeploymentValidationResult { | ||
| 133 | 157 | * Never throws — returns structured `{ errors, warnings }`. Callers should abort |
| 134 | 158 | * on any error and print warnings while continuing. |
| 135 | 159 | */ |
| 136 | export function validateDeploymentConfig(config: CloudConfig): DeploymentValidationResult { | |
| 160 | export function validateDeploymentConfig( | |
| 161 | config: CloudConfig, | |
| 162 | options: ValidateDeploymentOptions = {}, | |
| 163 | ): DeploymentValidationResult { | |
| 137 | 164 | const errors: string[] = [] |
| 138 | 165 | const warnings: string[] = [] |
| 139 | 166 | const sites = config.sites || {} |
| @@ -233,6 +260,18 @@ export function validateDeploymentConfig(config: CloudConfig): DeploymentValidat | ||
| 233 | 260 | } |
| 234 | 261 | |
| 235 | 262 | if (typeof site.port === 'number') { |
| 263 | // A co-tenant on the same box holds this port. Reported separately from | |
| 264 | // the same-config clash below because the fix is different: the operator | |
| 265 | // cannot see the other project's config from here, so the message has to | |
| 266 | // name the owning project rather than a sibling site. | |
| 267 | const coTenant = options.occupiedPorts?.get(site.port) | |
| 268 | if (coTenant) { | |
| 269 | errors.push( | |
| 270 | `Site '${name}' wants port ${site.port}, which project '${coTenant}' already serves on this box. ` | |
| 271 | + `Attached projects share one port namespace. Give '${name}' a free port, or let the attach allocate one.`, | |
| 272 | ) | |
| 273 | } | |
| 274 | ||
| 236 | 275 | const existing = portOwners.get(site.port) |
| 237 | 276 | if (existing) { |
| 238 | 277 | errors.push( |