@specific.dev/spectest 0.71.1 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/daemon.ts CHANGED
@@ -135,6 +135,7 @@ import {
135
135
  } from "./harness/ingress-table.js";
136
136
  import { startTlsTerminator, type TlsTerminator } from "./harness/tls-terminator.js";
137
137
  import { runContainerArgs } from "./harness/container-run.js";
138
+ import { IMAGE_CACHE_MANIFEST, imageCachePathsSync, isOnImageCache, type ImageCachePaths } from "./harness/image-cache.js";
138
139
  import {
139
140
  assertAbsolute,
140
141
  certificateHostnames,
@@ -714,7 +715,6 @@ async function hasBuildx(): Promise<boolean> {
714
715
  // in-VM buildkitd keeps its exported cache there too, so a fresh VM finds
715
716
  // every layer it built before. Detected once; if the daemon will not
716
717
  // start, dockerd's own BuildKit builds instead.
717
- const IMAGE_CACHE_MANIFEST = "/run/spectest-image-cache.json";
718
718
  const LOCAL_BUILDER_NAME = "spectest-local";
719
719
  const LOCAL_BUILDKIT_ADDR = "tcp://127.0.0.1:1234";
720
720
  /** The bring-up script the cache base bakes (`base.rs::BUILDKITD_UP_SH`). */
@@ -723,16 +723,8 @@ const BUILDKITD_UP_PATH = "/usr/local/bin/spectest-buildkitd-up";
723
723
  /** Where the control plane mounted this VM's image cache: containerd's
724
724
  * root (read-write, this VM's own) and the layers disk (read-only, shared
725
725
  * by every VM of a generation). `null` when the VM carries no cache. */
726
- async function imageCachePaths(): Promise<{ root: string; layers: string } | null> {
727
- try {
728
- const raw = await fs.readFile(IMAGE_CACHE_MANIFEST, "utf8");
729
- const parsed = JSON.parse(raw) as { disks?: { role?: string; path?: string }[] };
730
- const root = (parsed.disks ?? []).find((d) => d.role === "root" && d.path)?.path;
731
- const layers = (parsed.disks ?? []).find((d) => d.role === "layers" && d.path)?.path;
732
- return root && layers ? { root, layers } : null;
733
- } catch {
734
- return null;
735
- }
726
+ async function imageCachePaths(): Promise<ImageCachePaths | null> {
727
+ return imageCachePathsSync(IMAGE_CACHE_MANIFEST);
736
728
  }
737
729
 
738
730
  let _localBuilder: boolean | undefined;
@@ -876,7 +868,10 @@ async function ensureVolumes(svc: NamedService): Promise<string[]> {
876
868
  // serving older SDKs. Leaving it out of the manifest is what
877
869
  // protects a project running this SDK against a server whose
878
870
  // teardown guard predates it.
879
- const durable = host.startsWith("/var/cache/spectest/");
871
+ // A directory on a cache disk is the same kind of thing: a nested
872
+ // runtime's containerd root (`k3s()`), kept as a cache by the
873
+ // lineage exactly as the container store one level up is.
874
+ const durable = host.startsWith("/var/cache/spectest/") || isOnImageCache(host, imageCachePathsSync(IMAGE_CACHE_MANIFEST));
880
875
  if (vol.source?.startsWith("/") && !durable) {
881
876
  await recordAbsoluteVolumeDir(host);
882
877
  }
@@ -1244,7 +1239,6 @@ async function prepareServiceImage(
1244
1239
  if (tagr.code !== 0) {
1245
1240
  throw new Error(`docker tag ${ref} ${tag} failed: ${tagr.stderr.trim()}`);
1246
1241
  }
1247
- await ensureCaTrustedImage(svc.name, tag);
1248
1242
  return { tag };
1249
1243
  }
1250
1244
  // Dockerfile build. Within one bootstrap, identical definitions (shared
@@ -1288,61 +1282,6 @@ async function prepareServiceImage(
1288
1282
  return buildServiceImage(svc.name, image, tag);
1289
1283
  }
1290
1284
 
1291
- /** Printed by the folded CA step when it could not write the trust
1292
- * store at all — the one outcome that still needs the derivative build. */
1293
- const CA_FOLD_UNWRITABLE = "[spectest-ca] trust store not writable";
1294
-
1295
- /**
1296
- * The CA-trust steps as a suffix appended to a dockerfile service's OWN
1297
- * Dockerfile, so one build produces the finished image instead of a build
1298
- * plus a derivative rebuild ({@link ensureCaTrustedImage}) per service.
1299
- * Returns null when there is no CA to layer, or when the PEM can't be
1300
- * quoted — the caller then falls back to the derivative build.
1301
- *
1302
- * The PEM is written INLINE rather than `COPY`d: the build context is
1303
- * `/workspace` under a per-service ignore file that the project itself
1304
- * contributes to (a `**` line with re-includes is the common idiom), and
1305
- * a context path we don't control is a context path that can be excluded.
1306
- * printf needs nothing but a shell.
1307
- *
1308
- * Two rules make this safe to bolt onto user code. It must never fail the
1309
- * build — every branch ends in an echo, so the RUN exits 0 whatever the
1310
- * image lacks — and it must never change the image, beyond the trust
1311
- * store: notably no `USER root`, since we cannot know statically what
1312
- * user to hand back. An image that declares a non-root user therefore
1313
- * fails to write and is finished by the derivative build, which inspects
1314
- * the built image and can escalate properly.
1315
- */
1316
- async function caTrustSuffix(): Promise<string | null> {
1317
- if (!existsSync(CA_PATH)) return null;
1318
- const pem = (await fs.readFile(CA_PATH, "utf8")).trim();
1319
- // A quote in the PEM would break out of the shell quoting below. PEM is
1320
- // base64 and dashes, so this is a guard, not a case we expect.
1321
- if (!pem || pem.includes("'")) return null;
1322
- const args = pem
1323
- .split("\n")
1324
- .map((l) => `'${l.trimEnd()}'`)
1325
- .join(" ");
1326
- const dst = "/usr/local/share/ca-certificates/spectest-ca.crt";
1327
- return `
1328
- # spectest: trust the environment's root CA. Appended by the harness —
1329
- # not part of the project's Dockerfile.
1330
- RUN P='[spectest-ca]'; \\
1331
- mkdir -p /usr/local/share/ca-certificates 2>/dev/null; \\
1332
- if printf '%s\\n' ${args} > ${dst} 2>/dev/null; then \\
1333
- if command -v update-ca-certificates >/dev/null 2>&1 && update-ca-certificates >/dev/null 2>&1; then \\
1334
- echo "$P trusted via update-ca-certificates"; \\
1335
- elif command -v update-ca-trust >/dev/null 2>&1 && cp ${dst} /etc/pki/ca-trust/source/anchors/spectest-ca.crt && update-ca-trust extract >/dev/null 2>&1; then \\
1336
- echo "$P trusted via update-ca-trust"; \\
1337
- else \\
1338
- echo "$P no system CA trust tool in image; env-var trust only"; \\
1339
- fi; \\
1340
- else \\
1341
- echo "$P trust store not writable by this image's user"; \\
1342
- fi
1343
- `;
1344
- }
1345
-
1346
1285
  /** The built form of a dockerfile image. `resolveServiceImage` read a
1347
1286
  * `path` into `content` at config time, so a service that still carries
1348
1287
  * `path` here skipped that step — a programming error, not user input. */
@@ -1355,73 +1294,38 @@ function dockerfileContent(svc: NamedService): DockerfileBuild {
1355
1294
  }
1356
1295
 
1357
1296
  /**
1358
- * Build a dockerfile service's image.
1297
+ * Build a dockerfile service's image — the project's Dockerfile as
1298
+ * written, nothing appended.
1359
1299
  *
1360
- * The CA-trust layer is folded into THIS build when it can be (see
1361
- * {@link caTrustSuffix}), so a service costs one image build and one
1362
- * export rather than two. The derivative build stays as the fallback for
1363
- * everything the folded form can't serve: an image with no shell to run
1364
- * the step (distroless, scratch — the appended RUN can't execute, so the
1365
- * build fails and we rebuild the project's Dockerfile untouched), and an
1366
- * image that declares a non-root user (the step runs as that user and
1367
- * can't write the trust store).
1300
+ * Trust in the environment's CA is not the image's business any more:
1301
+ * `runContainer` mounts the combined bundle over the system trust store
1302
+ * (`container-run.ts`, `SYSTEM_TRUST_BUNDLE_PATHS`). Until 2026-09-06 a
1303
+ * CA step was folded into this build and a derivative `docker build`
1304
+ * finished any image the fold could not write to (non-root `USER`, no
1305
+ * shell), which was one or two extra builds per service, and one per
1306
+ * pulled image, on every cold start.
1368
1307
  */
1369
1308
  async function buildServiceImage(
1370
1309
  name: string,
1371
1310
  image: DockerfileBuild,
1372
1311
  tag: string,
1373
1312
  ): Promise<{ tag: string; buildSteps?: BuildStep[] }> {
1374
- const suffix = await caTrustSuffix();
1375
- let attempt = await runServiceBuild(name, image, tag, suffix);
1376
- if (!attempt.ok && suffix) {
1377
- // Our step must not be able to break a project's build.
1378
- // eslint-disable-next-line no-console
1379
- console.warn(
1380
- `[ca-trust] folded CA step could not run in ${name}'s image; rebuilding without it`,
1381
- );
1382
- attempt = await runServiceBuild(name, image, tag, null);
1383
- }
1313
+ const attempt = await runServiceBuild(name, image, tag);
1384
1314
  if (!attempt.ok) {
1385
1315
  progressService(name, { status: "failed" });
1386
1316
  throw new Error(`docker build for ${name} failed:\n${attempt.log}`);
1387
1317
  }
1388
- // The derivative build is still needed for the one thing the folded step
1389
- // cannot do: write the trust store of an image that does not run as
1390
- // root. That is decided on the IMAGE, not on the build log — a CACHED
1391
- // layer prints nothing, so a log-only check would quietly stop
1392
- // re-applying the moment BuildKit had the layer. The log covers the
1393
- // rarer case of a root image whose /etc is read-only.
1394
- //
1395
- // An image with no trust tool at all needs nothing further: the
1396
- // derivative build would reach the same dead end, and `runContainer`'s
1397
- // env vars are the fallback either way.
1398
- const needsDerivative =
1399
- !attempt.folded ||
1400
- attempt.log.includes(CA_FOLD_UNWRITABLE) ||
1401
- !(await imageRunsAsRoot(tag));
1402
- if (needsDerivative) await ensureCaTrustedImage(name, tag);
1403
1318
  return { tag, buildSteps: attempt.buildSteps };
1404
1319
  }
1405
1320
 
1406
- /** Whether `tag`'s declared `USER` is root (or unset, which means root). */
1407
- async function imageRunsAsRoot(tag: string): Promise<boolean> {
1408
- const user = (
1409
- await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)
1410
- ).stdout.trim();
1411
- return user === "" || user === "root" || user === "0";
1412
- }
1413
-
1414
1321
  async function runServiceBuild(
1415
1322
  name: string,
1416
1323
  image: DockerfileBuild,
1417
1324
  tag: string,
1418
- caSuffix: string | null,
1419
- ): Promise<{ ok: boolean; folded: boolean; log: string; buildSteps?: BuildStep[] }> {
1325
+ ): Promise<{ ok: boolean; log: string; buildSteps?: BuildStep[] }> {
1420
1326
  let buildSteps: BuildStep[] | undefined;
1421
1327
  {
1422
- const content = caSuffix
1423
- ? `${image.content.replace(/\n*$/, "\n")}${caSuffix}`
1424
- : image.content;
1328
+ const content = image.content;
1425
1329
  const dfDir = path.join(WORKSPACE, ".spectest", "services", name);
1426
1330
  await fs.mkdir(dfDir, { recursive: true });
1427
1331
  const dfPath = path.join(dfDir, "Dockerfile");
@@ -1519,9 +1423,7 @@ async function runServiceBuild(
1519
1423
  });
1520
1424
  const log = `${build.stderr.trim()}\n${build.stdout.trim()}`;
1521
1425
  if (build.code !== 0) {
1522
- // The caller decides whether this is fatal: a failure with the CA
1523
- // step appended is retried without it before anyone hears about it.
1524
- return { ok: false, folded: caSuffix !== null, log };
1426
+ return { ok: false, log };
1525
1427
  }
1526
1428
  if (useBuildKit) {
1527
1429
  // Keep only the slowest dozen steps ≥1s — enough to profile, small
@@ -1530,55 +1432,7 @@ async function runServiceBuild(
1530
1432
  .filter((s) => s.secs >= 1)
1531
1433
  .slice(0, 12);
1532
1434
  }
1533
- return { ok: true, folded: caSuffix !== null, log, buildSteps };
1534
- }
1535
- }
1536
-
1537
- /**
1538
- * Build a derivative image on top of `tag` that copies the spectest
1539
- * root CA into the system trust store. Tagged back as `tag`, so the
1540
- * rest of the orchestrator (runContainer, image cache) is oblivious.
1541
- * Failures are warned-and-ignored: the env-var injection in
1542
- * runContainer is the universal fallback, so apps that use it (most
1543
- * Node/Python/Ruby/AWS clients) still trust the CA even when the
1544
- * image's trust store can't be updated.
1545
- */
1546
- async function ensureCaTrustedImage(serviceName: string, tag: string): Promise<void> {
1547
- if (!existsSync(CA_PATH)) {
1548
- // Daemon running outside a base-snapshot VM (dev/test). Nothing to
1549
- // layer; env vars also harmless (they point at a missing path, but
1550
- // most consumers ignore missing files).
1551
- return;
1552
- }
1553
- const ctxDir = path.join(WORKSPACE, ".spectest", "ca-trust", serviceName);
1554
- await fs.mkdir(ctxDir, { recursive: true });
1555
- await fs.copyFile(CA_PATH, path.join(ctxDir, "spectest-ca.crt"));
1556
- // An image that declares a non-root `USER` runs this step as that user,
1557
- // and writing the trust store then fails ("cannot create
1558
- // /etc/ssl/certs/ca-certificates.crt.new: Permission denied") — the CA
1559
- // silently never lands. Take root for the one command, then hand the
1560
- // image back its own user so containers still run as it.
1561
- const declaredUser = (
1562
- await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)
1563
- ).stdout.trim();
1564
- const needsRoot = declaredUser !== "" && declaredUser !== "root" && declaredUser !== "0";
1565
- const dockerfile = `FROM ${tag}
1566
- ${needsRoot ? "USER root\n" : ""}COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
1567
- RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
1568
- update-ca-certificates; \\
1569
- elif command -v update-ca-trust >/dev/null 2>&1; then \\
1570
- cp /usr/local/share/ca-certificates/spectest-ca.crt /etc/pki/ca-trust/source/anchors/spectest-ca.crt && update-ca-trust extract; \\
1571
- else \\
1572
- echo "[spectest] no system CA trust tool in image; env-var trust only"; \\
1573
- fi
1574
- ${needsRoot ? `USER ${declaredUser}\n` : ""}`;
1575
- await fs.writeFile(path.join(ctxDir, "Dockerfile"), dockerfile);
1576
- const build = await docker(["build", "-t", tag, ctxDir], 300_000);
1577
- if (build.code !== 0) {
1578
- // eslint-disable-next-line no-console
1579
- console.warn(
1580
- `[ca-trust] could not layer spectest CA into ${serviceName} (${tag}); env-var fallback only:\n${build.stderr.trim() || build.stdout.trim()}`,
1581
- );
1435
+ return { ok: true, log, buildSteps };
1582
1436
  }
1583
1437
  }
1584
1438
 
@@ -1590,10 +1444,14 @@ let caBundlePromise: Promise<string | null> | null = null;
1590
1444
  * return its path, or null when there's no CA to trust (daemon running
1591
1445
  * outside a base-snapshot VM).
1592
1446
  *
1593
- * This is what the replace-semantics trust env vars must point at. Falling
1594
- * back to the bare CA when the guest bundle is unreadable keeps the old
1595
- * behaviour — fakes verify, public HTTPS doesn't — which is strictly better
1596
- * than dropping the CA and breaking the fakes everything else depends on.
1447
+ * This is what the replace-semantics trust env vars must point at, and
1448
+ * what `runContainer` mounts over the image's own system trust store
1449
+ * (`container-run.ts`). Falling back to the bare CA when the guest bundle
1450
+ * is unreadable keeps the old behaviour for the variables — fakes verify,
1451
+ * public HTTPS doesn't — which is strictly better than dropping the CA and
1452
+ * breaking the fakes everything else depends on; the system-store mount
1453
+ * is skipped in that case, since the bare CA there would take the public
1454
+ * roots away from clients that read no variable.
1597
1455
  */
1598
1456
  async function ensureCaBundle(): Promise<string | null> {
1599
1457
  caBundlePromise ??= (async () => {
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { TCP_RETRIES2, runContainerArgs, type ContainerRunInput } from "./container-run";
3
+ import {
4
+ SYSTEM_TRUST_BUNDLE_PATHS,
5
+ TCP_RETRIES2,
6
+ runContainerArgs,
7
+ type ContainerRunInput,
8
+ } from "./container-run";
4
9
 
5
10
  const base = (over: Partial<ContainerRunInput> = {}): ContainerRunInput => ({
6
11
  svc: { name: "web" },
@@ -122,6 +127,31 @@ describe("certificate trust", () => {
122
127
  const mounts = args.filter((a) => a === "--volume=/etc/spectest/ca.crt:/etc/spectest/ca.crt:ro");
123
128
  expect(mounts).toHaveLength(1);
124
129
  });
130
+
131
+ /** Clients that read no variable — Go, curl, OpenSSL defaults, Python's
132
+ * `ssl` — open the distro's bundle file. The combined bundle is mounted
133
+ * over each known location, read-only, in place of the derivative
134
+ * image build this replaced. */
135
+ test("mounts the bundle over the system trust store paths", () => {
136
+ const args = runContainerArgs(base({ caBundle: "/etc/spectest/ca-bundle.crt" }));
137
+ expect(SYSTEM_TRUST_BUNDLE_PATHS).toContain("/etc/ssl/certs/ca-certificates.crt");
138
+ expect(SYSTEM_TRUST_BUNDLE_PATHS).toContain("/etc/pki/tls/certs/ca-bundle.crt");
139
+ for (const p of SYSTEM_TRUST_BUNDLE_PATHS) {
140
+ expect(args).toContain(`--volume=/etc/spectest/ca-bundle.crt:${p}:ro`);
141
+ }
142
+ });
143
+
144
+ /** The bare CA over the system store would drop every public root, the
145
+ * same trap as the replacing variables — so no bundle means no mount. */
146
+ test("never mounts the bare CA over the system trust store", () => {
147
+ // No bundle at all, and the fallback where the bundle IS the bare CA.
148
+ for (const caBundle of [null, "/etc/spectest/ca.crt"]) {
149
+ const args = runContainerArgs(base({ caBundle }));
150
+ for (const p of SYSTEM_TRUST_BUNDLE_PATHS) {
151
+ expect(args.some((a) => a.endsWith(`:${p}:ro`))).toBe(false);
152
+ }
153
+ }
154
+ });
125
155
  });
126
156
 
127
157
  describe("service configuration", () => {
@@ -35,6 +35,20 @@
35
35
  * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
36
  * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
37
  *
38
+ * **The system trust store is the fourth, and it is a mount, not a
39
+ * layer.** Go, curl, OpenSSL's defaults and Python's `ssl` with no
40
+ * variable set read the distro's bundle file directly, so the combined
41
+ * bundle is bind-mounted read-only over the paths those bundles live at
42
+ * ({@link SYSTEM_TRUST_BUNDLE_PATHS}). Until 2026-09-06 this was a
43
+ * derivative image build per service (`COPY` the CA, run
44
+ * `update-ca-certificates`) — a `docker build` for every image, built or
45
+ * pulled, on every cold start, and most of what a cached bring-up still
46
+ * paid. A mount costs nothing, needs no cache, and behaves the same on a
47
+ * warm, delta or cold start. What it does not cover: a Java keystore
48
+ * (`cacerts`), and an entrypoint that runs `update-ca-certificates`
49
+ * itself — its final `mv` over the bundle fails with EBUSY on a
50
+ * mountpoint.
51
+ *
38
52
  * **`command` and `args` are different overrides.** `command` replaces the
39
53
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
40
54
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -95,6 +109,22 @@ export interface ContainerRunInput {
95
109
  */
96
110
  export const TCP_RETRIES2 = 6;
97
111
 
112
+ /**
113
+ * Where a distro keeps the bundle that clients reading the system store
114
+ * open. Debian, Ubuntu and Alpine resolve every default path to the first
115
+ * through symlinks (`/etc/ssl/cert.pem`, `/usr/lib/ssl/cert.pem`); the
116
+ * RHEL family resolves to the second (itself a symlink into
117
+ * `/etc/pki/ca-trust/extracted/pem/`, which the runtime follows before it
118
+ * mounts). Go checks both names itself. An image with neither path gets
119
+ * the file created by the mount, which is what a scratch image with a
120
+ * Go binary wants. Read-only, so a container cannot change what its
121
+ * siblings trust.
122
+ */
123
+ export const SYSTEM_TRUST_BUNDLE_PATHS = [
124
+ "/etc/ssl/certs/ca-certificates.crt",
125
+ "/etc/pki/tls/certs/ca-bundle.crt",
126
+ ] as const;
127
+
98
128
  /** Build the full `docker run` argv. Pure: no I/O, no module state. */
99
129
  export function runContainerArgs(input: ContainerRunInput): string[] {
100
130
  const { svc, tag, network } = input;
@@ -151,6 +181,13 @@ export function runContainerArgs(input: ContainerRunInput): string[] {
151
181
  args.push("-e", `SSL_CERT_FILE=${bundle}`);
152
182
  args.push("-e", `REQUESTS_CA_BUNDLE=${bundle}`);
153
183
  args.push("-e", `AWS_CA_BUNDLE=${bundle}`);
184
+ // The system store itself, for clients that read no variable at all.
185
+ // Only a real bundle: the caller falls back to the bare CA when the
186
+ // guest's roots are unreadable, and the bare CA over the system store
187
+ // would leave Go and curl trusting spectest and nothing else.
188
+ if (bundle !== input.caPath) {
189
+ for (const p of SYSTEM_TRUST_BUNDLE_PATHS) args.push(`--volume=${bundle}:${p}:ro`);
190
+ }
154
191
  }
155
192
 
156
193
  // The service's own env comes after ours, so a project can override the
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The image cache as the guest sees it (`CONTAINER_STORE.md`).
3
+ *
4
+ * The control plane writes one manifest per VM at start naming the two
5
+ * cache disks it attached: the **root** (containerd's own root,
6
+ * read-write, this VM's clone) and the **layers** disk (read-only, one
7
+ * EROFS file per layer, shared by every VM of a generation). Both paths
8
+ * are fixed by the control plane; the manifest is how a harness learns
9
+ * whether this VM carries a cache at all (the fake backend does not,
10
+ * and neither does a server older than the cache).
11
+ *
12
+ * Read synchronously as well as asynchronously: a component's service
13
+ * definition is built inside `defineEnvironment`, which is synchronous,
14
+ * and `k3s()` decides its mounts there.
15
+ */
16
+
17
+ import { existsSync, readFileSync } from "node:fs";
18
+
19
+ /** Written by `env.rs` before the harness starts. */
20
+ export const IMAGE_CACHE_MANIFEST = "/run/spectest-image-cache.json";
21
+
22
+ /** Where the cache's paths are, when this VM carries one. */
23
+ export interface ImageCachePaths {
24
+ /** containerd's root: read-write, this VM's own clone. */
25
+ root: string;
26
+ /** The layers disk: read-only, shared by every VM of a generation. */
27
+ layers: string;
28
+ }
29
+
30
+ /**
31
+ * Directory under the root disk holding a nested runtime's containerd
32
+ * root, one per service: `<root>/spectest-nested/<service>`. The merge
33
+ * (`image_cache/merge.rs::NESTED_DIR`) reads every store it finds there
34
+ * exactly as it reads the disk's own.
35
+ */
36
+ export const NESTED_STORES_DIR = "spectest-nested";
37
+
38
+ /** The guest's static `mkfs.erofs`, which a nested runtime's EROFS
39
+ * differ needs and no runtime image ships. */
40
+ export const MKFS_EROFS_PATH = "/usr/local/bin/mkfs.erofs";
41
+
42
+ /** The guest's adopt helper (`base.rs::STORE_ADOPT_SH`), POSIX sh so a
43
+ * nested runtime's busybox can run the same file. */
44
+ export const STORE_ADOPT_PATH = "/usr/local/bin/spectest-store-adopt";
45
+
46
+ function parse(raw: string): ImageCachePaths | null {
47
+ const parsed = JSON.parse(raw) as { disks?: { role?: string; path?: string }[] };
48
+ const root = (parsed.disks ?? []).find((d) => d.role === "root" && d.path)?.path;
49
+ const layers = (parsed.disks ?? []).find((d) => d.role === "layers" && d.path)?.path;
50
+ return root && layers ? { root, layers } : null;
51
+ }
52
+
53
+ /** The cache's paths, or `null` when this VM carries none.
54
+ * `SPECTEST_IMAGE_CACHE_MANIFEST` points a test at another file. */
55
+ export function imageCachePathsSync(
56
+ manifest: string = process.env.SPECTEST_IMAGE_CACHE_MANIFEST || IMAGE_CACHE_MANIFEST,
57
+ ): ImageCachePaths | null {
58
+ try {
59
+ if (!existsSync(manifest)) return null;
60
+ return parse(readFileSync(manifest, "utf8"));
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /** The host directory a nested runtime keeps its containerd root in. */
67
+ export function nestedStoreDir(paths: ImageCachePaths, service: string): string {
68
+ return `${paths.root}/${NESTED_STORES_DIR}/${service}`;
69
+ }
70
+
71
+ /**
72
+ * Is a volume's host path on a cache disk? Such a directory is a cache
73
+ * the lineage keeps — like the container store one level up — and the
74
+ * delta-restore teardown must not wipe it.
75
+ */
76
+ export function isOnImageCache(hostPath: string, paths: ImageCachePaths | null): boolean {
77
+ if (!paths) return false;
78
+ return hostPath === paths.root || hostPath.startsWith(`${paths.root}/`) || hostPath === paths.layers || hostPath.startsWith(`${paths.layers}/`);
79
+ }
@@ -63,8 +63,9 @@ export function sanitizeSegment(p: string): string {
63
63
  * `files` does: a component cannot know the map key the user will give it,
64
64
  * and an **absolute** source gets no automatic per-service directory. A
65
65
  * component that needs one — a nested runtime keeping its store under
66
- * {@link NESTED_STORE_ROOT}, where two of them sharing one directory would
67
- * be two daemons on one metadata store — writes the token into the path.
66
+ * the image cache's `spectest-nested/<service>` (`image-cache.ts::nestedStoreDir`),
67
+ * where two of them sharing one directory would be two daemons on one
68
+ * metadata store — writes the token into the path.
68
69
  *
69
70
  * `workspace` is a parameter rather than a module constant so the rule is
70
71
  * testable without touching the filesystem.