@specific.dev/spectest 0.131.13 → 0.131.14
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/daemon.js +46 -2
- package/dist/harness/built-image.d.ts +11 -2
- package/dist/harness/built-image.js +11 -3
- package/dist/harness/component-images.d.ts +7 -0
- package/dist/harness/component-images.js +12 -1
- package/dist/harness/sticky-consumer.js +6 -0
- package/package.json +1 -1
- package/src/daemon.ts +45 -2
- package/src/harness/built-image.ts +11 -3
- package/src/harness/component-images.test.ts +19 -1
- package/src/harness/component-images.ts +15 -1
- package/src/harness/sticky-consumer.ts +6 -0
package/dist/daemon.js
CHANGED
|
@@ -5,7 +5,7 @@ import { phoneScope, phoneRecordingChunk, releasePhoneRecording } from "./phone.
|
|
|
5
5
|
import { runWithBackgroundFailures } from "./harness/background-failures.js";
|
|
6
6
|
import { generateHostCert as mintHostCert } from "./harness/certificate.js";
|
|
7
7
|
import { containerdImageOutput } from "./harness/built-image.js";
|
|
8
|
-
import { componentBuildTasks, COMPONENT_RUNTIME } from "./harness/component-images.js";
|
|
8
|
+
import { componentBuildTasks, COMPONENT_RUNTIME, isComponentImageTask, isDigestReference } from "./harness/component-images.js";
|
|
9
9
|
import { waitForAll } from "./harness/concurrency.js";
|
|
10
10
|
import { updateServiceProgress, serviceProgressSnapshot } from "./harness/service-progress.js";
|
|
11
11
|
import { androidScope, androidRecordingChunk, releaseAndroidRecording, isAndroidApp } from "./android.js";
|
|
@@ -492,6 +492,18 @@ function ensureLocalBuildkitd() {
|
|
|
492
492
|
})();
|
|
493
493
|
return _localBuilder;
|
|
494
494
|
}
|
|
495
|
+
/** `true` when a digest-pinned reference is already complete in the store. */
|
|
496
|
+
async function imagePresentByDigest(ref) {
|
|
497
|
+
if (!isDigestReference(ref))
|
|
498
|
+
return false;
|
|
499
|
+
// `inspect` succeeds only when every layer of the image for this
|
|
500
|
+
// platform is in the store; a partial or missing image falls through to
|
|
501
|
+
// the ordinary pull, which fetches what is absent. dockerd's containerd
|
|
502
|
+
// store also unpacks on demand at container creation, so a record whose
|
|
503
|
+
// snapshots were reclaimed is repaired there rather than failing.
|
|
504
|
+
const present = await docker(["image", "inspect", "--format", "{{.Id}}", ref], 30_000);
|
|
505
|
+
return present.code === 0;
|
|
506
|
+
}
|
|
495
507
|
/** Host directory backing one volume mount. Rules (notably which volumes
|
|
496
508
|
* survive the delta-restore teardown) live in `harness/volume-paths.ts`. */
|
|
497
509
|
function resolveHostPath(service, vol) {
|
|
@@ -898,6 +910,19 @@ async function prepareServiceImageInner(svc, opts) {
|
|
|
898
910
|
// tags (`foo:latest`) at whatever the previous generation pulled, for
|
|
899
911
|
// as long as the delta chain lives — a silent semantic divergence
|
|
900
912
|
// from the cold build a delta restore must be equivalent to.
|
|
913
|
+
//
|
|
914
|
+
// The one reference that cannot float is a digest. `name@sha256:…`
|
|
915
|
+
// names exactly one image for as long as it exists, so a copy already
|
|
916
|
+
// in the store is the image the registry would return, and the
|
|
917
|
+
// round-trip buys nothing: skip it. Component images pinned by digest
|
|
918
|
+
// and user references pinned by digest take this path; a tag never does.
|
|
919
|
+
if (await imagePresentByDigest(ref)) {
|
|
920
|
+
progressService(svc.name, { status: "pulling", detail: `present by digest` });
|
|
921
|
+
const tagged = await docker(["tag", ref, tag], 30_000);
|
|
922
|
+
if (tagged.code !== 0)
|
|
923
|
+
throw new Error(`docker tag ${ref} ${tag} failed: ${tagged.stderr.trim()}`);
|
|
924
|
+
return { tag };
|
|
925
|
+
}
|
|
901
926
|
progressService(svc.name, { status: "pulling", detail: `pulling ${ref}` });
|
|
902
927
|
let layers = 0;
|
|
903
928
|
const pull = await shxStream("docker", ["pull", ref], 900_000, undefined, (line) => {
|
|
@@ -1042,11 +1067,30 @@ async function runServiceBuild(name, image, tag) {
|
|
|
1042
1067
|
// containerd exporter replaces the tag directly, bypassing Docker's
|
|
1043
1068
|
// dangling-image retention. Old versions must remain usable until our
|
|
1044
1069
|
// image GC decides they are no longer needed (including by nested stores).
|
|
1070
|
+
//
|
|
1071
|
+
// `compression=uncompressed` (`containerdImageOutput`): the image never leaves this store, so
|
|
1072
|
+
// gzip on export only to gunzip on unpack is pure cost — on Listen,
|
|
1073
|
+
// 64 s compressing a 2.7 GB layer and 56 s decompressing it again,
|
|
1074
|
+
// single-threaded, in the same guest that already held the layer as
|
|
1075
|
+
// a build snapshot (CONTAINER_STORE.md, "Image delivery inside the
|
|
1076
|
+
// guest"). With the default `force-compression=false` this applies
|
|
1077
|
+
// to layers this build creates; a pulled base image's gzip blobs are
|
|
1078
|
+
// reused as they are, so no existing layer is re-encoded or
|
|
1079
|
+
// re-unpacked. The uncompressed blob is a second on-disk copy of the
|
|
1080
|
+
// layer; the sticky headroom logic already budgets for blobs.
|
|
1081
|
+
//
|
|
1082
|
+
// `unpack`: dockerd needs the snapshot chain to start a container,
|
|
1083
|
+
// so a service image is unpacked here rather than at `docker run`.
|
|
1084
|
+
// A component image task (a `k3s()` build) is consumed only through
|
|
1085
|
+
// the content store — `k3s-image-loader.ts` serves its blobs and the
|
|
1086
|
+
// cluster's containerd unpacks them into its own store — so unpacking
|
|
1087
|
+
// it in dockerd's store would be a third copy nobody reads.
|
|
1088
|
+
const unpack = !isComponentImageTask(name);
|
|
1045
1089
|
buildArgs = [
|
|
1046
1090
|
"buildx", "build",
|
|
1047
1091
|
"--builder", LOCAL_BUILDER_NAME,
|
|
1048
1092
|
"--progress=plain",
|
|
1049
|
-
"--output", containerdImageOutput(tag),
|
|
1093
|
+
"--output", containerdImageOutput(tag, { unpack }),
|
|
1050
1094
|
...argFlags,
|
|
1051
1095
|
"-f", dfPath, contextDir,
|
|
1052
1096
|
];
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/** Containerd exports replace mutable tags without Docker's dangling-image
|
|
2
|
-
* retention. Keep each result addressable until the SDK's image GC evicts it.
|
|
3
|
-
|
|
2
|
+
* retention. Keep each result addressable until the SDK's image GC evicts it.
|
|
3
|
+
*
|
|
4
|
+
* Layers this build creates are stored uncompressed: the image never leaves
|
|
5
|
+
* the VM's store, so compressing on export only to decompress on unpack was
|
|
6
|
+
* pure cost (64 s + 56 s on Listen's 2.7 GB layer). `force-compression` stays
|
|
7
|
+
* off, so a pulled base image's blobs are reused as they are. `unpack` is
|
|
8
|
+
* false for images only a nested store reads (CONTAINER_STORE.md, "Image
|
|
9
|
+
* delivery inside the guest"). */
|
|
10
|
+
export declare function containerdImageOutput(reference: string, options?: {
|
|
11
|
+
unpack?: boolean;
|
|
12
|
+
}): string;
|
|
4
13
|
export interface Descriptor {
|
|
5
14
|
digest: string;
|
|
6
15
|
size: number;
|
|
@@ -4,9 +4,17 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { canonicalImageReference } from "./component-images.js";
|
|
6
6
|
/** Containerd exports replace mutable tags without Docker's dangling-image
|
|
7
|
-
* retention. Keep each result addressable until the SDK's image GC evicts it.
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
* retention. Keep each result addressable until the SDK's image GC evicts it.
|
|
8
|
+
*
|
|
9
|
+
* Layers this build creates are stored uncompressed: the image never leaves
|
|
10
|
+
* the VM's store, so compressing on export only to decompress on unpack was
|
|
11
|
+
* pure cost (64 s + 56 s on Listen's 2.7 GB layer). `force-compression` stays
|
|
12
|
+
* off, so a pulled base image's blobs are reused as they are. `unpack` is
|
|
13
|
+
* false for images only a nested store reads (CONTAINER_STORE.md, "Image
|
|
14
|
+
* delivery inside the guest"). */
|
|
15
|
+
export function containerdImageOutput(reference, options = {}) {
|
|
16
|
+
const unpack = options.unpack ?? true;
|
|
17
|
+
return `type=image,name=${canonicalImageReference(reference)},name-canonical=true,unpack=${unpack},compression=uncompressed`;
|
|
10
18
|
}
|
|
11
19
|
const digest = (bytes) => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
12
20
|
const descriptor = (bytes, mediaType) => ({ digest: digest(bytes), size: bytes.length, mediaType });
|
|
@@ -5,10 +5,17 @@ export type KubernetesImage = string | (BuildImage & {
|
|
|
5
5
|
});
|
|
6
6
|
/** containerd/CRI use fully qualified names, including the default tag. */
|
|
7
7
|
export declare function canonicalImageReference(reference: string): string;
|
|
8
|
+
/** A `name@sha256:…` reference names one immutable image; a tag can move. */
|
|
9
|
+
export declare function isDigestReference(reference: string): boolean;
|
|
8
10
|
export declare function kubernetesImages(images: readonly KubernetesImage[]): {
|
|
9
11
|
pulls: string[];
|
|
10
12
|
builds: ComponentImageBuild[];
|
|
11
13
|
};
|
|
14
|
+
/** Prefix of every component image task name; `isComponentImageTask` is the test. */
|
|
15
|
+
export declare const COMPONENT_IMAGE_TASK_PREFIX = "spectest-image-";
|
|
16
|
+
/** A component image task builds for a component's own runtime (today: a
|
|
17
|
+
* `k3s()` cluster's containerd), never for a container dockerd starts. */
|
|
18
|
+
export declare function isComponentImageTask(name: string): boolean;
|
|
12
19
|
/** Stable internal task names, separate from the service graph. */
|
|
13
20
|
export declare function componentBuildTasks<T extends ServiceConfig & {
|
|
14
21
|
name: string;
|
|
@@ -24,6 +24,10 @@ export function canonicalImageReference(reference) {
|
|
|
24
24
|
result += ":latest";
|
|
25
25
|
return result;
|
|
26
26
|
}
|
|
27
|
+
/** A `name@sha256:…` reference names one immutable image; a tag can move. */
|
|
28
|
+
export function isDigestReference(reference) {
|
|
29
|
+
return /@sha256:[a-f0-9]{64}$/.test(reference);
|
|
30
|
+
}
|
|
27
31
|
export function kubernetesImages(images) {
|
|
28
32
|
if (!Array.isArray(images))
|
|
29
33
|
throw new Error("k3s(): images must be an array of registry references or Dockerfile builds");
|
|
@@ -54,10 +58,17 @@ export function kubernetesImages(images) {
|
|
|
54
58
|
}
|
|
55
59
|
return { pulls, builds };
|
|
56
60
|
}
|
|
61
|
+
/** Prefix of every component image task name; `isComponentImageTask` is the test. */
|
|
62
|
+
export const COMPONENT_IMAGE_TASK_PREFIX = "spectest-image-";
|
|
63
|
+
/** A component image task builds for a component's own runtime (today: a
|
|
64
|
+
* `k3s()` cluster's containerd), never for a container dockerd starts. */
|
|
65
|
+
export function isComponentImageTask(name) {
|
|
66
|
+
return name.startsWith(COMPONENT_IMAGE_TASK_PREFIX);
|
|
67
|
+
}
|
|
57
68
|
/** Stable internal task names, separate from the service graph. */
|
|
58
69
|
export function componentBuildTasks(services) {
|
|
59
70
|
return services.flatMap(svc => (svc.imageBuilds ?? []).map(build => ({
|
|
60
|
-
name:
|
|
71
|
+
name: `${COMPONENT_IMAGE_TASK_PREFIX}${createHash("sha256").update(`${svc.name}\0${build.reference}`).digest("hex")}`,
|
|
61
72
|
owner: svc.name,
|
|
62
73
|
reference: build.reference,
|
|
63
74
|
image: build.image,
|
|
@@ -101,6 +101,12 @@ disabled_plugins = ["io.containerd.grpc.v1.cri"]
|
|
|
101
101
|
dockerConfig["data-root"] = `${config.path}/docker`;
|
|
102
102
|
dockerConfig.features = { "containerd-snapshotter": true };
|
|
103
103
|
dockerConfig["storage-driver"] = "overlayfs";
|
|
104
|
+
// Pull up to ten layers at once instead of dockerd's default three. The
|
|
105
|
+
// containerd image store honours this since Docker 29.7.0; extraction is
|
|
106
|
+
// still one layer at a time, so this only moves the download side off
|
|
107
|
+
// the critical path of many-layer images. Set here as well as in the
|
|
108
|
+
// golden's daemon.json so an SDK release carries it without a rebuild.
|
|
109
|
+
dockerConfig["max-concurrent-downloads"] = 10;
|
|
104
110
|
await writeFile(`${STICKY_RUNTIME}/docker.json`, JSON.stringify(dockerConfig));
|
|
105
111
|
await writeFile(`${STICKY_RUNTIME}/buildkit.toml`, stickyBuildkitConfig(config, (await diskUsage(config.path)).reservedBytes));
|
|
106
112
|
const start = async (unit, program, args) => {
|
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { phoneScope, phoneRecordingChunk, releasePhoneRecording, type PhoneRecor
|
|
|
5
5
|
import { runWithBackgroundFailures } from "./harness/background-failures.js";
|
|
6
6
|
import { generateHostCert as mintHostCert } from "./harness/certificate.js";
|
|
7
7
|
import { containerdImageOutput } from "./harness/built-image.js";
|
|
8
|
-
import { componentBuildTasks, COMPONENT_RUNTIME } from "./harness/component-images.js";
|
|
8
|
+
import { componentBuildTasks, COMPONENT_RUNTIME, isComponentImageTask, isDigestReference } from "./harness/component-images.js";
|
|
9
9
|
import { waitForAll } from "./harness/concurrency.js";
|
|
10
10
|
import { updateServiceProgress, serviceProgressSnapshot, type ServiceProgress } from "./harness/service-progress.js";
|
|
11
11
|
import { androidScope, androidRecordingChunk, releaseAndroidRecording, isAndroidApp, type AndroidRecording } from "./android.js";
|
|
@@ -749,6 +749,18 @@ function ensureLocalBuildkitd(): Promise<boolean> {
|
|
|
749
749
|
return _localBuilder;
|
|
750
750
|
}
|
|
751
751
|
|
|
752
|
+
/** `true` when a digest-pinned reference is already complete in the store. */
|
|
753
|
+
async function imagePresentByDigest(ref: string): Promise<boolean> {
|
|
754
|
+
if (!isDigestReference(ref)) return false;
|
|
755
|
+
// `inspect` succeeds only when every layer of the image for this
|
|
756
|
+
// platform is in the store; a partial or missing image falls through to
|
|
757
|
+
// the ordinary pull, which fetches what is absent. dockerd's containerd
|
|
758
|
+
// store also unpacks on demand at container creation, so a record whose
|
|
759
|
+
// snapshots were reclaimed is repaired there rather than failing.
|
|
760
|
+
const present = await docker(["image", "inspect", "--format", "{{.Id}}", ref], 30_000);
|
|
761
|
+
return present.code === 0;
|
|
762
|
+
}
|
|
763
|
+
|
|
752
764
|
/** Host directory backing one volume mount. Rules (notably which volumes
|
|
753
765
|
* survive the delta-restore teardown) live in `harness/volume-paths.ts`. */
|
|
754
766
|
function resolveHostPath(service: string, vol: VolumeMount): string {
|
|
@@ -1214,6 +1226,18 @@ async function prepareServiceImageInner(
|
|
|
1214
1226
|
// tags (`foo:latest`) at whatever the previous generation pulled, for
|
|
1215
1227
|
// as long as the delta chain lives — a silent semantic divergence
|
|
1216
1228
|
// from the cold build a delta restore must be equivalent to.
|
|
1229
|
+
//
|
|
1230
|
+
// The one reference that cannot float is a digest. `name@sha256:…`
|
|
1231
|
+
// names exactly one image for as long as it exists, so a copy already
|
|
1232
|
+
// in the store is the image the registry would return, and the
|
|
1233
|
+
// round-trip buys nothing: skip it. Component images pinned by digest
|
|
1234
|
+
// and user references pinned by digest take this path; a tag never does.
|
|
1235
|
+
if (await imagePresentByDigest(ref)) {
|
|
1236
|
+
progressService(svc.name, { status: "pulling", detail: `present by digest` });
|
|
1237
|
+
const tagged = await docker(["tag", ref, tag], 30_000);
|
|
1238
|
+
if (tagged.code !== 0) throw new Error(`docker tag ${ref} ${tag} failed: ${tagged.stderr.trim()}`);
|
|
1239
|
+
return { tag };
|
|
1240
|
+
}
|
|
1217
1241
|
progressService(svc.name, { status: "pulling", detail: `pulling ${ref}` });
|
|
1218
1242
|
let layers = 0;
|
|
1219
1243
|
const pull = await shxStream("docker", ["pull", ref], 900_000, undefined, (line) => {
|
|
@@ -1368,11 +1392,30 @@ async function runServiceBuild(
|
|
|
1368
1392
|
// containerd exporter replaces the tag directly, bypassing Docker's
|
|
1369
1393
|
// dangling-image retention. Old versions must remain usable until our
|
|
1370
1394
|
// image GC decides they are no longer needed (including by nested stores).
|
|
1395
|
+
//
|
|
1396
|
+
// `compression=uncompressed` (`containerdImageOutput`): the image never leaves this store, so
|
|
1397
|
+
// gzip on export only to gunzip on unpack is pure cost — on Listen,
|
|
1398
|
+
// 64 s compressing a 2.7 GB layer and 56 s decompressing it again,
|
|
1399
|
+
// single-threaded, in the same guest that already held the layer as
|
|
1400
|
+
// a build snapshot (CONTAINER_STORE.md, "Image delivery inside the
|
|
1401
|
+
// guest"). With the default `force-compression=false` this applies
|
|
1402
|
+
// to layers this build creates; a pulled base image's gzip blobs are
|
|
1403
|
+
// reused as they are, so no existing layer is re-encoded or
|
|
1404
|
+
// re-unpacked. The uncompressed blob is a second on-disk copy of the
|
|
1405
|
+
// layer; the sticky headroom logic already budgets for blobs.
|
|
1406
|
+
//
|
|
1407
|
+
// `unpack`: dockerd needs the snapshot chain to start a container,
|
|
1408
|
+
// so a service image is unpacked here rather than at `docker run`.
|
|
1409
|
+
// A component image task (a `k3s()` build) is consumed only through
|
|
1410
|
+
// the content store — `k3s-image-loader.ts` serves its blobs and the
|
|
1411
|
+
// cluster's containerd unpacks them into its own store — so unpacking
|
|
1412
|
+
// it in dockerd's store would be a third copy nobody reads.
|
|
1413
|
+
const unpack = !isComponentImageTask(name);
|
|
1371
1414
|
buildArgs = [
|
|
1372
1415
|
"buildx", "build",
|
|
1373
1416
|
"--builder", LOCAL_BUILDER_NAME,
|
|
1374
1417
|
"--progress=plain",
|
|
1375
|
-
"--output", containerdImageOutput(tag),
|
|
1418
|
+
"--output", containerdImageOutput(tag, { unpack }),
|
|
1376
1419
|
...argFlags,
|
|
1377
1420
|
"-f", dfPath, contextDir,
|
|
1378
1421
|
];
|
|
@@ -5,9 +5,17 @@ import { promisify } from "node:util";
|
|
|
5
5
|
import { canonicalImageReference } from "./component-images.js";
|
|
6
6
|
|
|
7
7
|
/** Containerd exports replace mutable tags without Docker's dangling-image
|
|
8
|
-
* retention. Keep each result addressable until the SDK's image GC evicts it.
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
* retention. Keep each result addressable until the SDK's image GC evicts it.
|
|
9
|
+
*
|
|
10
|
+
* Layers this build creates are stored uncompressed: the image never leaves
|
|
11
|
+
* the VM's store, so compressing on export only to decompress on unpack was
|
|
12
|
+
* pure cost (64 s + 56 s on Listen's 2.7 GB layer). `force-compression` stays
|
|
13
|
+
* off, so a pulled base image's blobs are reused as they are. `unpack` is
|
|
14
|
+
* false for images only a nested store reads (CONTAINER_STORE.md, "Image
|
|
15
|
+
* delivery inside the guest"). */
|
|
16
|
+
export function containerdImageOutput(reference: string, options: { unpack?: boolean } = {}): string {
|
|
17
|
+
const unpack = options.unpack ?? true;
|
|
18
|
+
return `type=image,name=${canonicalImageReference(reference)},name-canonical=true,unpack=${unpack},compression=uncompressed`;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
export interface Descriptor { digest: string; size: number; mediaType: string; platform?: { os: string; architecture: string } }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import { canonicalImageReference, componentBuildTasks, kubernetesImages } from "./component-images";
|
|
2
|
+
import { canonicalImageReference, componentBuildTasks, isComponentImageTask, isDigestReference, kubernetesImages } from "./component-images";
|
|
3
3
|
import { validateServiceImage, type BuildImage } from "../index";
|
|
4
4
|
|
|
5
5
|
test("services and Kubernetes share all Dockerfile build options without a type discriminator", () => {
|
|
@@ -42,3 +42,21 @@ test("build task identity survives list reordering but is scoped to the owning c
|
|
|
42
42
|
expect(() => validateServiceImage("app", { content: "FROM scratch", buildArgs: { "bad-key": "x" } })).toThrow("ARG");
|
|
43
43
|
expect(() => validateServiceImage("app", { content: "FROM scratch", exclude: [1] as never })).toThrow("exclude");
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
test("component image tasks are recognised by name and services are not", () => {
|
|
47
|
+
const [task] = componentBuildTasks([{ name: "cluster", image: { type: "registry", reference: "rancher/k3s:v1.33.1-k3s1" },
|
|
48
|
+
imageBuilds: [{ reference: "registry.internal/app:dev", image: { type: "dockerfile", content: "FROM scratch" } }] } as any]);
|
|
49
|
+
expect(isComponentImageTask(task!.name)).toBe(true);
|
|
50
|
+
expect(isComponentImageTask("cluster")).toBe(false);
|
|
51
|
+
expect(isComponentImageTask("spectest-image")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("only a full sha256 digest reference is immutable", () => {
|
|
55
|
+
const digest = "a".repeat(64);
|
|
56
|
+
expect(isDigestReference(`postgres@sha256:${digest}`)).toBe(true);
|
|
57
|
+
expect(isDigestReference(`ghcr.io/org/app:1.2@sha256:${digest}`)).toBe(true);
|
|
58
|
+
expect(isDigestReference("postgres:16.4")).toBe(false);
|
|
59
|
+
expect(isDigestReference("postgres:latest")).toBe(false);
|
|
60
|
+
expect(isDigestReference(`postgres@sha256:${digest.slice(1)}`)).toBe(false);
|
|
61
|
+
expect(isDigestReference(`postgres@sha512:${digest}`)).toBe(false);
|
|
62
|
+
});
|
|
@@ -26,6 +26,11 @@ export function canonicalImageReference(reference: string): string {
|
|
|
26
26
|
return result;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** A `name@sha256:…` reference names one immutable image; a tag can move. */
|
|
30
|
+
export function isDigestReference(reference: string): boolean {
|
|
31
|
+
return /@sha256:[a-f0-9]{64}$/.test(reference);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export function kubernetesImages(images: readonly KubernetesImage[]): { pulls: string[]; builds: ComponentImageBuild[] } {
|
|
30
35
|
if (!Array.isArray(images)) throw new Error("k3s(): images must be an array of registry references or Dockerfile builds");
|
|
31
36
|
const pulls: string[] = [];
|
|
@@ -53,10 +58,19 @@ export function kubernetesImages(images: readonly KubernetesImage[]): { pulls: s
|
|
|
53
58
|
return { pulls, builds };
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/** Prefix of every component image task name; `isComponentImageTask` is the test. */
|
|
62
|
+
export const COMPONENT_IMAGE_TASK_PREFIX = "spectest-image-";
|
|
63
|
+
|
|
64
|
+
/** A component image task builds for a component's own runtime (today: a
|
|
65
|
+
* `k3s()` cluster's containerd), never for a container dockerd starts. */
|
|
66
|
+
export function isComponentImageTask(name: string): boolean {
|
|
67
|
+
return name.startsWith(COMPONENT_IMAGE_TASK_PREFIX);
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
/** Stable internal task names, separate from the service graph. */
|
|
57
71
|
export function componentBuildTasks<T extends ServiceConfig & { name: string }>(services: readonly T[]) {
|
|
58
72
|
return services.flatMap(svc => (svc.imageBuilds ?? []).map(build => ({
|
|
59
|
-
name:
|
|
73
|
+
name: `${COMPONENT_IMAGE_TASK_PREFIX}${createHash("sha256").update(`${svc.name}\0${build.reference}`).digest("hex")}`,
|
|
60
74
|
owner: svc.name,
|
|
61
75
|
reference: build.reference,
|
|
62
76
|
image: build.image,
|
|
@@ -104,6 +104,12 @@ disabled_plugins = ["io.containerd.grpc.v1.cri"]
|
|
|
104
104
|
dockerConfig["data-root"] = `${config.path}/docker`;
|
|
105
105
|
dockerConfig.features = { "containerd-snapshotter": true };
|
|
106
106
|
dockerConfig["storage-driver"] = "overlayfs";
|
|
107
|
+
// Pull up to ten layers at once instead of dockerd's default three. The
|
|
108
|
+
// containerd image store honours this since Docker 29.7.0; extraction is
|
|
109
|
+
// still one layer at a time, so this only moves the download side off
|
|
110
|
+
// the critical path of many-layer images. Set here as well as in the
|
|
111
|
+
// golden's daemon.json so an SDK release carries it without a rebuild.
|
|
112
|
+
dockerConfig["max-concurrent-downloads"] = 10;
|
|
107
113
|
await writeFile(`${STICKY_RUNTIME}/docker.json`, JSON.stringify(dockerConfig));
|
|
108
114
|
await writeFile(`${STICKY_RUNTIME}/buildkit.toml`, stickyBuildkitConfig(config, (await diskUsage(config.path)).reservedBytes));
|
|
109
115
|
const start = async (unit: string, program: string, args: string[]) => {
|