@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/dist/components/k3s.d.ts +30 -16
- package/dist/components/k3s.js +207 -108
- package/dist/daemon.js +28 -160
- package/dist/harness/container-run.d.ts +26 -0
- package/dist/harness/container-run.js +37 -0
- package/dist/harness/image-cache.d.ts +48 -0
- package/dist/harness/image-cache.js +63 -0
- package/dist/harness/volume-paths.d.ts +3 -2
- package/dist/harness/volume-paths.js +3 -2
- package/package.json +1 -1
- package/src/components/k3s.ts +226 -111
- package/src/daemon.ts +29 -171
- package/src/harness/container-run.test.ts +31 -1
- package/src/harness/container-run.ts +37 -0
- package/src/harness/image-cache.ts +79 -0
- package/src/harness/volume-paths.ts +3 -2
package/dist/daemon.js
CHANGED
|
@@ -50,6 +50,7 @@ import { certCovers as hostmatchCertCovers, hostWithoutPort, matchRoute, selectC
|
|
|
50
50
|
import { INGRESS_HTTPS_PORT, INGRESS_HTTP_PORT, bindRoute, clearTables, emptyTables, planBind, registryTarget, routesFor, unbindRoute, } from "./harness/ingress-table.js";
|
|
51
51
|
import { startTlsTerminator } from "./harness/tls-terminator.js";
|
|
52
52
|
import { runContainerArgs } from "./harness/container-run.js";
|
|
53
|
+
import { IMAGE_CACHE_MANIFEST, imageCachePathsSync, isOnImageCache } from "./harness/image-cache.js";
|
|
53
54
|
import { assertAbsolute, certificateHostnames, defaultKeyMode, expandServiceToken, isNoopChown, mountFlag, needsIdTables, numericId, resolveChownIds, } from "./harness/file-mounts.js";
|
|
54
55
|
import { conflict, notFound, requireString, } from "./harness/methods.js";
|
|
55
56
|
import { openTerminal } from "./terminal.js";
|
|
@@ -448,7 +449,6 @@ async function hasBuildx() {
|
|
|
448
449
|
// in-VM buildkitd keeps its exported cache there too, so a fresh VM finds
|
|
449
450
|
// every layer it built before. Detected once; if the daemon will not
|
|
450
451
|
// start, dockerd's own BuildKit builds instead.
|
|
451
|
-
const IMAGE_CACHE_MANIFEST = "/run/spectest-image-cache.json";
|
|
452
452
|
const LOCAL_BUILDER_NAME = "spectest-local";
|
|
453
453
|
const LOCAL_BUILDKIT_ADDR = "tcp://127.0.0.1:1234";
|
|
454
454
|
/** The bring-up script the cache base bakes (`base.rs::BUILDKITD_UP_SH`). */
|
|
@@ -457,16 +457,7 @@ const BUILDKITD_UP_PATH = "/usr/local/bin/spectest-buildkitd-up";
|
|
|
457
457
|
* root (read-write, this VM's own) and the layers disk (read-only, shared
|
|
458
458
|
* by every VM of a generation). `null` when the VM carries no cache. */
|
|
459
459
|
async function imageCachePaths() {
|
|
460
|
-
|
|
461
|
-
const raw = await fs.readFile(IMAGE_CACHE_MANIFEST, "utf8");
|
|
462
|
-
const parsed = JSON.parse(raw);
|
|
463
|
-
const root = (parsed.disks ?? []).find((d) => d.role === "root" && d.path)?.path;
|
|
464
|
-
const layers = (parsed.disks ?? []).find((d) => d.role === "layers" && d.path)?.path;
|
|
465
|
-
return root && layers ? { root, layers } : null;
|
|
466
|
-
}
|
|
467
|
-
catch {
|
|
468
|
-
return null;
|
|
469
|
-
}
|
|
460
|
+
return imageCachePathsSync(IMAGE_CACHE_MANIFEST);
|
|
470
461
|
}
|
|
471
462
|
let _localBuilder;
|
|
472
463
|
/**
|
|
@@ -602,7 +593,10 @@ async function ensureVolumes(svc) {
|
|
|
602
593
|
// serving older SDKs. Leaving it out of the manifest is what
|
|
603
594
|
// protects a project running this SDK against a server whose
|
|
604
595
|
// teardown guard predates it.
|
|
605
|
-
|
|
596
|
+
// A directory on a cache disk is the same kind of thing: a nested
|
|
597
|
+
// runtime's containerd root (`k3s()`), kept as a cache by the
|
|
598
|
+
// lineage exactly as the container store one level up is.
|
|
599
|
+
const durable = host.startsWith("/var/cache/spectest/") || isOnImageCache(host, imageCachePathsSync(IMAGE_CACHE_MANIFEST));
|
|
606
600
|
if (vol.source?.startsWith("/") && !durable) {
|
|
607
601
|
await recordAbsoluteVolumeDir(host);
|
|
608
602
|
}
|
|
@@ -919,7 +913,6 @@ async function prepareServiceImage(svc, opts) {
|
|
|
919
913
|
if (tagr.code !== 0) {
|
|
920
914
|
throw new Error(`docker tag ${ref} ${tag} failed: ${tagr.stderr.trim()}`);
|
|
921
915
|
}
|
|
922
|
-
await ensureCaTrustedImage(svc.name, tag);
|
|
923
916
|
return { tag };
|
|
924
917
|
}
|
|
925
918
|
// Dockerfile build. Within one bootstrap, identical definitions (shared
|
|
@@ -964,61 +957,6 @@ async function prepareServiceImage(svc, opts) {
|
|
|
964
957
|
}
|
|
965
958
|
return buildServiceImage(svc.name, image, tag);
|
|
966
959
|
}
|
|
967
|
-
/** Printed by the folded CA step when it could not write the trust
|
|
968
|
-
* store at all — the one outcome that still needs the derivative build. */
|
|
969
|
-
const CA_FOLD_UNWRITABLE = "[spectest-ca] trust store not writable";
|
|
970
|
-
/**
|
|
971
|
-
* The CA-trust steps as a suffix appended to a dockerfile service's OWN
|
|
972
|
-
* Dockerfile, so one build produces the finished image instead of a build
|
|
973
|
-
* plus a derivative rebuild ({@link ensureCaTrustedImage}) per service.
|
|
974
|
-
* Returns null when there is no CA to layer, or when the PEM can't be
|
|
975
|
-
* quoted — the caller then falls back to the derivative build.
|
|
976
|
-
*
|
|
977
|
-
* The PEM is written INLINE rather than `COPY`d: the build context is
|
|
978
|
-
* `/workspace` under a per-service ignore file that the project itself
|
|
979
|
-
* contributes to (a `**` line with re-includes is the common idiom), and
|
|
980
|
-
* a context path we don't control is a context path that can be excluded.
|
|
981
|
-
* printf needs nothing but a shell.
|
|
982
|
-
*
|
|
983
|
-
* Two rules make this safe to bolt onto user code. It must never fail the
|
|
984
|
-
* build — every branch ends in an echo, so the RUN exits 0 whatever the
|
|
985
|
-
* image lacks — and it must never change the image, beyond the trust
|
|
986
|
-
* store: notably no `USER root`, since we cannot know statically what
|
|
987
|
-
* user to hand back. An image that declares a non-root user therefore
|
|
988
|
-
* fails to write and is finished by the derivative build, which inspects
|
|
989
|
-
* the built image and can escalate properly.
|
|
990
|
-
*/
|
|
991
|
-
async function caTrustSuffix() {
|
|
992
|
-
if (!existsSync(CA_PATH))
|
|
993
|
-
return null;
|
|
994
|
-
const pem = (await fs.readFile(CA_PATH, "utf8")).trim();
|
|
995
|
-
// A quote in the PEM would break out of the shell quoting below. PEM is
|
|
996
|
-
// base64 and dashes, so this is a guard, not a case we expect.
|
|
997
|
-
if (!pem || pem.includes("'"))
|
|
998
|
-
return null;
|
|
999
|
-
const args = pem
|
|
1000
|
-
.split("\n")
|
|
1001
|
-
.map((l) => `'${l.trimEnd()}'`)
|
|
1002
|
-
.join(" ");
|
|
1003
|
-
const dst = "/usr/local/share/ca-certificates/spectest-ca.crt";
|
|
1004
|
-
return `
|
|
1005
|
-
# spectest: trust the environment's root CA. Appended by the harness —
|
|
1006
|
-
# not part of the project's Dockerfile.
|
|
1007
|
-
RUN P='[spectest-ca]'; \\
|
|
1008
|
-
mkdir -p /usr/local/share/ca-certificates 2>/dev/null; \\
|
|
1009
|
-
if printf '%s\\n' ${args} > ${dst} 2>/dev/null; then \\
|
|
1010
|
-
if command -v update-ca-certificates >/dev/null 2>&1 && update-ca-certificates >/dev/null 2>&1; then \\
|
|
1011
|
-
echo "$P trusted via update-ca-certificates"; \\
|
|
1012
|
-
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 \\
|
|
1013
|
-
echo "$P trusted via update-ca-trust"; \\
|
|
1014
|
-
else \\
|
|
1015
|
-
echo "$P no system CA trust tool in image; env-var trust only"; \\
|
|
1016
|
-
fi; \\
|
|
1017
|
-
else \\
|
|
1018
|
-
echo "$P trust store not writable by this image's user"; \\
|
|
1019
|
-
fi
|
|
1020
|
-
`;
|
|
1021
|
-
}
|
|
1022
960
|
/** The built form of a dockerfile image. `resolveServiceImage` read a
|
|
1023
961
|
* `path` into `content` at config time, so a service that still carries
|
|
1024
962
|
* `path` here skipped that step — a programming error, not user input. */
|
|
@@ -1030,58 +968,29 @@ function dockerfileContent(svc) {
|
|
|
1030
968
|
return image;
|
|
1031
969
|
}
|
|
1032
970
|
/**
|
|
1033
|
-
* Build a dockerfile service's image
|
|
971
|
+
* Build a dockerfile service's image — the project's Dockerfile as
|
|
972
|
+
* written, nothing appended.
|
|
1034
973
|
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
* image
|
|
1042
|
-
* can't write the trust store).
|
|
974
|
+
* Trust in the environment's CA is not the image's business any more:
|
|
975
|
+
* `runContainer` mounts the combined bundle over the system trust store
|
|
976
|
+
* (`container-run.ts`, `SYSTEM_TRUST_BUNDLE_PATHS`). Until 2026-09-06 a
|
|
977
|
+
* CA step was folded into this build and a derivative `docker build`
|
|
978
|
+
* finished any image the fold could not write to (non-root `USER`, no
|
|
979
|
+
* shell), which was one or two extra builds per service, and one per
|
|
980
|
+
* pulled image, on every cold start.
|
|
1043
981
|
*/
|
|
1044
982
|
async function buildServiceImage(name, image, tag) {
|
|
1045
|
-
const
|
|
1046
|
-
let attempt = await runServiceBuild(name, image, tag, suffix);
|
|
1047
|
-
if (!attempt.ok && suffix) {
|
|
1048
|
-
// Our step must not be able to break a project's build.
|
|
1049
|
-
// eslint-disable-next-line no-console
|
|
1050
|
-
console.warn(`[ca-trust] folded CA step could not run in ${name}'s image; rebuilding without it`);
|
|
1051
|
-
attempt = await runServiceBuild(name, image, tag, null);
|
|
1052
|
-
}
|
|
983
|
+
const attempt = await runServiceBuild(name, image, tag);
|
|
1053
984
|
if (!attempt.ok) {
|
|
1054
985
|
progressService(name, { status: "failed" });
|
|
1055
986
|
throw new Error(`docker build for ${name} failed:\n${attempt.log}`);
|
|
1056
987
|
}
|
|
1057
|
-
// The derivative build is still needed for the one thing the folded step
|
|
1058
|
-
// cannot do: write the trust store of an image that does not run as
|
|
1059
|
-
// root. That is decided on the IMAGE, not on the build log — a CACHED
|
|
1060
|
-
// layer prints nothing, so a log-only check would quietly stop
|
|
1061
|
-
// re-applying the moment BuildKit had the layer. The log covers the
|
|
1062
|
-
// rarer case of a root image whose /etc is read-only.
|
|
1063
|
-
//
|
|
1064
|
-
// An image with no trust tool at all needs nothing further: the
|
|
1065
|
-
// derivative build would reach the same dead end, and `runContainer`'s
|
|
1066
|
-
// env vars are the fallback either way.
|
|
1067
|
-
const needsDerivative = !attempt.folded ||
|
|
1068
|
-
attempt.log.includes(CA_FOLD_UNWRITABLE) ||
|
|
1069
|
-
!(await imageRunsAsRoot(tag));
|
|
1070
|
-
if (needsDerivative)
|
|
1071
|
-
await ensureCaTrustedImage(name, tag);
|
|
1072
988
|
return { tag, buildSteps: attempt.buildSteps };
|
|
1073
989
|
}
|
|
1074
|
-
|
|
1075
|
-
async function imageRunsAsRoot(tag) {
|
|
1076
|
-
const user = (await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)).stdout.trim();
|
|
1077
|
-
return user === "" || user === "root" || user === "0";
|
|
1078
|
-
}
|
|
1079
|
-
async function runServiceBuild(name, image, tag, caSuffix) {
|
|
990
|
+
async function runServiceBuild(name, image, tag) {
|
|
1080
991
|
let buildSteps;
|
|
1081
992
|
{
|
|
1082
|
-
const content =
|
|
1083
|
-
? `${image.content.replace(/\n*$/, "\n")}${caSuffix}`
|
|
1084
|
-
: image.content;
|
|
993
|
+
const content = image.content;
|
|
1085
994
|
const dfDir = path.join(WORKSPACE, ".spectest", "services", name);
|
|
1086
995
|
await fs.mkdir(dfDir, { recursive: true });
|
|
1087
996
|
const dfPath = path.join(dfDir, "Dockerfile");
|
|
@@ -1182,9 +1091,7 @@ async function runServiceBuild(name, image, tag, caSuffix) {
|
|
|
1182
1091
|
});
|
|
1183
1092
|
const log = `${build.stderr.trim()}\n${build.stdout.trim()}`;
|
|
1184
1093
|
if (build.code !== 0) {
|
|
1185
|
-
|
|
1186
|
-
// step appended is retried without it before anyone hears about it.
|
|
1187
|
-
return { ok: false, folded: caSuffix !== null, log };
|
|
1094
|
+
return { ok: false, log };
|
|
1188
1095
|
}
|
|
1189
1096
|
if (useBuildKit) {
|
|
1190
1097
|
// Keep only the slowest dozen steps ≥1s — enough to profile, small
|
|
@@ -1193,50 +1100,7 @@ async function runServiceBuild(name, image, tag, caSuffix) {
|
|
|
1193
1100
|
.filter((s) => s.secs >= 1)
|
|
1194
1101
|
.slice(0, 12);
|
|
1195
1102
|
}
|
|
1196
|
-
return { ok: true,
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Build a derivative image on top of `tag` that copies the spectest
|
|
1201
|
-
* root CA into the system trust store. Tagged back as `tag`, so the
|
|
1202
|
-
* rest of the orchestrator (runContainer, image cache) is oblivious.
|
|
1203
|
-
* Failures are warned-and-ignored: the env-var injection in
|
|
1204
|
-
* runContainer is the universal fallback, so apps that use it (most
|
|
1205
|
-
* Node/Python/Ruby/AWS clients) still trust the CA even when the
|
|
1206
|
-
* image's trust store can't be updated.
|
|
1207
|
-
*/
|
|
1208
|
-
async function ensureCaTrustedImage(serviceName, tag) {
|
|
1209
|
-
if (!existsSync(CA_PATH)) {
|
|
1210
|
-
// Daemon running outside a base-snapshot VM (dev/test). Nothing to
|
|
1211
|
-
// layer; env vars also harmless (they point at a missing path, but
|
|
1212
|
-
// most consumers ignore missing files).
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
const ctxDir = path.join(WORKSPACE, ".spectest", "ca-trust", serviceName);
|
|
1216
|
-
await fs.mkdir(ctxDir, { recursive: true });
|
|
1217
|
-
await fs.copyFile(CA_PATH, path.join(ctxDir, "spectest-ca.crt"));
|
|
1218
|
-
// An image that declares a non-root `USER` runs this step as that user,
|
|
1219
|
-
// and writing the trust store then fails ("cannot create
|
|
1220
|
-
// /etc/ssl/certs/ca-certificates.crt.new: Permission denied") — the CA
|
|
1221
|
-
// silently never lands. Take root for the one command, then hand the
|
|
1222
|
-
// image back its own user so containers still run as it.
|
|
1223
|
-
const declaredUser = (await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)).stdout.trim();
|
|
1224
|
-
const needsRoot = declaredUser !== "" && declaredUser !== "root" && declaredUser !== "0";
|
|
1225
|
-
const dockerfile = `FROM ${tag}
|
|
1226
|
-
${needsRoot ? "USER root\n" : ""}COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
|
|
1227
|
-
RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
|
|
1228
|
-
update-ca-certificates; \\
|
|
1229
|
-
elif command -v update-ca-trust >/dev/null 2>&1; then \\
|
|
1230
|
-
cp /usr/local/share/ca-certificates/spectest-ca.crt /etc/pki/ca-trust/source/anchors/spectest-ca.crt && update-ca-trust extract; \\
|
|
1231
|
-
else \\
|
|
1232
|
-
echo "[spectest] no system CA trust tool in image; env-var trust only"; \\
|
|
1233
|
-
fi
|
|
1234
|
-
${needsRoot ? `USER ${declaredUser}\n` : ""}`;
|
|
1235
|
-
await fs.writeFile(path.join(ctxDir, "Dockerfile"), dockerfile);
|
|
1236
|
-
const build = await docker(["build", "-t", tag, ctxDir], 300_000);
|
|
1237
|
-
if (build.code !== 0) {
|
|
1238
|
-
// eslint-disable-next-line no-console
|
|
1239
|
-
console.warn(`[ca-trust] could not layer spectest CA into ${serviceName} (${tag}); env-var fallback only:\n${build.stderr.trim() || build.stdout.trim()}`);
|
|
1103
|
+
return { ok: true, log, buildSteps };
|
|
1240
1104
|
}
|
|
1241
1105
|
}
|
|
1242
1106
|
/** Memoized across containers — the CA never rotates within a VM's life. */
|
|
@@ -1246,10 +1110,14 @@ let caBundlePromise = null;
|
|
|
1246
1110
|
* return its path, or null when there's no CA to trust (daemon running
|
|
1247
1111
|
* outside a base-snapshot VM).
|
|
1248
1112
|
*
|
|
1249
|
-
* This is what the replace-semantics trust env vars must point at
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1113
|
+
* This is what the replace-semantics trust env vars must point at, and
|
|
1114
|
+
* what `runContainer` mounts over the image's own system trust store
|
|
1115
|
+
* (`container-run.ts`). Falling back to the bare CA when the guest bundle
|
|
1116
|
+
* is unreadable keeps the old behaviour for the variables — fakes verify,
|
|
1117
|
+
* public HTTPS doesn't — which is strictly better than dropping the CA and
|
|
1118
|
+
* breaking the fakes everything else depends on; the system-store mount
|
|
1119
|
+
* is skipped in that case, since the bare CA there would take the public
|
|
1120
|
+
* roots away from clients that read no variable.
|
|
1253
1121
|
*/
|
|
1254
1122
|
async function ensureCaBundle() {
|
|
1255
1123
|
caBundlePromise ??= (async () => {
|
|
@@ -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
|
|
@@ -91,5 +105,17 @@ export interface ContainerRunInput {
|
|
|
91
105
|
* never `--network=host`, where writing `net.*` is denied.
|
|
92
106
|
*/
|
|
93
107
|
export declare const TCP_RETRIES2 = 6;
|
|
108
|
+
/**
|
|
109
|
+
* Where a distro keeps the bundle that clients reading the system store
|
|
110
|
+
* open. Debian, Ubuntu and Alpine resolve every default path to the first
|
|
111
|
+
* through symlinks (`/etc/ssl/cert.pem`, `/usr/lib/ssl/cert.pem`); the
|
|
112
|
+
* RHEL family resolves to the second (itself a symlink into
|
|
113
|
+
* `/etc/pki/ca-trust/extracted/pem/`, which the runtime follows before it
|
|
114
|
+
* mounts). Go checks both names itself. An image with neither path gets
|
|
115
|
+
* the file created by the mount, which is what a scratch image with a
|
|
116
|
+
* Go binary wants. Read-only, so a container cannot change what its
|
|
117
|
+
* siblings trust.
|
|
118
|
+
*/
|
|
119
|
+
export declare const SYSTEM_TRUST_BUNDLE_PATHS: readonly ["/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt"];
|
|
94
120
|
/** Build the full `docker run` argv. Pure: no I/O, no module state. */
|
|
95
121
|
export declare function runContainerArgs(input: ContainerRunInput): string[];
|
|
@@ -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
|
|
@@ -57,6 +71,21 @@
|
|
|
57
71
|
* never `--network=host`, where writing `net.*` is denied.
|
|
58
72
|
*/
|
|
59
73
|
export const TCP_RETRIES2 = 6;
|
|
74
|
+
/**
|
|
75
|
+
* Where a distro keeps the bundle that clients reading the system store
|
|
76
|
+
* open. Debian, Ubuntu and Alpine resolve every default path to the first
|
|
77
|
+
* through symlinks (`/etc/ssl/cert.pem`, `/usr/lib/ssl/cert.pem`); the
|
|
78
|
+
* RHEL family resolves to the second (itself a symlink into
|
|
79
|
+
* `/etc/pki/ca-trust/extracted/pem/`, which the runtime follows before it
|
|
80
|
+
* mounts). Go checks both names itself. An image with neither path gets
|
|
81
|
+
* the file created by the mount, which is what a scratch image with a
|
|
82
|
+
* Go binary wants. Read-only, so a container cannot change what its
|
|
83
|
+
* siblings trust.
|
|
84
|
+
*/
|
|
85
|
+
export const SYSTEM_TRUST_BUNDLE_PATHS = [
|
|
86
|
+
"/etc/ssl/certs/ca-certificates.crt",
|
|
87
|
+
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
88
|
+
];
|
|
60
89
|
/** Build the full `docker run` argv. Pure: no I/O, no module state. */
|
|
61
90
|
export function runContainerArgs(input) {
|
|
62
91
|
const { svc, tag, network } = input;
|
|
@@ -107,6 +136,14 @@ export function runContainerArgs(input) {
|
|
|
107
136
|
args.push("-e", `SSL_CERT_FILE=${bundle}`);
|
|
108
137
|
args.push("-e", `REQUESTS_CA_BUNDLE=${bundle}`);
|
|
109
138
|
args.push("-e", `AWS_CA_BUNDLE=${bundle}`);
|
|
139
|
+
// The system store itself, for clients that read no variable at all.
|
|
140
|
+
// Only a real bundle: the caller falls back to the bare CA when the
|
|
141
|
+
// guest's roots are unreadable, and the bare CA over the system store
|
|
142
|
+
// would leave Go and curl trusting spectest and nothing else.
|
|
143
|
+
if (bundle !== input.caPath) {
|
|
144
|
+
for (const p of SYSTEM_TRUST_BUNDLE_PATHS)
|
|
145
|
+
args.push(`--volume=${bundle}:${p}:ro`);
|
|
146
|
+
}
|
|
110
147
|
}
|
|
111
148
|
// The service's own env comes after ours, so a project can override the
|
|
112
149
|
// defaults we set rather than being silently overridden by them.
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
/** Written by `env.rs` before the harness starts. */
|
|
17
|
+
export declare const IMAGE_CACHE_MANIFEST = "/run/spectest-image-cache.json";
|
|
18
|
+
/** Where the cache's paths are, when this VM carries one. */
|
|
19
|
+
export interface ImageCachePaths {
|
|
20
|
+
/** containerd's root: read-write, this VM's own clone. */
|
|
21
|
+
root: string;
|
|
22
|
+
/** The layers disk: read-only, shared by every VM of a generation. */
|
|
23
|
+
layers: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Directory under the root disk holding a nested runtime's containerd
|
|
27
|
+
* root, one per service: `<root>/spectest-nested/<service>`. The merge
|
|
28
|
+
* (`image_cache/merge.rs::NESTED_DIR`) reads every store it finds there
|
|
29
|
+
* exactly as it reads the disk's own.
|
|
30
|
+
*/
|
|
31
|
+
export declare const NESTED_STORES_DIR = "spectest-nested";
|
|
32
|
+
/** The guest's static `mkfs.erofs`, which a nested runtime's EROFS
|
|
33
|
+
* differ needs and no runtime image ships. */
|
|
34
|
+
export declare const MKFS_EROFS_PATH = "/usr/local/bin/mkfs.erofs";
|
|
35
|
+
/** The guest's adopt helper (`base.rs::STORE_ADOPT_SH`), POSIX sh so a
|
|
36
|
+
* nested runtime's busybox can run the same file. */
|
|
37
|
+
export declare const STORE_ADOPT_PATH = "/usr/local/bin/spectest-store-adopt";
|
|
38
|
+
/** The cache's paths, or `null` when this VM carries none.
|
|
39
|
+
* `SPECTEST_IMAGE_CACHE_MANIFEST` points a test at another file. */
|
|
40
|
+
export declare function imageCachePathsSync(manifest?: string): ImageCachePaths | null;
|
|
41
|
+
/** The host directory a nested runtime keeps its containerd root in. */
|
|
42
|
+
export declare function nestedStoreDir(paths: ImageCachePaths, service: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Is a volume's host path on a cache disk? Such a directory is a cache
|
|
45
|
+
* the lineage keeps — like the container store one level up — and the
|
|
46
|
+
* delta-restore teardown must not wipe it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isOnImageCache(hostPath: string, paths: ImageCachePaths | null): boolean;
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
/** Written by `env.rs` before the harness starts. */
|
|
18
|
+
export const IMAGE_CACHE_MANIFEST = "/run/spectest-image-cache.json";
|
|
19
|
+
/**
|
|
20
|
+
* Directory under the root disk holding a nested runtime's containerd
|
|
21
|
+
* root, one per service: `<root>/spectest-nested/<service>`. The merge
|
|
22
|
+
* (`image_cache/merge.rs::NESTED_DIR`) reads every store it finds there
|
|
23
|
+
* exactly as it reads the disk's own.
|
|
24
|
+
*/
|
|
25
|
+
export const NESTED_STORES_DIR = "spectest-nested";
|
|
26
|
+
/** The guest's static `mkfs.erofs`, which a nested runtime's EROFS
|
|
27
|
+
* differ needs and no runtime image ships. */
|
|
28
|
+
export const MKFS_EROFS_PATH = "/usr/local/bin/mkfs.erofs";
|
|
29
|
+
/** The guest's adopt helper (`base.rs::STORE_ADOPT_SH`), POSIX sh so a
|
|
30
|
+
* nested runtime's busybox can run the same file. */
|
|
31
|
+
export const STORE_ADOPT_PATH = "/usr/local/bin/spectest-store-adopt";
|
|
32
|
+
function parse(raw) {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
const root = (parsed.disks ?? []).find((d) => d.role === "root" && d.path)?.path;
|
|
35
|
+
const layers = (parsed.disks ?? []).find((d) => d.role === "layers" && d.path)?.path;
|
|
36
|
+
return root && layers ? { root, layers } : null;
|
|
37
|
+
}
|
|
38
|
+
/** The cache's paths, or `null` when this VM carries none.
|
|
39
|
+
* `SPECTEST_IMAGE_CACHE_MANIFEST` points a test at another file. */
|
|
40
|
+
export function imageCachePathsSync(manifest = process.env.SPECTEST_IMAGE_CACHE_MANIFEST || IMAGE_CACHE_MANIFEST) {
|
|
41
|
+
try {
|
|
42
|
+
if (!existsSync(manifest))
|
|
43
|
+
return null;
|
|
44
|
+
return parse(readFileSync(manifest, "utf8"));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** The host directory a nested runtime keeps its containerd root in. */
|
|
51
|
+
export function nestedStoreDir(paths, service) {
|
|
52
|
+
return `${paths.root}/${NESTED_STORES_DIR}/${service}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Is a volume's host path on a cache disk? Such a directory is a cache
|
|
56
|
+
* the lineage keeps — like the container store one level up — and the
|
|
57
|
+
* delta-restore teardown must not wipe it.
|
|
58
|
+
*/
|
|
59
|
+
export function isOnImageCache(hostPath, paths) {
|
|
60
|
+
if (!paths)
|
|
61
|
+
return false;
|
|
62
|
+
return hostPath === paths.root || hostPath.startsWith(`${paths.root}/`) || hostPath === paths.layers || hostPath.startsWith(`${paths.layers}/`);
|
|
63
|
+
}
|
|
@@ -49,8 +49,9 @@ export declare function sanitizeSegment(p: string): string;
|
|
|
49
49
|
* `files` does: a component cannot know the map key the user will give it,
|
|
50
50
|
* and an **absolute** source gets no automatic per-service directory. A
|
|
51
51
|
* component that needs one — a nested runtime keeping its store under
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* the image cache's `spectest-nested/<service>` (`image-cache.ts::nestedStoreDir`),
|
|
53
|
+
* where two of them sharing one directory would be two daemons on one
|
|
54
|
+
* metadata store — writes the token into the path.
|
|
54
55
|
*
|
|
55
56
|
* `workspace` is a parameter rather than a module constant so the rule is
|
|
56
57
|
* testable without touching the filesystem.
|
|
@@ -45,8 +45,9 @@ export function sanitizeSegment(p) {
|
|
|
45
45
|
* `files` does: a component cannot know the map key the user will give it,
|
|
46
46
|
* and an **absolute** source gets no automatic per-service directory. A
|
|
47
47
|
* component that needs one — a nested runtime keeping its store under
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* the image cache's `spectest-nested/<service>` (`image-cache.ts::nestedStoreDir`),
|
|
49
|
+
* where two of them sharing one directory would be two daemons on one
|
|
50
|
+
* metadata store — writes the token into the path.
|
|
50
51
|
*
|
|
51
52
|
* `workspace` is a parameter rather than a module constant so the rule is
|
|
52
53
|
* testable without touching the filesystem.
|