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:
@@ -22,7 +22,9 @@ async function predecessor(already: Record<string, string> = {}): Promise<Settin
|
||||
}
|
||||
|
||||
/** The contributions file the mesh writes, in the shape the mesh's own proxy also reads. */
|
||||
function contributed(...given: { from: string; node?: string; at?: string; name: string; port: number }[]) {
|
||||
function contributed(
|
||||
...given: { from: string; node?: string; at?: string; name: string; port: number; limit?: unknown }[]
|
||||
) {
|
||||
return {
|
||||
contributions: 1,
|
||||
requirement: "route",
|
||||
@@ -30,7 +32,7 @@ function contributed(...given: { from: string; node?: string; at?: string; name:
|
||||
from: g.from,
|
||||
node: g.node ?? "control-node",
|
||||
at: g.at ?? "",
|
||||
values: { name: g.name, port: g.port },
|
||||
values: { name: g.name, port: g.port, ...(g.limit === undefined ? {} : { "max-request-body": g.limit }) },
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -211,3 +213,64 @@ test("it refuses when the predecessor's directory is not there, and says why", a
|
||||
const settings = { ...defaults, dynamic: join(await mkdtemp(join(tmpdir(), "route-adapter-")), "absent") };
|
||||
await assert.rejects(reconcile([], settings), /is not there.*`dynamic` setting.*mounts it/s);
|
||||
});
|
||||
|
||||
// **The registry's hand-over** (novox/hq ADR 0082, ADR 0104). A registry takes image layers in
|
||||
// single requests of gigabytes, and the predecessor served its public name with a twenty-gigabyte
|
||||
// `buffering` middleware. The contribution carries that limit as `max-request-body`; the adapter
|
||||
// writes it as the middleware the predecessor already understands, named after the router, and
|
||||
// writes nothing of the kind for a route that did not ask.
|
||||
test("a body limit is written as the predecessor's buffering middleware", async () => {
|
||||
const settings = await predecessor();
|
||||
const changed = await pass(settings, contributed(
|
||||
{ from: "distribution-gate", name: "registry-api.example", port: 5001, limit: 21474836480 },
|
||||
{ from: "gitea", name: "git.example", port: 2999 },
|
||||
));
|
||||
assert.deepEqual(changed.written, ["mesh-git.example.yml", "mesh-registry-api.example.yml"]);
|
||||
|
||||
assert.equal(await readFile(join(settings.dynamic, "mesh-registry-api.example.yml"), "utf8"), [
|
||||
marker,
|
||||
"# distribution-gate contributed this route. It is removed when that contribution goes.",
|
||||
"http:",
|
||||
" routers:",
|
||||
" mesh-registry-api-example:",
|
||||
" entryPoints: [websecure]",
|
||||
" rule: Host(`registry-api.example`)",
|
||||
" service: mesh-registry-api-example",
|
||||
" middlewares: [mesh-registry-api-example-body]",
|
||||
" tls:",
|
||||
" certResolver: le",
|
||||
" domains:",
|
||||
" - main: registry-api.example",
|
||||
" middlewares:",
|
||||
" mesh-registry-api-example-body:",
|
||||
" buffering:",
|
||||
" maxRequestBodyBytes: 21474836480",
|
||||
" services:",
|
||||
" mesh-registry-api-example:",
|
||||
" loadBalancer:",
|
||||
" servers:",
|
||||
" - url: http://host.docker.internal:5001",
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
// The route that asked for nothing carries no middleware — the predecessor's default stands.
|
||||
const plain = await readFile(join(settings.dynamic, "mesh-git.example.yml"), "utf8");
|
||||
assert.doesNotMatch(plain, /middlewares|buffering/);
|
||||
});
|
||||
|
||||
// A limit it cannot honour is a route it does not write. Written without it, the predecessor would
|
||||
// carry exactly what the module said not to carry, and this module would report success.
|
||||
test("a body limit that is not a number of bytes is skipped and named", () => {
|
||||
const machine = defaults.machine;
|
||||
for (const limit of ["20g", 0, -1, 1.5, true, null]) {
|
||||
const { routes, skipped } = routesFrom(
|
||||
contributed({ from: "gate", name: "registry-api.example", port: 5001, limit }), machine);
|
||||
assert.deepEqual(routes, [], `a limit of ${JSON.stringify(limit)} was served`);
|
||||
assert.equal(skipped.length, 1);
|
||||
assert.match(skipped[0]!, /max-request-body/);
|
||||
}
|
||||
// And a limit the controller would accept is carried, as a number.
|
||||
const { routes } = routesFrom(
|
||||
contributed({ from: "gate", name: "registry-api.example", port: 5001, limit: 1024 }), machine);
|
||||
assert.equal(routes[0]?.maxRequestBody, 1024);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user