diff --git a/test/integration/mesh.test.ts b/test/integration/mesh.test.ts index feca269..fbad073 100644 --- a/test/integration/mesh.test.ts +++ b/test/integration/mesh.test.ts @@ -578,11 +578,24 @@ test("a machine filters exactly what its modules declared, and nothing else", { `"resources":[]}' > /tmp/talker.json`); // The rule set goes where this machine's nftables unit reads from, and the unit is declared to // reflect it. No command anywhere. + // The module ships the unit that loads its rules, rather than using the one the distribution's + // nftables package provides. That unit is `Type=oneshot` with no `RemainAfterExit`, so it does + // its work and goes inactive — and a host asked for a service that is "running" reports, quite + // correctly, that it is stopped. There is no state in the vocabulary for "ran and exited having + // done its job", so a module that wants one brings a unit that stays. + // + // Which is also the right shape: how a machine enforces rules is a fact about the machine, and + // the mesh has no business depending on what a distribution happens to package. await must("anchor", `printf %s '{"module":"firewall","version":"1",` + `"capabilities":["firewall"],` + - `"filtering":{"into":"/etc/nftables.conf"},` + + `"filtering":{"into":"/etc/mesh/filter.nft"},` + `"resources":[{"id":"nftables","type":"package","package":"nftables"},` + - `{"id":"filter","type":"service","unit":"nftables.service","state":"running",` + + `{"id":"dir","type":"directory","path":"/etc/mesh","mode":"0755"},` + + `{"id":"unit","type":"file","path":"/etc/systemd/system/mesh-filter.service",` + + `"mode":"0644","content":"[Unit]\\nDescription=What the mesh computed for this machine\\n` + + `[Service]\\nType=oneshot\\nRemainAfterExit=yes\\n` + + `ExecStart=/usr/bin/nft -f /etc/mesh/filter.nft\\n[Install]\\nWantedBy=multi-user.target\\n"},` + + `{"id":"filter","type":"service","unit":"mesh-filter.service","state":"running",` + `"boot":"enabled","restart-on":["filtering"]}]}' > /tmp/firewall.json`); for (const f of ["talker", "firewall"]) { await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`); @@ -593,7 +606,7 @@ test("a machine filters exactly what its modules declared, and nothing else", { await mesh("push laptop"); await new Promise((r) => setTimeout(r, 20_000)); - const written = await must("laptop", `cat /etc/nftables.conf`); + const written = await must("laptop", `cat /etc/mesh/filter.nft`); // A rule names its source. Not decoration: it is the only thing that answers "why is this open". assert.match(written, /# talker . the thing this test is about/, `the rule does not name what caused it:\n${written}`); @@ -719,7 +732,15 @@ test("the builder is a module the mesh assigns, with a credential the mesh deliv `> /root/built/module.json`); await must("anchor", `cd /root/built && git init -q . && git add -A && ` + `git -c user.email=lab -c user.name=lab commit -qm built`); - await mesh("build /root/built --wait 300s", 420_000); + try { + await mesh("build /root/built --wait 300s", 420_000); + } catch (why) { + // The builder's own account of itself. Without it the failure is "nothing consumed the + // queue", which names no cause and is the same sentence whether the credential was refused, + // the queue was never declared, or the process died three seconds in. + const said = (await on("anchor", `docker logs mesh-builder 2>&1 | tail -40`)).out; + throw new Error(`${(why as Error).message}\n\nwhat the builder said:\n${said}`); + } // Naming the module, and not merely containing its name: `builds` says "nothing has been built // yet" when there is nothing, and that sentence contains the word this was matching on.