Unify trunk on main: initialization → main #3

Merged
jschoubben merged 95 commits from initialization into main 2026-09-05 01:13:46 +00:00
4 changed files with 96 additions and 3 deletions
Showing only changes of commit 2f2f1d931e - Show all commits
+4
View File
@@ -47,6 +47,10 @@ images:
- mesh-provision-postgres:development
# And the proxy, which is what turns a route grant into traffic actually arriving.
- mesh-route-proxy:development
# And the cache, with its provisioner — the third provision after a database and a bucket,
# and the first whose tenancy is a keyspace rather than a namespace something else enforces.
- redis:7-alpine
- mesh-provision-redis:development
# And the object store's provisioner, so the module describing it can be planned. Without it
# that module still names an image nothing serves, and planning it is refused — correctly.
- mesh-provision-objectstore:development
+2 -1
View File
@@ -59,7 +59,8 @@ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] {
builds.push({
what: "images",
in: control,
argv: ["make", "image", "builder-image", "provisioner-image", "objectstore-image", "proxy-image"],
argv: ["make", "image", "builder-image", "provisioner-image", "objectstore-image",
"redis-provisioner-image", "proxy-image"],
});
const builder = env["MESH_LAB_BUILDER"];
if (builder) {
+88 -1
View File
@@ -1969,10 +1969,20 @@ test("the forge runs, on a database the mesh gave it", { skip, timeout: 900_000
"the login exists and the database it owns does not");
// And the forge itself, answering. Not that its container exists — that it serves.
//
// On the port the mesh assigned, not the one the module declared (novox/hq ADR 0038): the
// module says 3000 and the machine publishes wherever the mesh put it. Read from the plan,
// because the plan is the same composition a push sends.
const planned = await mesh("plan anchor --json", 120_000);
const mapping = (JSON.parse(planned.slice(planned.indexOf("{"))).resources as any[])
.find((r) => r.id === "gitea.server")?.ports
?.map(String).find((p: string) => p.endsWith(":3000"));
assert.ok(mapping, "the plan does not say where the machine publishes the forge");
const at = mapping.split(":")[0];
let answered = false;
let said = { out: "", ok: false };
for (let i = 0; i < 60 && !answered; i++) {
said = await on("anchor", `curl -sf -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:3000/`, 30_000);
said = await on("anchor", `curl -sf -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:${at}/`, 30_000);
answered = said.out.trim().startsWith("2") || said.out.trim() === "303";
if (!answered) await new Promise((r) => setTimeout(r, 5000));
}
@@ -1990,3 +2000,80 @@ test("the forge runs, on a database the mesh gave it", { skip, timeout: 900_000
await mesh("unassign anchor postgres");
await mesh("push anchor", 300_000);
});
test("a consumer's cache grant means exactly its own keys", { skip, timeout: 600_000 }, async (t) => {
// The third provision after a database and a bucket, and the first whose tenancy is enforced
// by the store's own ACL rather than by separate namespaces: every consumer shares one
// keyspace, so the grant is a pattern — and the test is that the pattern means what the
// manifest said, in both directions.
const raw = readFileSync(`${process.env["MESH_LAB_MODULES"]}/redis.json`, "utf8");
const pinned = pinnedInto(raw, stocked);
assert.deepEqual(stillUnpinned(pinned), [],
"redis still names an image nothing serves, so it could not start");
await must("anchor", `printf %s ${quote(pinned)} > /run-redis.json`);
await must("anchor", `docker cp /run-redis.json mesh-control:/run-redis.json`);
await mesh("module add /run-redis.json");
// A consumer with no container: what is under test is the credential's reach, and files on the
// machine are enough to prove it — the same reduction the first credential test makes.
await must("anchor", `printf %s '{"module":"cachetest","version":"1",` +
`"requires":["redis-cache"],` +
`"contributes":{"redis-cache":{"prefix":"cachetest"}},` +
`"binds":{"redis-cache":"/var/lib/cachetest/cache.json"},` +
`"secrets":{"redis-cache":"/var/lib/cachetest/cache.secret"},` +
`"resources":[{"id":"state","type":"directory","path":"/var/lib/cachetest","mode":"0700"}]}' ` +
`> /cachetest.json`);
await must("anchor", `docker cp /cachetest.json mesh-control:/cachetest.json`);
await mesh("module add /cachetest.json");
await mesh("assign anchor redis");
await mesh("assign anchor cachetest");
await mesh("push anchor", 300_000);
await settled("anchor");
t.after(async () => {
for (const name of ["cachetest", "redis"]) {
await mesh(`unassign anchor ${name}`).catch(() => {});
}
await mesh("push anchor", 300_000).catch(() => {});
});
// What the mesh told each end. The consumer's user name comes from its binding; the user's
// password from the sealed file beside it — both written by the host, neither invented here.
const bound = JSON.parse(await must("anchor", `cat /var/lib/cachetest/cache.json`));
const user = bound.as;
assert.ok(user?.startsWith("mesh_"), `the binding does not carry a usable user: ${user}`);
const secret = (await must("anchor", `cat /var/lib/cachetest/cache.secret`)).trim();
// The provisioner has to have run before anything can authenticate. Waited for via the store
// itself: the user list, asked with the server's own password, which the conf file the host
// wrote holds on the machine.
const admin = (await must("anchor",
`awk '/^requirepass/ {print $2}' /var/lib/redis-module/redis.conf`)).trim();
let granted = false;
for (let i = 0; i < 40 && !granted; i++) {
const users = (await on("anchor",
`docker exec redis redis-cli --no-auth-warning -a ${quote(admin)} ACL USERS`)).out;
granted = users.includes(user);
if (!granted) await new Promise((r) => setTimeout(r, 3000));
}
assert.ok(granted, `no user was created for the consumer:
` +
`${(await on("anchor", "docker logs mesh-provision-redis 2>&1 | tail -20")).out}`);
const asConsumer = (command: string) =>
on("anchor", `docker exec redis redis-cli --no-auth-warning ` +
`--user ${quote(user)} --pass ${quote(secret)} ${command}`);
// Its own keys: usable.
assert.match((await asConsumer("SET cachetest:proof yes")).out, /OK/,
"the consumer cannot write under the prefix it was granted");
assert.match((await asConsumer("GET cachetest:proof")).out, /yes/,
"the consumer cannot read back what it wrote");
// Anyone else's: refused by the store itself, which is the entire point of the grant.
assert.match((await asConsumer("SET other:proof no")).out, /NOPERM|no permissions/i,
"the consumer wrote outside its prefix — the grant means more than the manifest said");
assert.match((await asConsumer("FLUSHALL")).out, /NOPERM|no permissions/i,
"the consumer can flush the store, which no tenant may");
});
+2 -1
View File
@@ -32,7 +32,8 @@ test("every image the lab runs is rebuilt, not only the control plane's", () =>
const images = builds.find((b) => b.what === "images");
assert.ok(images, "no image build at all");
for (const target of [
"image", "builder-image", "provisioner-image", "objectstore-image", "proxy-image",
"image", "builder-image", "provisioner-image", "objectstore-image",
"redis-provisioner-image", "proxy-image",
]) {
assert.ok(images.argv.includes(target), `${target} is never built, so the lab runs a stale one`);
}