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.
This commit is contained in:
@@ -77,17 +77,6 @@ export async function getPluginPorts(appName: string): Promise<Record<string, nu
|
|||||||
* remembered port must say so loudly rather than quietly choosing another, because the number is written
|
* remembered port must say so loudly rather than quietly choosing another, because the number is written
|
||||||
* down in configuration this database cannot see.
|
* down in configuration this database cannot see.
|
||||||
*/
|
*/
|
||||||
/** Can we bind it right now? The only honest test, and true only for this instant. */
|
|
||||||
async function isFree(port: number): Promise<boolean> {
|
|
||||||
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<number>): Promise<number> {
|
async function freePort(taken: Set<number>): Promise<number> {
|
||||||
for (let attempt = 0; attempt < 20; attempt++) {
|
for (let attempt = 0; attempt < 20; attempt++) {
|
||||||
const probe = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: () => new Response('') });
|
const probe = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: () => new Response('') });
|
||||||
@@ -105,7 +94,7 @@ async function freePort(taken: Set<number>): Promise<number> {
|
|||||||
* point — see the note on `pluginPorts`. Re-installing, upgrading and restarting all return what was
|
* point — see the note on `pluginPorts`. Re-installing, upgrading and restarting all return what was
|
||||||
* given the first time.
|
* given the first time.
|
||||||
*/
|
*/
|
||||||
export async function allocatePluginPort(appName: string, name: string, preferred?: number): Promise<number> {
|
export async function allocatePluginPort(appName: string, name: string): Promise<number> {
|
||||||
const [existing] = await db
|
const [existing] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(pluginPorts)
|
.from(pluginPorts)
|
||||||
@@ -120,16 +109,7 @@ export async function allocatePluginPort(appName: string, name: string, preferre
|
|||||||
*/
|
*/
|
||||||
for (let attempt = 0; attempt < 5; attempt++) {
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
const taken = new Set((await db.select().from(pluginPorts)).map((r) => r.port));
|
const taken = new Set((await db.select().from(pluginPorts)).map((r) => r.port));
|
||||||
/*
|
const port = await freePort(taken);
|
||||||
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));
|
|
||||||
try {
|
try {
|
||||||
const [row] = await db.insert(pluginPorts).values({ appName, name, port }).returning();
|
const [row] = await db.insert(pluginPorts).values({ appName, name, port }).returning();
|
||||||
return row!.port;
|
return row!.port;
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ export async function installPlugin(
|
|||||||
*/
|
*/
|
||||||
const ports: Record<string, number> = {};
|
const ports: Record<string, number> = {};
|
||||||
for (const declared of plugin.manifest.extraPorts ?? []) {
|
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) {
|
if (Object.keys(ports).length) {
|
||||||
await step(
|
await step(
|
||||||
|
|||||||
@@ -436,20 +436,19 @@ export type PluginManifest = {
|
|||||||
* process has already had to decide what to bind.
|
* process has already had to decide what to bind.
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* extraPorts: [{ name: 'site' }] // any free port, remembered
|
* extraPorts: [{ name: 'site' }] // → PORT_SITE=<n> in the sidecar's environment
|
||||||
* extraPorts: [{ name: 'site', preferred: 3011 }] // that one if it is free, otherwise any
|
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* `preferred` is a WISH, not a claim. It exists because a plugin often arrives after the thing that
|
* A manifest does NOT get to ask for a particular number, and that is deliberate. A preference sounds
|
||||||
* connects to it — an nginx proxy host already pointing at 3011, a firewall rule already written — and
|
* helpful and fails silently: when the wished-for port is taken the plugin is handed a different one,
|
||||||
* moving that configuration is work the platform can spare somebody. If the number is already given to
|
* the install succeeds, and whatever was configured against the old number keeps pointing at nothing.
|
||||||
* another plugin, or is not free on this machine, the plugin gets a different one and no error: a
|
* Nobody is told. Drawing a fresh one every time makes the number always something you had to go and
|
||||||
* preference that could fail the install would be a claim wearing a softer word.
|
* 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
|
* 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.
|
* 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.
|
* 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<PluginManife
|
|||||||
// while looking distinct here.
|
// while looking distinct here.
|
||||||
if (seen.has(name)) problems.push(`extraPorts[${i}] repeats the name "${name}"`);
|
if (seen.has(name)) problems.push(`extraPorts[${i}] repeats the name "${name}"`);
|
||||||
seen.add(name);
|
seen.add(name);
|
||||||
|
|
||||||
const preferred = (entry as { preferred?: unknown }).preferred;
|
|
||||||
if (preferred !== undefined) {
|
|
||||||
// Above 1024 because nothing here runs as root, and below 65536 because that is the range.
|
|
||||||
const ok = typeof preferred === 'number' && Number.isInteger(preferred) && preferred > 1024 && preferred < 65536;
|
|
||||||
if (!ok) problems.push(`extraPorts[${i}].preferred must be a port number between 1025 and 65535`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user