Prove a route reaches the workload, and does not outlive it
The request goes to the name, across the private network, and returns the workload's own answer. Then the module is unassigned and the same request must stop working — a stale public name pointing at nothing fails more visibly than a stale grant. The workload declares its port as well as its route, because they are different questions and the earlier test leaves this machine filtering: a module that asked for a route and not for the port would be unreachable by the proxy it just asked for.
This commit is contained in:
@@ -851,3 +851,89 @@ test("rotating a credential moves both ends, and the old one stops working", {
|
||||
assert.ok(!(await login(first)).ok,
|
||||
"the password that was rotated away still authenticates, so nothing was rotated");
|
||||
});
|
||||
|
||||
test("a route is a grant: a workload is reached by the name it asked for", {
|
||||
skip, timeout: 900_000,
|
||||
}, async () => {
|
||||
// novox/hq 08-connectivity §3. The mirror of a database grant: there the consumer supplies a
|
||||
// name and receives credentials; here it supplies a target and receives a name. Nothing new in
|
||||
// the vocabulary — a route is a provision like any other.
|
||||
//
|
||||
// The workload is the registry image, because it is an HTTP server this scenario already has.
|
||||
// What is being tested is the mesh's arrangement, not the workload.
|
||||
await must("anchor", `printf %s '{"module":"frontdoor","version":"1",` +
|
||||
`"provides":[{"name":"route","scope":"mesh"}],` +
|
||||
`"capabilities":["container-runtime"],` +
|
||||
`"receives":{"route":"/etc/frontdoor/routes.json"},` +
|
||||
`"serves":{"route":{"domain":"mesh.test"}},` +
|
||||
`"listens":[{"port":8081,"from":"mesh","why":"the front door"}],` +
|
||||
`"resources":[{"id":"dir","type":"directory","path":"/etc/frontdoor","mode":"0755"},` +
|
||||
`{"id":"proxy","type":"container","name":"front-door",` +
|
||||
`"image":"${pinned("mesh-route-proxy")}","network":"host",` +
|
||||
`"volumes":["/etc/frontdoor:/etc/frontdoor:ro"],` +
|
||||
`"env":{"ROUTES":"/etc/frontdoor/routes.json","LISTEN":":8081"}}]}' ` +
|
||||
`> /tmp/frontdoor.json`);
|
||||
// The workload declares the port it listens on as well as the route it wants. Both, because
|
||||
// they are different questions: one says who may reach it, the other says by what name — and
|
||||
// the earlier test left this machine filtering, so a module that asked for a route and not for
|
||||
// the port would be unreachable by the proxy it just asked for.
|
||||
await must("anchor", `printf %s '{"module":"storefront","version":"1",` +
|
||||
`"requires":["route"],"capabilities":["container-runtime"],` +
|
||||
`"contributes":{"route":{"name":"shop.mesh.test","port":8088}},` +
|
||||
`"binds":{"route":"/etc/storefront/route.json"},` +
|
||||
`"listens":[{"port":8088,"from":"mesh","why":"the proxy reaches it here"}],` +
|
||||
`"resources":[{"id":"dir","type":"directory","path":"/etc/storefront","mode":"0755"},` +
|
||||
`{"id":"app","type":"container","name":"storefront",` +
|
||||
`"image":"${pinned("registry")}","ports":["8088:5000"]}]}' > /tmp/storefront.json`);
|
||||
for (const f of ["frontdoor", "storefront"]) {
|
||||
await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`);
|
||||
await mesh(`module add /${f}.json`);
|
||||
}
|
||||
await mesh("assign anchor frontdoor");
|
||||
await mesh("assign laptop storefront");
|
||||
await mesh("push");
|
||||
await new Promise((r) => setTimeout(r, 25_000));
|
||||
|
||||
// The provider was told who asked, and where that machine is — which it needs in order to
|
||||
// reach back, and which it must not have to derive from a naming convention.
|
||||
const routes = await must("anchor", `cat /etc/frontdoor/routes.json`);
|
||||
assert.match(routes, /shop\.mesh\.test/, `the proxy was not told about the route:\n${routes}`);
|
||||
assert.match(routes, /"at": *"laptop\.internal"/,
|
||||
`the proxy was not told where the consumer is, so it cannot reach it:\n${routes}`);
|
||||
|
||||
// And the consumer was told what the provider serves, which is how it knows its own name.
|
||||
const bound = await must("laptop", `cat /etc/storefront/route.json`);
|
||||
assert.match(bound, /mesh\.test/, `the consumer was not told the public name:\n${bound}`);
|
||||
|
||||
// The whole point: a request for the name reaches the workload, across the private network.
|
||||
let reached = false;
|
||||
let said = "";
|
||||
for (let i = 0; i < 20 && !reached; i++) {
|
||||
const answer = await on("anchor",
|
||||
`curl -sf -H 'Host: shop.mesh.test' http://127.0.0.1:8081/v2/ -o /dev/null -w '%{http_code}'`);
|
||||
said = answer.out;
|
||||
reached = answer.ok && said.trim() === "200";
|
||||
if (!reached) await new Promise((r) => setTimeout(r, 4000));
|
||||
}
|
||||
assert.ok(reached, `a request for the name did not reach the workload (${said}):\n` +
|
||||
`${(await on("anchor", `docker logs front-door 2>&1 | tail -20`)).out}`);
|
||||
|
||||
// Withdrawal, which 08-connectivity lists as open: a stale public name pointing at nothing
|
||||
// fails more visibly than a stale grant, so it must not survive the module leaving.
|
||||
await mesh("unassign laptop storefront");
|
||||
await mesh("push");
|
||||
await new Promise((r) => setTimeout(r, 20_000));
|
||||
|
||||
const after = await must("anchor", `cat /etc/frontdoor/routes.json`);
|
||||
assert.doesNotMatch(after, /shop\.mesh\.test/,
|
||||
`the route outlived the module that asked for it:\n${after}`);
|
||||
|
||||
let gone = false;
|
||||
for (let i = 0; i < 15 && !gone; i++) {
|
||||
const answer = await on("anchor",
|
||||
`curl -s -H 'Host: shop.mesh.test' http://127.0.0.1:8081/v2/ -o /dev/null -w '%{http_code}'`);
|
||||
gone = answer.out.trim() === "404";
|
||||
if (!gone) await new Promise((r) => setTimeout(r, 3000));
|
||||
}
|
||||
assert.ok(gone, "the proxy still serves a name whose module was unassigned");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user