The registry's public name is a second module beside the store, locked by the registry itself
The predecessor serves the registry under a public name, behind htpasswd basic auth, with a twenty-gigabyte body limit for layer pushes. The mesh's registry has no name, no lock and no limit — by design inside the mesh, where the private network is the boundary and every node pulls without an account (hq ADR 0082). Taking the name over must not change that. A route on `distribution` itself would: contributing a route is requiring one, and the store is raised at genesis on a node with no proxy. So the public door is `distribution-gate`, a second registry process on the same volume, behind the registry's own htpasswd (the predecessor's realm, the predecessor's file, carried in with `secret accept`), with the route and its limit. It requires the store's storage as a node-scoped provision, so it can only land beside the store. The store's own door is untouched — no auth, no htpasswd — which is what keeps the builder's pushes and every node's pulls working. Both processes read the predecessor's configuration where it changed behaviour: delete enabled, which tag retention depends on; no per-process descriptor cache, which two processes over one store cannot share; the CORS headers for the retired interface dropped. route-adapter writes the limit as the predecessor's own buffering middleware, named after the router, only when asked for — and skips a route whose limit it cannot read rather than carrying what the module said not to. hq ADR 0082/0104, the registry hand-over.
This commit is contained in:
@@ -75,6 +75,16 @@ export interface Route {
|
||||
from: string;
|
||||
/** Where the predecessor's proxy is to send it. */
|
||||
target: string;
|
||||
/**
|
||||
* The largest request body, in bytes, the predecessor may carry to it — the contribution's
|
||||
* `max-request-body`. Absent is whatever the predecessor does by default.
|
||||
*
|
||||
* **The registry's hand-over is why it exists** (novox/hq ADR 0082). A registry takes image
|
||||
* layers in single requests of gigabytes, and the predecessor served the registry's public name
|
||||
* with exactly this as a buffering middleware; a route that could not say it would have a public
|
||||
* name it could not be pushed to.
|
||||
*/
|
||||
maxRequestBody?: number;
|
||||
}
|
||||
|
||||
/** What one pass changed. */
|
||||
@@ -156,10 +166,22 @@ export function routesFrom(document: unknown, machine: string): { routes: Route[
|
||||
skipped.push(`${from} asked for route ${name} and gave no usable port`);
|
||||
continue;
|
||||
}
|
||||
// A limit it cannot honour is a route it does not write — skipped and named, like a port that
|
||||
// is not one. Written without the limit instead, the predecessor would carry exactly what the
|
||||
// module said not to carry, and this module would report success.
|
||||
const asked_limit = entry.values?.["max-request-body"];
|
||||
const limit = asBodyLimit(asked_limit);
|
||||
if (limit === null) {
|
||||
skipped.push(
|
||||
`${from} asked for route ${name} with a max-request-body of ${JSON.stringify(asked_limit)}, ` +
|
||||
`which is not a whole positive number of bytes`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Where the mesh says that machine is. Empty means this one, and this one is reached from
|
||||
// inside the predecessor's container by the machine's own name, not by loopback.
|
||||
const at = typeof entry.at === "string" && entry.at.trim() !== "" ? entry.at.trim() : machine;
|
||||
routes.push({ name, from, target: `http://${at}:${port}` });
|
||||
routes.push({ name, from, target: `http://${at}:${port}`, ...(limit === undefined ? {} : { maxRequestBody: limit }) });
|
||||
}
|
||||
return { routes, skipped };
|
||||
}
|
||||
@@ -183,6 +205,10 @@ export function routerNameFor(name: string): string {
|
||||
*/
|
||||
export function routeFile(route: Route, settings: Settings): string {
|
||||
const id = routerNameFor(route.name);
|
||||
// The body limit is a middleware in the predecessor's vocabulary — its `buffering`, with the
|
||||
// one field the predecessor's own registry route set — named after the router so the two halves
|
||||
// cannot drift, and written only when the contribution asked for it.
|
||||
const limited = route.maxRequestBody !== undefined;
|
||||
return [
|
||||
marker,
|
||||
`# ${route.from} contributed this route. It is removed when that contribution goes.`,
|
||||
@@ -192,10 +218,19 @@ export function routeFile(route: Route, settings: Settings): string {
|
||||
` entryPoints: [${settings.entrypoint}]`,
|
||||
` rule: Host(\`${route.name}\`)`,
|
||||
` service: ${id}`,
|
||||
...(limited ? [` middlewares: [${id}-body]`] : []),
|
||||
" tls:",
|
||||
` certResolver: ${settings.resolver}`,
|
||||
" domains:",
|
||||
` - main: ${route.name}`,
|
||||
...(limited
|
||||
? [
|
||||
" middlewares:",
|
||||
` ${id}-body:`,
|
||||
" buffering:",
|
||||
` maxRequestBodyBytes: ${route.maxRequestBody}`,
|
||||
]
|
||||
: []),
|
||||
" services:",
|
||||
` ${id}:`,
|
||||
" loadBalancer:",
|
||||
@@ -279,6 +314,22 @@ export async function reconcile(routes: Route[], settings: Settings): Promise<Pa
|
||||
return pass;
|
||||
}
|
||||
|
||||
/**
|
||||
* A contribution's `max-request-body`: `undefined` when it said nothing, the number of bytes when
|
||||
* it is a whole positive number, and `null` when it is anything else — the same rule the
|
||||
* controller's catalogue applies when it parses the manifest, so a limit that reaches here has
|
||||
* already passed it once, and one that fails it was laid over by a setting.
|
||||
*/
|
||||
function asBodyLimit(value: unknown): number | undefined | null {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** What JSON makes of a port, which is a float even where it was written 8080. */
|
||||
function asPort(value: unknown): number | undefined {
|
||||
const port = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
||||
|
||||
Reference in New Issue
Block a user