@specific.dev/spectest 0.119.6 → 0.119.7
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 +5 -8
- package/dist/components/k3s.js +12 -165
- package/dist/daemon.js +8 -148
- package/dist/harness/buildkit-progress.d.ts +1 -1
- package/dist/harness/buildkit-progress.js +1 -1
- package/dist/harness/container-run.d.ts +1 -1
- package/dist/harness/volume-paths.d.ts +1 -1
- package/dist/harness/volume-paths.js +1 -1
- package/package.json +1 -1
- package/src/components/k3s.ts +12 -176
- package/src/daemon.ts +7 -168
- package/src/harness/buildkit-progress.ts +1 -1
- package/src/harness/container-run.ts +1 -1
- package/src/harness/volume-paths.ts +1 -1
- package/dist/harness/image-cache.d.ts +0 -117
- package/dist/harness/image-cache.js +0 -150
- package/src/harness/image-cache.test.ts +0 -81
- package/src/harness/image-cache.ts +0 -175
|
@@ -1,150 +0,0 @@
|
|
|
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
|
-
/**
|
|
33
|
-
* The BuildKit cache directory under each cache disk (`merge.rs::CACHE_DIR`):
|
|
34
|
-
* on the layers disk the merged export of every earlier build, read-only;
|
|
35
|
-
* on the root disk this lineage's own exports, which the merge harvests.
|
|
36
|
-
*/
|
|
37
|
-
export const BUILDKIT_CACHE_DIR = "spectest-buildkit-cache";
|
|
38
|
-
/**
|
|
39
|
-
* One directory that is both: an overlayfs with the layers disk's cache
|
|
40
|
-
* directory as the read-only lower and the root disk's as the upper,
|
|
41
|
-
* mounted here by the harness when the in-VM builder comes up
|
|
42
|
-
* (`daemon.ts::ensureBuildkitCacheUnion`). It is for the PROJECT'S OWN
|
|
43
|
-
* BuildKit — the `/services` docs page tells it to mount this one path
|
|
44
|
-
* for both of its cache flags. BuildKit's local exporter skips a blob its
|
|
45
|
-
* destination already holds, and through the union every merged blob is
|
|
46
|
-
* already there, so a fully cached build exports its index and manifest
|
|
47
|
-
* and nothing else (a fresh directory on the root cost every
|
|
48
|
-
* uncompressed layer again, 1.1 GB and 20 s for one service, measured
|
|
49
|
-
* 2026-09-06). New layers land in the upper, which the merge reads.
|
|
50
|
-
*
|
|
51
|
-
* The harness's own service builds used it too until SDK 0.76.0; since
|
|
52
|
-
* then their cache is the in-VM daemon's own state, which lives on the
|
|
53
|
-
* root disk and rides the base root into the next cold build
|
|
54
|
-
* (CONTAINER_STORE.md §20).
|
|
55
|
-
*/
|
|
56
|
-
export const BUILDKIT_CACHE_UNION = "/var/lib/spectest-buildkit-cache";
|
|
57
|
-
/**
|
|
58
|
-
* The prefix the docs reserve for spectest in that directory's one flat
|
|
59
|
-
* tag namespace. The harness tagged its service builds under it from
|
|
60
|
-
* SDK 0.75.0 to 0.76.0, after Specific's `nginx-mod` service tagged its
|
|
61
|
-
* own `buildctl` cache `nginx-mod` and the two overwrote each other on
|
|
62
|
-
* alternate boots (2026-09-06); the harness writes no tags now, and the
|
|
63
|
-
* prefix stays reserved so nothing a project names can ever collide
|
|
64
|
-
* with something spectest writes there later.
|
|
65
|
-
*/
|
|
66
|
-
export const BUILDKIT_CACHE_TAG_PREFIX = "spectest/service/";
|
|
67
|
-
/** The tag a service's Dockerfile build would carry under the reserved
|
|
68
|
-
* prefix: the service name, lowercased with anything outside `[a-z0-9-]`
|
|
69
|
-
* folded to `-`. Unused by the harness since SDK 0.76.0. */
|
|
70
|
-
export function buildkitCacheTag(service) {
|
|
71
|
-
return `${BUILDKIT_CACHE_TAG_PREFIX}${service.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}`;
|
|
72
|
-
}
|
|
73
|
-
function parse(raw) {
|
|
74
|
-
const parsed = JSON.parse(raw);
|
|
75
|
-
const root = (parsed.disks ?? []).find((d) => d.role === "root" && d.path)?.path;
|
|
76
|
-
const layers = (parsed.disks ?? []).find((d) => d.role === "layers" && d.path)?.path;
|
|
77
|
-
return root && layers ? { root, layers } : null;
|
|
78
|
-
}
|
|
79
|
-
/** The cache's paths, or `null` when this VM carries none.
|
|
80
|
-
* `SPECTEST_IMAGE_CACHE_MANIFEST` points a test at another file. */
|
|
81
|
-
export function imageCachePathsSync(manifest = process.env.SPECTEST_IMAGE_CACHE_MANIFEST || IMAGE_CACHE_MANIFEST) {
|
|
82
|
-
try {
|
|
83
|
-
if (!existsSync(manifest))
|
|
84
|
-
return null;
|
|
85
|
-
return parse(readFileSync(manifest, "utf8"));
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/** The host directory a nested runtime keeps its containerd root in. */
|
|
92
|
-
export function nestedStoreDir(paths, service) {
|
|
93
|
-
return `${paths.root}/${NESTED_STORES_DIR}/${service}`;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Is a volume's host path on a cache disk? Such a directory is a cache
|
|
97
|
-
* the lineage keeps — like the container store one level up — and the
|
|
98
|
-
* delta-restore teardown must not wipe it.
|
|
99
|
-
*/
|
|
100
|
-
export function isOnImageCache(hostPath, paths) {
|
|
101
|
-
if (!paths)
|
|
102
|
-
return false;
|
|
103
|
-
// The union is the root's cache directory seen through the layers
|
|
104
|
-
// disk: wiping it would wipe the upper, i.e. this lineage's exports.
|
|
105
|
-
if (hostPath === BUILDKIT_CACHE_UNION || hostPath.startsWith(`${BUILDKIT_CACHE_UNION}/`))
|
|
106
|
-
return true;
|
|
107
|
-
return hostPath === paths.root || hostPath.startsWith(`${paths.root}/`) || hostPath === paths.layers || hostPath.startsWith(`${paths.layers}/`);
|
|
108
|
-
}
|
|
109
|
-
const REF_NAME = "org.opencontainers.image.ref.name";
|
|
110
|
-
/**
|
|
111
|
-
* The index the BuildKit cache union must show: every tag of the merged
|
|
112
|
-
* cache (lower), overridden by this lineage's own exports (upper) where
|
|
113
|
-
* the upper still holds the manifest.
|
|
114
|
-
*
|
|
115
|
-
* overlayfs shadows whole files, so an `index.json` in the upper hides
|
|
116
|
-
* the lower's completely — and a root disk can carry one: the exports
|
|
117
|
-
* of a lineage that built before the union existed, or an index left
|
|
118
|
-
* behind with its blobs gone. Either way BuildKit then imports from an
|
|
119
|
-
* index naming a few tags whose manifests may not exist, skips the
|
|
120
|
-
* import, and every step runs (seen 2026-09-06: a cold build with
|
|
121
|
-
* every service's cache on the layers disk and 4 stale tags in the
|
|
122
|
-
* upper built everything from scratch). `upperHasBlob` says whether the
|
|
123
|
-
* upper holds a manifest's blob; an upper entry without one is dropped,
|
|
124
|
-
* since it would fail the whole import.
|
|
125
|
-
*/
|
|
126
|
-
export function mergeCacheIndex(lower, upper, upperHasBlob) {
|
|
127
|
-
const byTag = new Map();
|
|
128
|
-
const untagged = [];
|
|
129
|
-
for (const m of lower?.manifests ?? []) {
|
|
130
|
-
const tag = m.annotations?.[REF_NAME];
|
|
131
|
-
if (tag)
|
|
132
|
-
byTag.set(tag, m);
|
|
133
|
-
else
|
|
134
|
-
untagged.push(m);
|
|
135
|
-
}
|
|
136
|
-
for (const m of upper?.manifests ?? []) {
|
|
137
|
-
if (!upperHasBlob(m.digest.replace(/^sha256:/, "")))
|
|
138
|
-
continue;
|
|
139
|
-
const tag = m.annotations?.[REF_NAME];
|
|
140
|
-
if (tag)
|
|
141
|
-
byTag.set(tag, m);
|
|
142
|
-
else
|
|
143
|
-
untagged.push(m);
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
schemaVersion: 2,
|
|
147
|
-
mediaType: lower?.mediaType ?? upper?.mediaType ?? "application/vnd.oci.image.index.v1+json",
|
|
148
|
-
manifests: [...untagged, ...byTag.values()],
|
|
149
|
-
};
|
|
150
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { BUILDKIT_CACHE_UNION, isOnImageCache } from "./image-cache";
|
|
4
|
-
|
|
5
|
-
const paths = { root: "/var/lib/containerd", layers: "/var/lib/spectest-image-cache" };
|
|
6
|
-
|
|
7
|
-
describe("isOnImageCache", () => {
|
|
8
|
-
test("the two cache disks and anything under them", () => {
|
|
9
|
-
expect(isOnImageCache("/var/lib/containerd", paths)).toBe(true);
|
|
10
|
-
expect(isOnImageCache("/var/lib/containerd/spectest-nested/k8s", paths)).toBe(true);
|
|
11
|
-
expect(isOnImageCache("/var/lib/spectest-image-cache/spectest-buildkit-cache", paths)).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
/** The union is the root's export directory seen through the layers
|
|
15
|
-
* disk; wiping it at a delta restore would wipe this lineage's exports. */
|
|
16
|
-
test("the BuildKit cache union counts as cache", () => {
|
|
17
|
-
expect(isOnImageCache(BUILDKIT_CACHE_UNION, paths)).toBe(true);
|
|
18
|
-
expect(isOnImageCache(`${BUILDKIT_CACHE_UNION}/blobs`, paths)).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("an ordinary project volume does not", () => {
|
|
22
|
-
expect(isOnImageCache("/var/cache/spectest/volumes/db", paths)).toBe(false);
|
|
23
|
-
expect(isOnImageCache("/var/lib/containerd-not", paths)).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("no cache, nothing counts", () => {
|
|
27
|
-
expect(isOnImageCache(BUILDKIT_CACHE_UNION, null)).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
import { BUILDKIT_CACHE_TAG_PREFIX, buildkitCacheTag, mergeCacheIndex, type OciIndex } from "./image-cache";
|
|
32
|
-
|
|
33
|
-
const entry = (tag: string, hex: string) => ({ mediaType: "application/vnd.oci.image.manifest.v1+json", digest: `sha256:${hex}`, size: 1, annotations: { "org.opencontainers.image.ref.name": tag } });
|
|
34
|
-
|
|
35
|
-
describe("mergeCacheIndex", () => {
|
|
36
|
-
const lower: OciIndex = { schemaVersion: 2, manifests: [entry("api", "a1"), entry("cli", "c1")] };
|
|
37
|
-
|
|
38
|
-
test("no upper index: the merged cache's tags, unchanged", () => {
|
|
39
|
-
const out = mergeCacheIndex(lower, null, () => false);
|
|
40
|
-
expect(out.manifests.map((m) => m.annotations?.["org.opencontainers.image.ref.name"])).toEqual(["api", "cli"]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
/** The lineage's own export of a service wins over the merged one. */
|
|
44
|
-
test("an upper entry whose manifest exists overrides the lower's tag", () => {
|
|
45
|
-
const upper: OciIndex = { schemaVersion: 2, manifests: [entry("api", "a2")] };
|
|
46
|
-
const out = mergeCacheIndex(lower, upper, (hex) => hex === "a2");
|
|
47
|
-
const api = out.manifests.find((m) => m.annotations?.["org.opencontainers.image.ref.name"] === "api");
|
|
48
|
-
expect(api?.digest).toBe("sha256:a2");
|
|
49
|
-
expect(out.manifests).toHaveLength(2);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/** A stale upper index — blobs emptied, index left — would hide every
|
|
53
|
-
* lower tag and fail the import; its entries are dropped instead. */
|
|
54
|
-
test("an upper entry without its manifest blob is dropped", () => {
|
|
55
|
-
const upper: OciIndex = { schemaVersion: 2, manifests: [entry("api", "a2"), entry("worker", "w1")] };
|
|
56
|
-
const out = mergeCacheIndex(lower, upper, () => false);
|
|
57
|
-
expect(out.manifests.map((m) => m.digest)).toEqual(["sha256:a1", "sha256:c1"]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("a valid upper tag the lower lacks is added", () => {
|
|
61
|
-
const upper: OciIndex = { schemaVersion: 2, manifests: [entry("worker", "w1")] };
|
|
62
|
-
const out = mergeCacheIndex(lower, upper, () => true);
|
|
63
|
-
expect(out.manifests.map((m) => m.annotations?.["org.opencontainers.image.ref.name"])).toEqual(["api", "cli", "worker"]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
/** The directory is one tag namespace shared with the project's own
|
|
67
|
-
* BuildKit. The harness's tags are prefixed so a project tag named after
|
|
68
|
-
* the same service sits beside it instead of replacing it. */
|
|
69
|
-
test("a project's tag and the harness's tag for the same service both survive", () => {
|
|
70
|
-
const both: OciIndex = { schemaVersion: 2, manifests: [entry("nginx-mod", "p1"), entry(buildkitCacheTag("nginx-mod"), "h1")] };
|
|
71
|
-
const out = mergeCacheIndex(both, null, () => false);
|
|
72
|
-
expect(out.manifests.map((m) => m.digest)).toEqual(["sha256:p1", "sha256:h1"]);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("buildkitCacheTag", () => {
|
|
77
|
-
test("the reserved prefix plus the normalised service name", () => {
|
|
78
|
-
expect(buildkitCacheTag("nginx-mod")).toBe(`${BUILDKIT_CACHE_TAG_PREFIX}nginx-mod`);
|
|
79
|
-
expect(buildkitCacheTag("Api_v2")).toBe("spectest/service/api-v2");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,175 +0,0 @@
|
|
|
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
|
-
/**
|
|
47
|
-
* The BuildKit cache directory under each cache disk (`merge.rs::CACHE_DIR`):
|
|
48
|
-
* on the layers disk the merged export of every earlier build, read-only;
|
|
49
|
-
* on the root disk this lineage's own exports, which the merge harvests.
|
|
50
|
-
*/
|
|
51
|
-
export const BUILDKIT_CACHE_DIR = "spectest-buildkit-cache";
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* One directory that is both: an overlayfs with the layers disk's cache
|
|
55
|
-
* directory as the read-only lower and the root disk's as the upper,
|
|
56
|
-
* mounted here by the harness when the in-VM builder comes up
|
|
57
|
-
* (`daemon.ts::ensureBuildkitCacheUnion`). It is for the PROJECT'S OWN
|
|
58
|
-
* BuildKit — the `/services` docs page tells it to mount this one path
|
|
59
|
-
* for both of its cache flags. BuildKit's local exporter skips a blob its
|
|
60
|
-
* destination already holds, and through the union every merged blob is
|
|
61
|
-
* already there, so a fully cached build exports its index and manifest
|
|
62
|
-
* and nothing else (a fresh directory on the root cost every
|
|
63
|
-
* uncompressed layer again, 1.1 GB and 20 s for one service, measured
|
|
64
|
-
* 2026-09-06). New layers land in the upper, which the merge reads.
|
|
65
|
-
*
|
|
66
|
-
* The harness's own service builds used it too until SDK 0.76.0; since
|
|
67
|
-
* then their cache is the in-VM daemon's own state, which lives on the
|
|
68
|
-
* root disk and rides the base root into the next cold build
|
|
69
|
-
* (CONTAINER_STORE.md §20).
|
|
70
|
-
*/
|
|
71
|
-
export const BUILDKIT_CACHE_UNION = "/var/lib/spectest-buildkit-cache";
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The prefix the docs reserve for spectest in that directory's one flat
|
|
75
|
-
* tag namespace. The harness tagged its service builds under it from
|
|
76
|
-
* SDK 0.75.0 to 0.76.0, after Specific's `nginx-mod` service tagged its
|
|
77
|
-
* own `buildctl` cache `nginx-mod` and the two overwrote each other on
|
|
78
|
-
* alternate boots (2026-09-06); the harness writes no tags now, and the
|
|
79
|
-
* prefix stays reserved so nothing a project names can ever collide
|
|
80
|
-
* with something spectest writes there later.
|
|
81
|
-
*/
|
|
82
|
-
export const BUILDKIT_CACHE_TAG_PREFIX = "spectest/service/";
|
|
83
|
-
|
|
84
|
-
/** The tag a service's Dockerfile build would carry under the reserved
|
|
85
|
-
* prefix: the service name, lowercased with anything outside `[a-z0-9-]`
|
|
86
|
-
* folded to `-`. Unused by the harness since SDK 0.76.0. */
|
|
87
|
-
export function buildkitCacheTag(service: string): string {
|
|
88
|
-
return `${BUILDKIT_CACHE_TAG_PREFIX}${service.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function parse(raw: string): ImageCachePaths | null {
|
|
92
|
-
const parsed = JSON.parse(raw) as { disks?: { role?: string; path?: string }[] };
|
|
93
|
-
const root = (parsed.disks ?? []).find((d) => d.role === "root" && d.path)?.path;
|
|
94
|
-
const layers = (parsed.disks ?? []).find((d) => d.role === "layers" && d.path)?.path;
|
|
95
|
-
return root && layers ? { root, layers } : null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** The cache's paths, or `null` when this VM carries none.
|
|
99
|
-
* `SPECTEST_IMAGE_CACHE_MANIFEST` points a test at another file. */
|
|
100
|
-
export function imageCachePathsSync(
|
|
101
|
-
manifest: string = process.env.SPECTEST_IMAGE_CACHE_MANIFEST || IMAGE_CACHE_MANIFEST,
|
|
102
|
-
): ImageCachePaths | null {
|
|
103
|
-
try {
|
|
104
|
-
if (!existsSync(manifest)) return null;
|
|
105
|
-
return parse(readFileSync(manifest, "utf8"));
|
|
106
|
-
} catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** The host directory a nested runtime keeps its containerd root in. */
|
|
112
|
-
export function nestedStoreDir(paths: ImageCachePaths, service: string): string {
|
|
113
|
-
return `${paths.root}/${NESTED_STORES_DIR}/${service}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Is a volume's host path on a cache disk? Such a directory is a cache
|
|
118
|
-
* the lineage keeps — like the container store one level up — and the
|
|
119
|
-
* delta-restore teardown must not wipe it.
|
|
120
|
-
*/
|
|
121
|
-
export function isOnImageCache(hostPath: string, paths: ImageCachePaths | null): boolean {
|
|
122
|
-
if (!paths) return false;
|
|
123
|
-
// The union is the root's cache directory seen through the layers
|
|
124
|
-
// disk: wiping it would wipe the upper, i.e. this lineage's exports.
|
|
125
|
-
if (hostPath === BUILDKIT_CACHE_UNION || hostPath.startsWith(`${BUILDKIT_CACHE_UNION}/`)) return true;
|
|
126
|
-
return hostPath === paths.root || hostPath.startsWith(`${paths.root}/`) || hostPath === paths.layers || hostPath.startsWith(`${paths.layers}/`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The shape of an OCI layout's `index.json` the BuildKit local cache
|
|
130
|
-
* exporter writes: one manifest per tag, the tag in the ref-name
|
|
131
|
-
* annotation. */
|
|
132
|
-
export interface OciIndex {
|
|
133
|
-
schemaVersion?: number;
|
|
134
|
-
mediaType?: string;
|
|
135
|
-
manifests: Array<{ mediaType?: string; digest: string; size?: number; annotations?: Record<string, string> }>;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const REF_NAME = "org.opencontainers.image.ref.name";
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* The index the BuildKit cache union must show: every tag of the merged
|
|
142
|
-
* cache (lower), overridden by this lineage's own exports (upper) where
|
|
143
|
-
* the upper still holds the manifest.
|
|
144
|
-
*
|
|
145
|
-
* overlayfs shadows whole files, so an `index.json` in the upper hides
|
|
146
|
-
* the lower's completely — and a root disk can carry one: the exports
|
|
147
|
-
* of a lineage that built before the union existed, or an index left
|
|
148
|
-
* behind with its blobs gone. Either way BuildKit then imports from an
|
|
149
|
-
* index naming a few tags whose manifests may not exist, skips the
|
|
150
|
-
* import, and every step runs (seen 2026-09-06: a cold build with
|
|
151
|
-
* every service's cache on the layers disk and 4 stale tags in the
|
|
152
|
-
* upper built everything from scratch). `upperHasBlob` says whether the
|
|
153
|
-
* upper holds a manifest's blob; an upper entry without one is dropped,
|
|
154
|
-
* since it would fail the whole import.
|
|
155
|
-
*/
|
|
156
|
-
export function mergeCacheIndex(lower: OciIndex | null, upper: OciIndex | null, upperHasBlob: (hex: string) => boolean): OciIndex {
|
|
157
|
-
const byTag = new Map<string, OciIndex["manifests"][number]>();
|
|
158
|
-
const untagged: OciIndex["manifests"] = [];
|
|
159
|
-
for (const m of lower?.manifests ?? []) {
|
|
160
|
-
const tag = m.annotations?.[REF_NAME];
|
|
161
|
-
if (tag) byTag.set(tag, m);
|
|
162
|
-
else untagged.push(m);
|
|
163
|
-
}
|
|
164
|
-
for (const m of upper?.manifests ?? []) {
|
|
165
|
-
if (!upperHasBlob(m.digest.replace(/^sha256:/, ""))) continue;
|
|
166
|
-
const tag = m.annotations?.[REF_NAME];
|
|
167
|
-
if (tag) byTag.set(tag, m);
|
|
168
|
-
else untagged.push(m);
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
schemaVersion: 2,
|
|
172
|
-
mediaType: lower?.mediaType ?? upper?.mediaType ?? "application/vnd.oci.image.index.v1+json",
|
|
173
|
-
manifests: [...untagged, ...byTag.values()],
|
|
174
|
-
};
|
|
175
|
-
}
|