The route-forwarding bed installs the catalogue's authority, proxy and consumer; the whole-mesh bed installs the vault first #46
@@ -22,7 +22,18 @@ DOCKERFILE="$MESH_CATALOG/modules/route-proxy/Dockerfile"
|
|||||||
[ -f "$MESH_CONTROL/examples/route-proxy/main.go" ] || {
|
[ -f "$MESH_CONTROL/examples/route-proxy/main.go" ] || {
|
||||||
echo "no proxy source at $MESH_CONTROL/examples/route-proxy" >&2; exit 1; }
|
echo "no proxy source at $MESH_CONTROL/examples/route-proxy" >&2; exit 1; }
|
||||||
|
|
||||||
|
# The bases the manifest declares (novox/hq ADR 0097) are what this build starts FROM — the same
|
||||||
|
# images the mesh's builder would copy and hand the recipe, not the Dockerfile's floating defaults.
|
||||||
|
BASES=()
|
||||||
|
while IFS=$'\t' read -r arg image; do
|
||||||
|
[ -n "$arg" ] && BASES+=(--build-arg "$arg=$image")
|
||||||
|
done < <(python3 -c '
|
||||||
|
import json, sys
|
||||||
|
for on in json.load(open(sys.argv[1])).get("build", {}).get("on", []):
|
||||||
|
print(on["arg"], on["image"], sep="\t")
|
||||||
|
' "$MESH_CATALOG/modules/route-proxy/module.json")
|
||||||
|
|
||||||
# Context is the mesh-controller repository root: the proxy compiles against that module's go.mod and
|
# Context is the mesh-controller repository root: the proxy compiles against that module's go.mod and
|
||||||
# its examples/route-proxy package.
|
# its examples/route-proxy package.
|
||||||
docker build -f "$DOCKERFILE" -t "$TAG" "$MESH_CONTROL"
|
docker build -f "$DOCKERFILE" "${BASES[@]}" -t "$TAG" "$MESH_CONTROL"
|
||||||
echo "built $TAG (from $MESH_CONTROL/examples/route-proxy)"
|
echo "built $TAG (from $MESH_CONTROL/examples/route-proxy)"
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ const STILL_CARRIED: Record<string, { modules: string[]; why: string }> = {
|
|||||||
why: "BESIDE (lavinmq, with a bootstrap step and a data directory the catalogue has not got); DIFFERS (amqp-ping names its entrypoint)" },
|
why: "BESIDE (lavinmq, with a bootstrap step and a data directory the catalogue has not got); DIFFERS (amqp-ping names its entrypoint)" },
|
||||||
"provider-uses-mesh-credential.test.ts": { modules: ["redis"], why: "WEARING: a credential mechanism test" },
|
"provider-uses-mesh-credential.test.ts": { modules: ["redis"], why: "WEARING: a credential mechanism test" },
|
||||||
"runtime-restart-on-config.test.ts": { modules: ["grafana"], why: "WEARING: a restart mechanism test" },
|
"runtime-restart-on-config.test.ts": { modules: ["grafana"], why: "WEARING: a restart mechanism test" },
|
||||||
"route-forwarding.test.ts": { modules: ["route-proxy", "hello-web"],
|
|
||||||
why: "WEARING: route-proxy without its certificate authority, hello-web with the route shape ADR 0066 replaced" },
|
|
||||||
"mesh.test.ts": { modules: ["postgres", "builder", "umami"],
|
"mesh.test.ts": { modules: ["postgres", "builder", "umami"],
|
||||||
why: "WEARING: a postgres with no resources, a builder that builds itself, an umami that is another module of that name" },
|
why: "WEARING: a postgres with no resources, a builder that builds itself, an umami that is another module of that name" },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,3 +85,17 @@ test("a manifest the catalogue does not have is refused by name", () => {
|
|||||||
assert.throws(() => catalogueModule("nothing", held), /no manifest for nothing/);
|
assert.throws(() => catalogueModule("nothing", held), /no manifest for nothing/);
|
||||||
} finally { restore(); }
|
} finally { restore(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("an upstream artifact resolves to the reference the manifest pins, as the machine pulls it", () => {
|
||||||
|
const web = {
|
||||||
|
module: "thing", version: "1",
|
||||||
|
resources: [{ id: "server", type: "container", name: "web", artifact: "server" }],
|
||||||
|
build: { artifacts: [{ name: "server", kind: "upstream", from: "alpine@" + digest("e") }] },
|
||||||
|
};
|
||||||
|
const restore = aCatalogueWith(web);
|
||||||
|
try {
|
||||||
|
const m = JSON.parse(catalogueModule("thing", held)) as { resources: Record<string, unknown>[] };
|
||||||
|
assert.equal(m.resources[0]!["image"], "alpine@" + digest("e"));
|
||||||
|
assert.equal(m.resources[0]!["artifact"], undefined);
|
||||||
|
} finally { restore(); }
|
||||||
|
});
|
||||||
|
|||||||
@@ -306,12 +306,22 @@ export interface ForTheLab {
|
|||||||
export function catalogueModule(module: string, held: HeldImage[], lab: ForTheLab = {}): string {
|
export function catalogueModule(module: string, held: HeldImage[], lab: ForTheLab = {}): string {
|
||||||
const m = JSON.parse(readFileSync(catalogueManifest(module), "utf8")) as {
|
const m = JSON.parse(readFileSync(catalogueManifest(module), "utf8")) as {
|
||||||
resources?: { id: string; type: string; image?: string; artifact?: string; ports?: string[]; env?: Record<string, string> }[];
|
resources?: { id: string; type: string; image?: string; artifact?: string; ports?: string[]; env?: Record<string, string> }[];
|
||||||
build?: unknown;
|
build?: { artifacts?: { name: string; kind: string; from?: string }[] };
|
||||||
};
|
};
|
||||||
const artifacts: Record<string, string> = { runtime: `mesh-runtime-${module}`, ...(lab.artifacts ?? {}) };
|
const artifacts: Record<string, string> = { runtime: `mesh-runtime-${module}`, ...(lab.artifacts ?? {}) };
|
||||||
|
// An upstream artifact is somebody else's image, which the mesh's builder copies into its own
|
||||||
|
// registry (ADR 0096). The lab stands in for the builder by using the reference the manifest
|
||||||
|
// pins, which the machine pulls over its uplink — the same bytes, without the copy.
|
||||||
|
const upstream = new Map<string, string>();
|
||||||
|
for (const a of m.build?.artifacts ?? []) {
|
||||||
|
if (a.kind === "upstream" && a.from) upstream.set(a.name, a.from);
|
||||||
|
}
|
||||||
for (const r of m.resources ?? []) {
|
for (const r of m.resources ?? []) {
|
||||||
if (r.type !== "container") continue;
|
if (r.type !== "container") continue;
|
||||||
if (typeof r.artifact === "string") {
|
if (typeof r.artifact === "string" && upstream.has(r.artifact) && !lab.artifacts?.[r.artifact]) {
|
||||||
|
r.image = onTheMachine(upstream.get(r.artifact)!, held);
|
||||||
|
delete r.artifact;
|
||||||
|
} else if (typeof r.artifact === "string") {
|
||||||
const repository = artifacts[r.artifact];
|
const repository = artifacts[r.artifact];
|
||||||
assert.ok(repository,
|
assert.ok(repository,
|
||||||
`${module}'s container '${r.id}' names the "${r.artifact}" artifact, which the mesh would ` +
|
`${module}'s container '${r.id}' names the "${r.artifact}" artifact, which the mesh would ` +
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
|||||||
import { raise } from "../../src/lifecycle/raise.ts";
|
import { raise } from "../../src/lifecycle/raise.ts";
|
||||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
import { labIsUsable, destroyAll, foundationBundle, catalogueModule, catalogueIsPresent, deriveTheFilterOn } from "./harness.ts";
|
||||||
import type { HeldImage } from "../../src/pinning.ts";
|
import type { HeldImage } from "../../src/pinning.ts";
|
||||||
|
|
||||||
const capability = await labIsUsable();
|
const capability = await labIsUsable();
|
||||||
@@ -50,12 +50,14 @@ const skip = !capability.usable
|
|||||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||||
: !bundle || !existsSync(bundle)
|
: !bundle || !existsSync(bundle)
|
||||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||||
: false;
|
: catalogueIsPresent();
|
||||||
|
|
||||||
const SCENARIO = "route-forwarding";
|
const SCENARIO = "route-forwarding";
|
||||||
const MACHINE = "anchor";
|
const MACHINE = "anchor";
|
||||||
const NAME = "hello.example";
|
const NAME = "hello.example";
|
||||||
const PAGE = "hello from hello-web, routed by the mesh";
|
const PAGE = "hello from hello-web, routed by the mesh";
|
||||||
|
/** The node's public domain; hello-web's label composes under it (ADR 0066). */
|
||||||
|
const DOMAIN = "example";
|
||||||
|
|
||||||
let instanceId = "";
|
let instanceId = "";
|
||||||
let held: HeldImage[] = [];
|
let held: HeldImage[] = [];
|
||||||
@@ -85,10 +87,6 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The reference a manifest should carry, once this scenario has been raised. */
|
/** The reference a manifest should carry, once this scenario has been raised. */
|
||||||
/** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */
|
|
||||||
function pinned(reference: string): string {
|
|
||||||
return onTheMachine(reference, held);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
|
/** The foundation bundle: ours by the ID the machine holds, everything else upstream. */
|
||||||
function bundleFor(images: HeldImage[]): string {
|
function bundleFor(images: HeldImage[]): string {
|
||||||
@@ -167,64 +165,32 @@ after(async () => {
|
|||||||
test("the mesh routes a public name through the proxy to the consumer, and withdraws it on unassign", {
|
test("the mesh routes a public name through the proxy to the consumer, and withdraws it on unassign", {
|
||||||
skip, timeout: 1_500_000,
|
skip, timeout: 1_500_000,
|
||||||
}, async () => {
|
}, async () => {
|
||||||
// The PROVIDER: route-proxy in the plain-HTTP shape — provides `route`, is given every consumer as
|
// All three from the catalogue (novox/hq ADR 0093, issue 074): the authority the proxy requires,
|
||||||
// the file at receives.route, forwards by Host. No TLS here (that is certificates.test.ts); the
|
// the proxy, and the consumer. The proxy's manifest names the runtime image the scenario stocks;
|
||||||
// image is pinned to what this scenario serves by digest.
|
// the authority and the consumer's server are pulled upstream by digest. The name the consumer
|
||||||
const proxyManifest = JSON.stringify({
|
// is routed under is composed from its label and the node's public domain (ADR 0066), which
|
||||||
module: "route-proxy",
|
// the bed sets first.
|
||||||
version: "1",
|
await mesh(`node public-domain ${MACHINE} ${DOMAIN}`);
|
||||||
capabilities: ["container-runtime"],
|
// The authority certifies itself for the machine's private-network address, which is what a
|
||||||
provides: [{ name: "route", scope: "mesh" }],
|
// consumer on any node dials (ADR 0098); a machine raised from the bundle has none until it is
|
||||||
serves: { route: {} },
|
// placed on the overlay. Placed as a hub of one, the way a real first node is, and converged
|
||||||
receives: { route: "/var/lib/route-proxy/routes/mesh.json" },
|
// BEFORE the modules arrive: the proxy fetches the authority's roots at that address at first
|
||||||
listens: [{ port: 80, protocol: "tcp", from: "anywhere", why: "public HTTP; the route-forwarding front door" }],
|
// start, so the interface must exist by then — in one push the order between modules is not
|
||||||
resources: [
|
// promised. The derived filter admits the hub's port, as genesis does on a control-node (ADR 0088).
|
||||||
{ id: "state", type: "directory", path: "/var/lib/route-proxy", mode: "0700" },
|
await mesh(`overlay place ${MACHINE} --hub --endpoint 192.0.2.10:51820 --site lab`);
|
||||||
{ id: "routes-dir", type: "directory", path: "/var/lib/route-proxy/routes", mode: "0700" },
|
await mesh(`assign ${MACHINE} networking`);
|
||||||
{
|
await mesh(`push ${MACHINE}`);
|
||||||
id: "server", type: "container", name: "route-proxy",
|
await settled();
|
||||||
image: pinned("mesh-route-proxy"), network: "host",
|
await deriveTheFilterOn({ machine: MACHINE, node: MACHINE, hubPort: 51820,
|
||||||
volumes: ["/var/lib/route-proxy/routes:/routes:ro"],
|
must: (_m, c, t) => must(c, t), mesh, on: (_m, c, t) => on(c, t) });
|
||||||
env: { ROUTES: "/routes/mesh.json", LISTEN: ":80" },
|
const overlay = await must(`ip -4 addr show dev mesh0 2>&1 || ip -4 addr 2>&1`);
|
||||||
},
|
assert.match(overlay, /inet 10\./, `${MACHINE} has no private-network address after networking converged:\n${overlay}`);
|
||||||
],
|
for (const name of ["step-ca", "route-proxy", "hello-web"]) {
|
||||||
});
|
await must(`printf %s ${quote(catalogueModule(name, held))} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`);
|
||||||
|
await mesh(`module add /${name}.json`);
|
||||||
// The CONSUMER: hello-web requires `route` and contributes the name it wants and the port it
|
if (name === "step-ca") await mesh(`module issue ${name} --node ${MACHINE}`);
|
||||||
// listens on. It runs no code of the mesh's — a bare alpine serving a fixed page over a busybox nc
|
await mesh(`assign ${MACHINE} ${name}`);
|
||||||
// loop stands in for a web service. `contributes` is what makes it *ask*: the grant forms from it.
|
}
|
||||||
const webManifest = JSON.stringify({
|
|
||||||
module: "hello-web",
|
|
||||||
slug: "hello",
|
|
||||||
version: "1",
|
|
||||||
capabilities: ["container-runtime"],
|
|
||||||
requires: ["route"],
|
|
||||||
contributes: { route: { name: NAME, port: 8080 } },
|
|
||||||
binds: { route: "/var/lib/hello-web/route.json" },
|
|
||||||
listens: [{ port: 8080, protocol: "tcp", from: "mesh", why: "the demo page; only the proxy reaches it" }],
|
|
||||||
resources: [
|
|
||||||
{ id: "state", type: "directory", path: "/var/lib/hello-web", mode: "0700" },
|
|
||||||
{ id: "page", type: "file", path: "/var/lib/hello-web/index.html", mode: "0644", content: `${PAGE}\n` },
|
|
||||||
{ id: "net", type: "network", name: "hello-web" },
|
|
||||||
{
|
|
||||||
id: "server", type: "container", name: "hello-web",
|
|
||||||
image: pinned("alpine"), network: "hello-web", ports: ["8080:8080"],
|
|
||||||
volumes: ["/var/lib/hello-web/index.html:/www/index.html:ro"],
|
|
||||||
args: ["sh", "-c",
|
|
||||||
"while true; do { printf 'HTTP/1.1 200 OK\\r\\nContent-Type: text/plain\\r\\nConnection: close\\r\\n\\r\\n'; cat /www/index.html; } | nc -l -p 8080; done"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await must(`printf %s ${quote(proxyManifest)} > /tmp/route-proxy.json && docker cp /tmp/route-proxy.json mesh-controller:/route-proxy.json`);
|
|
||||||
await mesh("module add /route-proxy.json");
|
|
||||||
// No `module issue`: route-proxy has no broker account and no own-secret to mint. `assign` resolves
|
|
||||||
// its plan and the provider is matchable by a consumer's route from that alone.
|
|
||||||
await mesh(`assign ${MACHINE} route-proxy`);
|
|
||||||
|
|
||||||
await must(`printf %s ${quote(webManifest)} > /tmp/hello-web.json && docker cp /tmp/hello-web.json mesh-controller:/hello-web.json`);
|
|
||||||
await mesh("module add /hello-web.json");
|
|
||||||
await mesh(`assign ${MACHINE} hello-web`);
|
|
||||||
|
|
||||||
await mesh(`push ${MACHINE}`);
|
await mesh(`push ${MACHINE}`);
|
||||||
await settled();
|
await settled();
|
||||||
|
|||||||
@@ -179,6 +179,9 @@ const NOVOX: Mod[] = [
|
|||||||
{ name: "mongodb", containers: ["mongo", "mesh-mongodb"] },
|
{ name: "mongodb", containers: ["mongo", "mesh-mongodb"] },
|
||||||
{ name: "mssql", containers: ["mssql", "mesh-mssql"] },
|
{ name: "mssql", containers: ["mssql", "mesh-mssql"] },
|
||||||
{ name: "lavinmq", containers: ["lavinmq", "mesh-lavinmq"] },
|
{ name: "lavinmq", containers: ["lavinmq", "mesh-lavinmq"] },
|
||||||
|
// The vault, before everything that keeps a secret from it: gitea, umami, influxdb, mailu
|
||||||
|
// require one (novox/hq ADRs 0085, 0094).
|
||||||
|
{ name: "mesh-vault", containers: ["mesh-vault"] },
|
||||||
// ADR 0066: the proxy now REQUIRES an `acme-ca`, so the bed must assign a provider of one or
|
// ADR 0066: the proxy now REQUIRES an `acme-ca`, so the bed must assign a provider of one or
|
||||||
// route-proxy is unresolvable and takes every routed module down with it. step-ca is that
|
// route-proxy is unresolvable and takes every routed module down with it. step-ca is that
|
||||||
// provider, on the anchor, at mesh scope.
|
// provider, on the anchor, at mesh scope.
|
||||||
@@ -306,14 +309,11 @@ const CREDENTIALS: { node: string; module: string; name: string; crash: string }
|
|||||||
{ node: "ace", module: "home-assistant", name: "token", crash: "no Home Assistant token" },
|
{ node: "ace", module: "home-assistant", name: "token", crash: "no Home Assistant token" },
|
||||||
{ node: "ace", module: "nzbget", name: "password", crash: "NZBGet not configured" },
|
{ node: "ace", module: "nzbget", name: "password", crash: "NZBGet not configured" },
|
||||||
{ node: "ace", module: "qbittorrent", name: "password", crash: "qBittorrent not configured" },
|
{ node: "ace", module: "qbittorrent", name: "password", crash: "qBittorrent not configured" },
|
||||||
{ node: "novox", module: "umami", name: "admin", crash: "admin password is not set" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Operator secrets for the credential modules that own-secret their whole app (mailu, de-spiegel). */
|
/** Operator secrets for the credential modules that own-secret their whole app (de-spiegel,
|
||||||
|
* amqp-email-forwarder). mailu's and umami's are kept in the vault now (ADR 0094), not delivered. */
|
||||||
const OPERATOR_SECRETS: { node: string; module: string; name: string; value: string }[] = [
|
const OPERATOR_SECRETS: { node: string; module: string; name: string; value: string }[] = [
|
||||||
{ node: "novox", module: "mailu", name: "secret-key", value: "0123456789abcdef0123456789abcdef" },
|
|
||||||
{ node: "novox", module: "mailu", name: "admin", value: "MailuAdminFakePass123" },
|
|
||||||
{ node: "novox", module: "mailu", name: "api-token", value: "mailuapitokenfake0123456789abcd" },
|
|
||||||
{ node: "novox", module: "de-spiegel", name: "smtp-user", value: "despiegel-smtp-fake" },
|
{ node: "novox", module: "de-spiegel", name: "smtp-user", value: "despiegel-smtp-fake" },
|
||||||
{ node: "novox", module: "de-spiegel", name: "smtp-pass", value: "despiegel-pass-fake" },
|
{ node: "novox", module: "de-spiegel", name: "smtp-pass", value: "despiegel-pass-fake" },
|
||||||
{ node: "novox", module: "amqp-email-forwarder", name: "smtp-user", value: "eef-smtp-fake" },
|
{ node: "novox", module: "amqp-email-forwarder", name: "smtp-user", value: "eef-smtp-fake" },
|
||||||
@@ -410,56 +410,8 @@ async function psMapOf(node: string): Promise<Map<string, string>> {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ADR 0098: the internal authority makes its own root at first start and serves it; the proxy
|
||||||
* ADR 0066: the internal CA is initialised FROM AN OPERATOR'S ROOT — it does not mint its own.
|
// fetches it. No operator root is delivered — the mesh mints only the authority's password.
|
||||||
*
|
|
||||||
* So the bed has to be an operator. The material is made on the anchor with openssl and handed to
|
|
||||||
* the mesh through the real `secret accept` path, exactly as a person would: the mesh cannot invent
|
|
||||||
* a PEM, and the random 32 bytes it makes for an own-secret nobody supplied would leave step-ca
|
|
||||||
* crash-looping on a root key that is not a key.
|
|
||||||
*/
|
|
||||||
async function deliverCaRoot(): Promise<boolean> {
|
|
||||||
const made = await on(CONTROL, [
|
|
||||||
"set -e",
|
|
||||||
"mkdir -p /tmp/ca && cd /tmp/ca",
|
|
||||||
// No trailing newline on a password file: step-ca reads the file as the password itself.
|
|
||||||
"openssl rand -hex 16 | tr -d '\\n' > key-password",
|
|
||||||
"openssl ecparam -genkey -name prime256v1 -out root.unenc",
|
|
||||||
"openssl ec -in root.unenc -aes256 -passout file:key-password -out root.key",
|
|
||||||
"rm -f root.unenc",
|
|
||||||
"openssl req -x509 -new -key root.key -passin file:key-password -sha256 -days 3650" +
|
|
||||||
` -out root.crt -subj "/CN=Mesh Internal CA/O=Novox Mesh Lab"`,
|
|
||||||
// Readable by the control plane, which is not root. Its image is FROM scratch and runs as
|
|
||||||
// 65534, and `docker cp` keeps the ownership and mode a file had outside — openssl writes a
|
|
||||||
// private key 0600 root-owned, so the copy landed unreadable and `secret accept` failed with
|
|
||||||
// `open /ca-root-key: permission denied`. The CA then crash-looped on a root it never got.
|
|
||||||
// Chowning it inside the container is not available: there is no shell in there to do it with.
|
|
||||||
//
|
|
||||||
// Safe here and nowhere else: these three exist for the seconds between being written and
|
|
||||||
// being sealed to the machine, on a lab node, for a CA thrown away with the scenario.
|
|
||||||
"chmod 0644 /tmp/ca/root.crt /tmp/ca/root.key /tmp/ca/key-password",
|
|
||||||
"docker cp /tmp/ca/root.crt mesh-controller:/ca-root-cert",
|
|
||||||
"docker cp /tmp/ca/root.key mesh-controller:/ca-root-key",
|
|
||||||
"docker cp /tmp/ca/key-password mesh-controller:/ca-root-key-password",
|
|
||||||
].join("\n"), 180_000);
|
|
||||||
if (!made.ok) {
|
|
||||||
console.log(`CA ROOT NOT MADE on ${CONTROL}:\n${made.out.split("\n").slice(-8).join("\n")}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (const [name, file] of [
|
|
||||||
["root-cert", "/ca-root-cert"],
|
|
||||||
["root-key", "/ca-root-key"],
|
|
||||||
["root-key-password", "/ca-root-key-password"],
|
|
||||||
] as const) {
|
|
||||||
try {
|
|
||||||
await mesh(`secret accept ${CONTROL} step-ca ${name} --from ${file}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`CA ROOT ACCEPT FAILED (${name}): ${(err as Error).message.split("\n").slice(0, 3).join(" | ")}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What a module's manifest says its route label is, or "" if it contributes no route. */
|
/** What a module's manifest says its route label is, or "" if it contributes no route. */
|
||||||
function routeLabelOf(name: string): string {
|
function routeLabelOf(name: string): string {
|
||||||
@@ -848,7 +800,9 @@ test("the full mesh forms across the access point and both server sets converge"
|
|||||||
for (const { name } of mods) {
|
for (const { name } of mods) {
|
||||||
try {
|
try {
|
||||||
const broker = await ensureAdded(name);
|
const broker = await ensureAdded(name);
|
||||||
if (broker) await mesh(`module issue ${name} --node ${node}`);
|
// Issued when the module holds a broker account or an own-secret the mesh mints for it
|
||||||
|
// (step-ca's password, ADR 0098); a requirement kept in the vault needs no issue.
|
||||||
|
if (broker || loadManifest(name).manifest.includes('"own-secrets"')) await mesh(`module issue ${name} --node ${node}`);
|
||||||
await mesh(`assign ${node} ${name}`);
|
await mesh(`assign ${node} ${name}`);
|
||||||
assigned[node]!.add(name);
|
assigned[node]!.add(name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -889,10 +843,6 @@ test("the full mesh forms across the access point and both server sets converge"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ADR 0066: the CA's root, before the push that would otherwise deliver a random 32 bytes for it.
|
|
||||||
const caRootDelivered = assigned["novox"]!.has("step-ca") ? await deliverCaRoot() : false;
|
|
||||||
if (!caRootDelivered) console.log("ADR 0066: no operator root delivered; step-ca cannot initialise.");
|
|
||||||
|
|
||||||
// ONE push per node (workstations first — cheap — then the heavy service nodes).
|
// ONE push per node (workstations first — cheap — then the heavy service nodes).
|
||||||
const pushError: Record<string, string> = {};
|
const pushError: Record<string, string> = {};
|
||||||
for (const node of ["shanks", "g14", "novox", "ace"]) {
|
for (const node of ["shanks", "g14", "novox", "ace"]) {
|
||||||
@@ -1027,7 +977,6 @@ test("the full mesh forms across the access point and both server sets converge"
|
|||||||
? (await on("novox", `curl -s -o /dev/null -w '%{http_code}' --max-time 10 -H 'Host: ${probe.name}' http://127.0.0.1/`)).out.trim()
|
? (await on("novox", `curl -s -o /dev/null -w '%{http_code}' --max-time 10 -H 'Host: ${probe.name}' http://127.0.0.1/`)).out.trim()
|
||||||
: "";
|
: "";
|
||||||
adr.push(`\n proxy answers for ${probe?.name ?? "(nothing composed)"}: ${servedCode || "no answer"}`);
|
adr.push(`\n proxy answers for ${probe?.name ?? "(nothing composed)"}: ${servedCode || "no answer"}`);
|
||||||
adr.push(` operator root delivered to step-ca: ${caRootDelivered}`);
|
|
||||||
adr.push(` step-ca container: ${psMaps["novox"]?.get("step-ca") ?? "MISSING"}`);
|
adr.push(` step-ca container: ${psMaps["novox"]?.get("step-ca") ?? "MISSING"}`);
|
||||||
console.log(adr.join("\n"));
|
console.log(adr.join("\n"));
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ const NODE = "novox";
|
|||||||
*/
|
*/
|
||||||
const MODULES: { name: string; containers: string[]; node?: boolean }[] = [
|
const MODULES: { name: string; containers: string[]; node?: boolean }[] = [
|
||||||
{ name: "postgres", containers: ["mesh-store", "mesh-postgres"] },
|
{ name: "postgres", containers: ["mesh-store", "mesh-postgres"] },
|
||||||
|
// The vault, before everything that keeps a secret from it: redis, gitea, umami, influxdb,
|
||||||
|
// mailu require one (novox/hq ADRs 0085, 0094).
|
||||||
|
{ name: "mesh-vault", containers: ["mesh-vault"] },
|
||||||
{ name: "redis", containers: ["redis", "mesh-redis"] },
|
{ name: "redis", containers: ["redis", "mesh-redis"] },
|
||||||
{ name: "minio", containers: ["minio", "mesh-minio"] },
|
{ name: "minio", containers: ["minio", "mesh-minio"] },
|
||||||
{ name: "mongodb", containers: ["mongo", "mesh-mongodb"] },
|
{ name: "mongodb", containers: ["mongo", "mesh-mongodb"] },
|
||||||
|
|||||||
Reference in New Issue
Block a user