From d9110c373d55afa93a79b1f7ce26c96b5f13ff34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 6 Sep 2026 10:58:41 +0100 Subject: [PATCH] a manifest cannot ask for a particular port A preference sounds helpful and fails silently. When the wished-for number is taken the plugin is handed a different one, the install succeeds, and whatever was configured against the old number goes on pointing at nothing - with nobody told. This afternoon that nearly happened: 3011 was only free because fofos-dev had been stopped twenty minutes earlier, and had it still been running the site would have been given a random port behind an nginx host still aimed at 3011. So the number is always drawn fresh and always kept. Always drawn means it is never something you can assume; always kept means the proxy host is configured against it exactly once. The failure mode that remains - having to go and read the number - is one that announces itself, which the other one did not. isFree went with it; it existed only to test a wish. --- .../officer_db/src/plugins/queries.ts | 24 ++----------------- src/servers/plugins/install.ts | 2 +- src/servers/plugins/manifest.ts | 22 ++++++----------- 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/src/databases/officer_db/src/plugins/queries.ts b/src/databases/officer_db/src/plugins/queries.ts index 9a59e520..8f83b3fa 100644 --- a/src/databases/officer_db/src/plugins/queries.ts +++ b/src/databases/officer_db/src/plugins/queries.ts @@ -77,17 +77,6 @@ export async function getPluginPorts(appName: string): Promise { - try { - const probe = Bun.serve({ port, hostname: '127.0.0.1', fetch: () => new Response('') }); - probe.stop(true); - return true; - } catch { - return false; - } -} - async function freePort(taken: Set): Promise { for (let attempt = 0; attempt < 20; attempt++) { const probe = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: () => new Response('') }); @@ -105,7 +94,7 @@ async function freePort(taken: Set): Promise { * point — see the note on `pluginPorts`. Re-installing, upgrading and restarting all return what was * given the first time. */ -export async function allocatePluginPort(appName: string, name: string, preferred?: number): Promise { +export async function allocatePluginPort(appName: string, name: string): Promise { const [existing] = await db .select() .from(pluginPorts) @@ -120,16 +109,7 @@ export async function allocatePluginPort(appName: string, name: string, preferre */ for (let attempt = 0; attempt < 5; attempt++) { const taken = new Set((await db.select().from(pluginPorts)).map((r) => r.port)); - /* - The wish first, and only on the first attempt. - - A preferred port that is already spoken for, or that something else on this machine is listening - on, is not an error — it is a plugin arriving somewhere its ideal address is taken, which is - ordinary. It falls back to any free one rather than failing, because an install that refuses over - a preference would make `preferred` a claim. - */ - const wish = attempt === 0 && preferred && !taken.has(preferred) && (await isFree(preferred)) ? preferred : null; - const port = wish ?? (await freePort(taken)); + const port = await freePort(taken); try { const [row] = await db.insert(pluginPorts).values({ appName, name, port }).returning(); return row!.port; diff --git a/src/servers/plugins/install.ts b/src/servers/plugins/install.ts index e604f974..aa79363a 100644 --- a/src/servers/plugins/install.ts +++ b/src/servers/plugins/install.ts @@ -463,7 +463,7 @@ export async function installPlugin( */ const ports: Record = {}; for (const declared of plugin.manifest.extraPorts ?? []) { - ports[declared.name] = await allocatePluginPort(appName, declared.name, declared.preferred); + ports[declared.name] = await allocatePluginPort(appName, declared.name); } if (Object.keys(ports).length) { await step( diff --git a/src/servers/plugins/manifest.ts b/src/servers/plugins/manifest.ts index b233e601..4a6023cf 100644 --- a/src/servers/plugins/manifest.ts +++ b/src/servers/plugins/manifest.ts @@ -436,20 +436,19 @@ export type PluginManifest = { * process has already had to decide what to bind. * * ```ts - * extraPorts: [{ name: 'site' }] // any free port, remembered - * extraPorts: [{ name: 'site', preferred: 3011 }] // that one if it is free, otherwise any + * extraPorts: [{ name: 'site' }] // → PORT_SITE= in the sidecar's environment * ``` * - * `preferred` is a WISH, not a claim. It exists because a plugin often arrives after the thing that - * connects to it — an nginx proxy host already pointing at 3011, a firewall rule already written — and - * moving that configuration is work the platform can spare somebody. If the number is already given to - * another plugin, or is not free on this machine, the plugin gets a different one and no error: a - * preference that could fail the install would be a claim wearing a softer word. + * A manifest does NOT get to ask for a particular number, and that is deliberate. A preference sounds + * helpful and fails silently: when the wished-for port is taken the plugin is handed a different one, + * the install succeeds, and whatever was configured against the old number keeps pointing at nothing. + * Nobody is told. Drawing a fresh one every time makes the number always something you had to go and + * read, which is the state that keeps the configuration honest. * * Binding is still the sidecar's business, including which interface. A port that exists to be reached * from a container cannot be loopback, and the platform has no way to know which a plugin means. */ - extraPorts?: { name: string; preferred?: number }[]; + extraPorts?: { name: string }[]; /** * Host binaries this plugin needs. Absent means it is self-sufficient, which offscale and example are. @@ -1018,13 +1017,6 @@ export function manifestProblems(appName: string, manifest: Partial 1024 && preferred < 65536; - if (!ok) problems.push(`extraPorts[${i}].preferred must be a port number between 1025 and 65535`); - } } } }