@specific.dev/spectest 0.90.3 → 0.91.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/expo.d.ts +2 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/k3s-trust.d.ts +57 -0
- package/dist/components/k3s-trust.js +125 -0
- package/dist/components/k3s.d.ts +17 -5
- package/dist/components/k3s.js +77 -11
- package/dist/daemon.js +53 -14
- package/dist/harness/component-images.d.ts +31 -0
- package/dist/harness/component-images.js +69 -0
- package/dist/harness/k3s-image-loader.d.ts +5 -0
- package/dist/harness/k3s-image-loader.js +150 -0
- package/dist/harness/k3s-prepull.d.ts +7 -0
- package/dist/harness/k3s-prepull.js +39 -0
- package/dist/harness/sticky-consumer.d.ts +3 -3
- package/dist/harness/sticky-consumer.js +19 -4
- package/dist/harness/sticky-kubernetes.d.ts +15 -0
- package/dist/harness/sticky-kubernetes.js +163 -0
- package/dist/harness/sticky-paths.d.ts +5 -0
- package/dist/harness/sticky-paths.js +18 -0
- package/dist/index.d.ts +32 -2
- package/dist/index.js +25 -2
- package/package.json +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/k3s-trust.test.ts +81 -0
- package/src/components/k3s-trust.ts +135 -0
- package/src/components/k3s.ts +81 -10
- package/src/daemon.ts +56 -15
- package/src/harness/component-images.test.ts +44 -0
- package/src/harness/component-images.ts +73 -0
- package/src/harness/k3s-image-loader.ts +129 -0
- package/src/harness/k3s-prepull.test.ts +47 -0
- package/src/harness/k3s-prepull.ts +36 -0
- package/src/harness/sticky-consumer.test.ts +28 -0
- package/src/harness/sticky-consumer.ts +14 -3
- package/src/harness/sticky-kubernetes.test.ts +88 -0
- package/src/harness/sticky-kubernetes.ts +165 -0
- package/src/harness/sticky-paths.ts +18 -0
- package/src/index.ts +48 -3
|
@@ -103,6 +103,7 @@ export declare function nodeMajorFromRange(range: string): number | null;
|
|
|
103
103
|
export declare function expo(opts?: ExpoOptions): {
|
|
104
104
|
image: {
|
|
105
105
|
type: "dockerfile";
|
|
106
|
+
reference?: string;
|
|
106
107
|
path: string;
|
|
107
108
|
content?: never;
|
|
108
109
|
context?: string;
|
|
@@ -111,6 +112,7 @@ export declare function expo(opts?: ExpoOptions): {
|
|
|
111
112
|
buildArgs?: Readonly<Record<string, string>>;
|
|
112
113
|
} | {
|
|
113
114
|
type: "dockerfile";
|
|
115
|
+
reference?: string;
|
|
114
116
|
content: string;
|
|
115
117
|
path?: never;
|
|
116
118
|
context?: string;
|
|
@@ -2,7 +2,7 @@ export { temporal, temporalClient, type TemporalOptions, type TemporalClient, ty
|
|
|
2
2
|
export { postgres, type PostgresOptions, type PostgresHelpers, } from "./postgres.js";
|
|
3
3
|
export { s3, type S3Options, type S3Helpers, } from "./s3.js";
|
|
4
4
|
export { SQL, type SqlClient, type SqlOptions } from "../sql.js";
|
|
5
|
-
export { k3s, type K3sOptions, type K3sHelpers, type K3sClient, type TaggedObjectApi, type KubernetesObjectRef, type ApplyOptions, type RolloutTarget, type WaitOptions, type LogsOptions, type KubernetesObject, type KubernetesListObject, } from "./k3s.js";
|
|
5
|
+
export { k3s, type K3sOptions, type KubernetesImage, type K3sHelpers, type K3sClient, type TaggedObjectApi, type KubernetesObjectRef, type ApplyOptions, type RolloutTarget, type WaitOptions, type LogsOptions, type KubernetesObject, type KubernetesListObject, } from "./k3s.js";
|
|
6
6
|
export { expo, type ExpoOptions, type ExpoHelpers, type ExpoPackageManager, } from "./expo.js";
|
|
7
7
|
export { supabase, type SupabaseOptions, type SupabaseHelpers, type SupabaseStack, } from "./supabase.js";
|
|
8
8
|
export { email, type EmailOptions, type EmailHelpers, type EmailMessage, type EmailSummary, type EmailMatch, type EmailAttachment, } from "./email.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export declare const POD_CA_PATH = "/spectest/ca-bundle.crt";
|
|
2
|
+
interface Container {
|
|
3
|
+
env?: {
|
|
4
|
+
name: string;
|
|
5
|
+
value?: string;
|
|
6
|
+
valueFrom?: unknown;
|
|
7
|
+
}[];
|
|
8
|
+
volumeMounts?: {
|
|
9
|
+
name: string;
|
|
10
|
+
mountPath: string;
|
|
11
|
+
subPath?: string;
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
interface Pod {
|
|
16
|
+
metadata?: {
|
|
17
|
+
annotations?: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
spec?: {
|
|
20
|
+
os?: {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
volumes?: {
|
|
24
|
+
name: string;
|
|
25
|
+
configMap?: {
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}[];
|
|
30
|
+
containers?: Container[];
|
|
31
|
+
initContainers?: Container[];
|
|
32
|
+
ephemeralContainers?: Container[];
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
interface Patch {
|
|
36
|
+
op: "add";
|
|
37
|
+
path: string;
|
|
38
|
+
value: unknown;
|
|
39
|
+
}
|
|
40
|
+
export declare const TRUST_OPT_OUT = "spectest.dev/inject-ca";
|
|
41
|
+
export declare function podTrustPatch(pod: Pod, configMap: string, ephemeral?: boolean): Patch[];
|
|
42
|
+
export interface TrustDependencies {
|
|
43
|
+
/** Ensure this immutable ConfigMap exists in the requesting namespace. */
|
|
44
|
+
ensureBundle(namespace: string, name: string, bundle: string): Promise<void>;
|
|
45
|
+
apply(configuration: unknown): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export declare function installPodTrust(options: {
|
|
48
|
+
name: string;
|
|
49
|
+
gateway: string;
|
|
50
|
+
bundle: string;
|
|
51
|
+
cert: string;
|
|
52
|
+
key: string;
|
|
53
|
+
ca: string;
|
|
54
|
+
}, deps: TrustDependencies): Promise<void>;
|
|
55
|
+
/** Release a component listener on replacement or in standalone tests. */
|
|
56
|
+
export declare function stopPodTrust(name: string): void;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Pod trust belongs to the cluster. Admission adds read-only ConfigMap
|
|
2
|
+
* mounts and environment defaults, without changing any image bytes. */
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { createServer } from "node:https";
|
|
5
|
+
export const POD_CA_PATH = "/spectest/ca-bundle.crt";
|
|
6
|
+
const TRUST_ENV = {
|
|
7
|
+
NODE_EXTRA_CA_CERTS: POD_CA_PATH, SSL_CERT_FILE: POD_CA_PATH,
|
|
8
|
+
REQUESTS_CA_BUNDLE: POD_CA_PATH, AWS_CA_BUNDLE: POD_CA_PATH,
|
|
9
|
+
PGSSLROOTCERT: POD_CA_PATH, DENO_TLS_CA_STORE: "mozilla,system",
|
|
10
|
+
};
|
|
11
|
+
const MOUNT_PATHS = [POD_CA_PATH, "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt"];
|
|
12
|
+
export const TRUST_OPT_OUT = "spectest.dev/inject-ca";
|
|
13
|
+
export function podTrustPatch(pod, configMap, ephemeral = false) {
|
|
14
|
+
if (!pod.spec || pod.spec.os?.name === "windows" || pod.metadata?.annotations?.[TRUST_OPT_OUT] === "false")
|
|
15
|
+
return [];
|
|
16
|
+
const patches = [];
|
|
17
|
+
const volumes = pod.spec.volumes ?? [];
|
|
18
|
+
// Reuse our immutable ConfigMap, but never overwrite a user's volume.
|
|
19
|
+
let volume = volumes.find(v => v.configMap?.name === configMap)?.name;
|
|
20
|
+
if (!volume && ephemeral)
|
|
21
|
+
return []; // Pod volumes cannot change on this subresource.
|
|
22
|
+
if (!volume) {
|
|
23
|
+
const names = new Set(volumes.map(v => v.name));
|
|
24
|
+
volume = "spectest-ca";
|
|
25
|
+
for (let n = 1; names.has(volume); n++)
|
|
26
|
+
volume = `spectest-ca-${n}`;
|
|
27
|
+
const value = { name: volume, configMap: { name: configMap, defaultMode: 0o444, items: [{ key: "ca-bundle.crt", path: "ca-bundle.crt" }] } };
|
|
28
|
+
patches.push(pod.spec.volumes
|
|
29
|
+
? { op: "add", path: "/spec/volumes/-", value }
|
|
30
|
+
: { op: "add", path: "/spec/volumes", value: [value] });
|
|
31
|
+
}
|
|
32
|
+
const groups = ephemeral ? ["ephemeralContainers"] : ["containers", "initContainers"];
|
|
33
|
+
for (const group of groups) {
|
|
34
|
+
for (const [index, container] of (pod.spec[group] ?? []).entries()) {
|
|
35
|
+
const prefix = `/spec/${group}/${index}`;
|
|
36
|
+
const env = container.env ?? [];
|
|
37
|
+
const names = new Set(env.map(e => e.name));
|
|
38
|
+
const extraEnv = Object.entries(TRUST_ENV).filter(([name]) => !names.has(name)).map(([name, value]) => ({ name, value }));
|
|
39
|
+
const mounts = container.volumeMounts ?? [];
|
|
40
|
+
// A user mount at a path (or a parent directory) owns that path.
|
|
41
|
+
const extraMounts = MOUNT_PATHS.filter(path => !mounts.some(m => path === m.mountPath || path.startsWith(m.mountPath.replace(/\/$/, "") + "/")))
|
|
42
|
+
.map(mountPath => ({ name: volume, mountPath, subPath: "ca-bundle.crt", readOnly: true }));
|
|
43
|
+
if (extraEnv.length)
|
|
44
|
+
patches.push({ op: "add", path: `${prefix}/env`, value: [...env, ...extraEnv] });
|
|
45
|
+
if (extraMounts.length)
|
|
46
|
+
patches.push({ op: "add", path: `${prefix}/volumeMounts`, value: [...mounts, ...extraMounts] });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return patches;
|
|
50
|
+
}
|
|
51
|
+
const servers = new Map();
|
|
52
|
+
export async function installPodTrust(options, deps) {
|
|
53
|
+
const configMap = `spectest-ca-${createHash("sha256").update(options.bundle).digest("hex").slice(0, 20)}`;
|
|
54
|
+
const endpoint = `/admit/${randomUUID()}`;
|
|
55
|
+
const server = createServer({ cert: options.cert, key: options.key }, async (req, res) => {
|
|
56
|
+
if (req.method !== "POST" || new URL(req.url ?? "/", "https://local").pathname !== endpoint) {
|
|
57
|
+
res.writeHead(404).end();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
try {
|
|
63
|
+
for await (const chunk of req) {
|
|
64
|
+
size += chunk.length;
|
|
65
|
+
if (size > 2 * 1024 * 1024) {
|
|
66
|
+
res.writeHead(413).end();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
chunks.push(Buffer.from(chunk));
|
|
70
|
+
}
|
|
71
|
+
const review = JSON.parse(Buffer.concat(chunks).toString());
|
|
72
|
+
const request = review.request;
|
|
73
|
+
if (!request?.uid || request.resource?.resource !== "pods" || !request.object || !request.namespace) {
|
|
74
|
+
res.writeHead(400).end("invalid Pod admission request");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const patch = podTrustPatch(request.object, configMap, request.subResource === "ephemeralcontainers");
|
|
78
|
+
// Dry runs return the same patch but perform no API writes.
|
|
79
|
+
if (patch.length && !request.dryRun)
|
|
80
|
+
await deps.ensureBundle(request.namespace, configMap, options.bundle);
|
|
81
|
+
res.setHeader("Content-Type", "application/json");
|
|
82
|
+
res.end(JSON.stringify({ apiVersion: "admission.k8s.io/v1", kind: "AdmissionReview", response: {
|
|
83
|
+
uid: request.uid, allowed: true,
|
|
84
|
+
...(patch.length ? { patchType: "JSONPatch", patch: Buffer.from(JSON.stringify(patch)).toString("base64") } : {}),
|
|
85
|
+
} }));
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error(`[k3s-trust] ${options.name}: ${err instanceof Error ? err.message : err}`);
|
|
89
|
+
res.writeHead(500).end("could not mount the environment CA");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
server.requestTimeout = 15_000;
|
|
93
|
+
try {
|
|
94
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, options.gateway, resolve); });
|
|
95
|
+
const port = server.address().port;
|
|
96
|
+
await deps.apply({
|
|
97
|
+
apiVersion: "admissionregistration.k8s.io/v1", kind: "MutatingWebhookConfiguration",
|
|
98
|
+
metadata: { name: "spectest-pod-trust" },
|
|
99
|
+
webhooks: [{
|
|
100
|
+
name: "pod-trust.spectest.dev", admissionReviewVersions: ["v1"],
|
|
101
|
+
sideEffects: "NoneOnDryRun", failurePolicy: "Fail", timeoutSeconds: 10,
|
|
102
|
+
reinvocationPolicy: "IfNeeded", matchPolicy: "Equivalent",
|
|
103
|
+
clientConfig: { url: `https://${options.gateway}:${port}${endpoint}`, caBundle: Buffer.from(options.ca).toString("base64") },
|
|
104
|
+
rules: [{ operations: ["CREATE"], apiGroups: [""], apiVersions: ["v1"], resources: ["pods"], scope: "Namespaced" },
|
|
105
|
+
{ operations: ["UPDATE"], apiGroups: [""], apiVersions: ["v1"], resources: ["pods/ephemeralcontainers"], scope: "Namespaced" }],
|
|
106
|
+
}],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
server.closeAllConnections();
|
|
111
|
+
server.close();
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
const previous = servers.get(options.name);
|
|
115
|
+
servers.set(options.name, server);
|
|
116
|
+
previous?.closeAllConnections();
|
|
117
|
+
previous?.close();
|
|
118
|
+
}
|
|
119
|
+
/** Release a component listener on replacement or in standalone tests. */
|
|
120
|
+
export function stopPodTrust(name) {
|
|
121
|
+
const server = servers.get(name);
|
|
122
|
+
servers.delete(name);
|
|
123
|
+
server?.closeAllConnections();
|
|
124
|
+
server?.close();
|
|
125
|
+
}
|
package/dist/components/k3s.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { AppsV1Api, BatchV1Api, CoreV1Api, KubeConfig, PatchStrategy, type KubernetesListObject, type KubernetesObject, type V1DeleteOptions, type V1Job, type V1Status } from "@kubernetes/client-node";
|
|
2
|
-
import type { ServiceHelpersContext } from "../index.js";
|
|
2
|
+
import type { ServiceHelpersContext, ServiceSetupContext } from "../index.js";
|
|
3
|
+
import { type KubernetesImage } from "../harness/component-images.js";
|
|
4
|
+
export type { KubernetesImage } from "../harness/component-images.js";
|
|
3
5
|
import type { Wrapped } from "../inspect.js";
|
|
4
6
|
export type { KubernetesObject, KubernetesListObject, } from "@kubernetes/client-node";
|
|
7
|
+
/** Kubernetes pods receive read-only environment CA mounts during admission.
|
|
8
|
+
* Image bytes and digests stay unchanged; explicit pod trust settings win. */
|
|
5
9
|
export interface K3sOptions {
|
|
10
|
+
/** Images available to kubelet before project setup. Use registry strings,
|
|
11
|
+
* or Dockerfile builds with a full `reference` matching your manifests. Builds
|
|
12
|
+
* support path/content, context, exclude, target and buildArgs, and run in
|
|
13
|
+
* parallel with service builds through the standard BuildKit pipeline.
|
|
14
|
+
* Setup waits for every pull and fails if a requested image cannot be pulled.
|
|
15
|
+
* Sticky-enabled projects retain image content and unpacked layers between
|
|
16
|
+
* fresh builds automatically. Digest references can reuse the cache offline;
|
|
17
|
+
* tags are resolved again on each fresh setup. Pod imagePullPolicy still applies.
|
|
18
|
+
*/
|
|
19
|
+
images?: readonly KubernetesImage[];
|
|
6
20
|
/**
|
|
7
21
|
* Image tag for the official `rancher/k3s` image. Default
|
|
8
22
|
* `"v1.33.5-k3s1"`. Any tag works; a k3s older than 1.33 embeds a
|
|
@@ -319,6 +333,7 @@ export declare function k3s(opts?: K3sOptions): {
|
|
|
319
333
|
type: "registry";
|
|
320
334
|
reference: string;
|
|
321
335
|
};
|
|
336
|
+
imageBuilds: import("../index.js").ComponentImageBuild[];
|
|
322
337
|
command: string;
|
|
323
338
|
privileged: true;
|
|
324
339
|
tmpfs: string[];
|
|
@@ -344,9 +359,6 @@ export declare function k3s(opts?: K3sOptions): {
|
|
|
344
359
|
command: string;
|
|
345
360
|
timeoutSecs: number;
|
|
346
361
|
};
|
|
347
|
-
setup: (
|
|
348
|
-
name: string;
|
|
349
|
-
helpers: K3sHelpers;
|
|
350
|
-
}) => Promise<void>;
|
|
362
|
+
setup: (ctx: ServiceSetupContext<K3sHelpers>) => Promise<void>;
|
|
351
363
|
helpers: ({ name, exec, poll }: ServiceHelpersContext) => Promise<K3sHelpers>;
|
|
352
364
|
};
|
package/dist/components/k3s.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { installPodTrust } from "./k3s-trust.js";
|
|
3
|
+
import { loadK3sBuildImages } from "../harness/k3s-image-loader.js";
|
|
1
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
5
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
3
6
|
import { randomUUID } from "node:crypto";
|
|
@@ -7,6 +10,10 @@ import { AppsV1Api, BatchV1Api, CoreV1Api, KubeConfig, KubernetesObjectApi, Patc
|
|
|
7
10
|
import { Observable } from "@kubernetes/client-node/dist/gen/rxjsStub.js";
|
|
8
11
|
import { dnsName, provides, SELF_SERVICE_TOKEN } from "../index.js";
|
|
9
12
|
import { MKFS_EROFS_PATH, STORE_ADOPT_PATH, imageCachePathsSync, nestedStoreDir, } from "../harness/image-cache.js";
|
|
13
|
+
import { stickyDiskPathSync } from "../harness/sticky-paths.js";
|
|
14
|
+
import { registerStickyKubernetesStore, stickyKubernetesDirectory } from "../harness/sticky-kubernetes.js";
|
|
15
|
+
import { prePullK3sImages } from "../harness/k3s-prepull.js";
|
|
16
|
+
import { componentRuntime, kubernetesImages } from "../harness/component-images.js";
|
|
10
17
|
import { deepUnwrap, readTag, wrap } from "../inspect.js";
|
|
11
18
|
import { recorderAnnotate, recorderRemove } from "../recorder.js";
|
|
12
19
|
/** Port the in-cluster registry listens on (plain HTTP). */
|
|
@@ -32,8 +39,13 @@ const CRI_MANAGED_LABEL = "io.cri-containerd.image=managed";
|
|
|
32
39
|
*/
|
|
33
40
|
const BOOT_MARKER = ".spectest-boot-incomplete";
|
|
34
41
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
42
|
+
* Sticky-enabled projects bind a per-service/version directory on the
|
|
43
|
+
* sticky disk at CLUSTER_STORE_TARGET and use overlayfs. Publication's
|
|
44
|
+
* sticky-kubernetes adapter removes runtime state and retains images and
|
|
45
|
+
* unpacked layers under the shared filesystem budget. Test forks do not
|
|
46
|
+
* write back. The preparation wrapper below also handles delta rebuilds.
|
|
47
|
+
*
|
|
48
|
+
* Legacy image-cache integration (`CONTAINER_STORE.md` §12):
|
|
37
49
|
*
|
|
38
50
|
* The cluster's embedded containerd keeps its root on the **root disk**
|
|
39
51
|
* of the environment's image cache, in a directory of its own
|
|
@@ -195,8 +207,8 @@ function storePrepareSh(cache) {
|
|
|
195
207
|
`timeout 60 ctr -a "$A" -n k8s.io containers rm $ids >/dev/null 2>&1 || true; fi; ` +
|
|
196
208
|
// containerd 2.x keeps sandboxes in a store of their own; on the 1.7
|
|
197
209
|
// k3s ships they are ordinary containers and this is a no-op.
|
|
198
|
-
`sb=$(timeout 30 ctr -a "$A" -n k8s.io sandboxes ls
|
|
199
|
-
`[ -n "$sb" ] && timeout 60 ctr -a "$A" -n k8s.io sandboxes rm $sb >/dev/null 2>&1; return 0; }; ` +
|
|
210
|
+
`sb=$(timeout 30 ctr -a "$A" -n k8s.io sandboxes ls 2>/dev/null | awk 'NR > 1 {print $1}'); ` +
|
|
211
|
+
`[ -n "$sb" ] && timeout 60 ctr -a "$A" -n k8s.io sandboxes rm --force $sb >/dev/null 2>&1; return 0; }; ` +
|
|
200
212
|
adopt +
|
|
201
213
|
`if [ -e "$M" ]; then ` +
|
|
202
214
|
`echo 'spectest: the previous cluster on this image store never became ready; starting from an empty one' >&2; ` +
|
|
@@ -287,7 +299,7 @@ async function issueIngressCert(hostnames) {
|
|
|
287
299
|
const id = `spectest-k3s-ingress-${randomUUID().slice(0, 8)}`;
|
|
288
300
|
const keyPath = `/tmp/${id}.key`;
|
|
289
301
|
const crtPath = `/tmp/${id}.crt`;
|
|
290
|
-
const sans = hostnames.map((h) =>
|
|
302
|
+
const sans = hostnames.map((h) => `${isIP(h) ? "IP" : "DNS"}:${h}`).join(",");
|
|
291
303
|
const r = await runProcess("openssl", [
|
|
292
304
|
"req",
|
|
293
305
|
"-newkey",
|
|
@@ -1393,6 +1405,7 @@ async function collectDiagnostics(client, namespace) {
|
|
|
1393
1405
|
const DEFAULT_K3S_VERSION = "v1.33.5-k3s1";
|
|
1394
1406
|
export function k3s(opts = {}) {
|
|
1395
1407
|
const version = opts.version ?? DEFAULT_K3S_VERSION;
|
|
1408
|
+
const images = kubernetesImages(opts.images ?? []);
|
|
1396
1409
|
const extra = opts.extraArgs ?? [];
|
|
1397
1410
|
assertUsableExtraArgs(extra);
|
|
1398
1411
|
const registryEnabled = opts.registry !== false;
|
|
@@ -1409,7 +1422,8 @@ export function k3s(opts = {}) {
|
|
|
1409
1422
|
// The environment's image cache, when this VM carries one: the
|
|
1410
1423
|
// cluster's store goes on its root disk and its layers come off the
|
|
1411
1424
|
// shared layers disk (see the store section above).
|
|
1412
|
-
const
|
|
1425
|
+
const sticky = stickyDiskPathSync();
|
|
1426
|
+
const cache = sticky ? null : imageCachePathsSync();
|
|
1413
1427
|
const serverArgs = [
|
|
1414
1428
|
"k3s",
|
|
1415
1429
|
"server",
|
|
@@ -1507,6 +1521,7 @@ export function k3s(opts = {}) {
|
|
|
1507
1521
|
const readyCmd = `kubectl get --raw=/readyz >/dev/null 2>&1 && rm -f ${CLUSTER_STORE_TARGET}/${BOOT_MARKER}`;
|
|
1508
1522
|
const def = {
|
|
1509
1523
|
image: { type: "registry", reference: `rancher/k3s:${version}` },
|
|
1524
|
+
imageBuilds: images.builds,
|
|
1510
1525
|
command: cmd,
|
|
1511
1526
|
privileged: true,
|
|
1512
1527
|
tmpfs: ["/run", "/var/run"],
|
|
@@ -1520,21 +1535,23 @@ export function k3s(opts = {}) {
|
|
|
1520
1535
|
// the same netns and are reachable the same way, without appearing
|
|
1521
1536
|
// in this list (it's documentation, not a firewall).
|
|
1522
1537
|
ports: registryEnabled ? [80, 443, 6443, K3S_REGISTRY_PORT] : [80, 443, 6443],
|
|
1523
|
-
//
|
|
1538
|
+
// Sticky disks retain an independent overlayfs store per service/version.
|
|
1539
|
+
// The legacy image cache (see the store section above) keeps its
|
|
1524
1540
|
// containerd root on the image cache's root disk, the shared layers
|
|
1525
1541
|
// disk read-only at its guest path, golden's mkfs.erofs and the adopt
|
|
1526
1542
|
// helper — or, with no cache, a per-environment volume. Two clusters
|
|
1527
1543
|
// in one environment get separate roots either way (the service
|
|
1528
1544
|
// token names the directory), because two containerd daemons cannot
|
|
1529
1545
|
// share a store: bolt takes an exclusive lock.
|
|
1530
|
-
volumes:
|
|
1531
|
-
? [
|
|
1546
|
+
volumes: sticky
|
|
1547
|
+
? [{ source: `${stickyKubernetesDirectory(SELF_SERVICE_TOKEN, version)}/containerd`, target: CLUSTER_STORE_TARGET }]
|
|
1548
|
+
: cache ? [
|
|
1532
1549
|
{ source: nestedStoreDir(cache, SELF_SERVICE_TOKEN), target: CLUSTER_STORE_TARGET },
|
|
1533
1550
|
{ source: cache.layers, target: cache.layers, readOnly: true },
|
|
1534
1551
|
{ source: MKFS_EROFS_PATH, target: MKFS_EROFS_PATH, readOnly: true },
|
|
1535
1552
|
{ source: STORE_ADOPT_PATH, target: STORE_ADOPT_PATH, readOnly: true },
|
|
1536
1553
|
]
|
|
1537
|
-
|
|
1554
|
+
: [{ target: CLUSTER_STORE_TARGET }],
|
|
1538
1555
|
files: [
|
|
1539
1556
|
...(registriesYaml ? [{ path: "/etc/rancher/k3s/registries.yaml", content: registriesYaml }] : []),
|
|
1540
1557
|
...(cache
|
|
@@ -1549,12 +1566,61 @@ export function k3s(opts = {}) {
|
|
|
1549
1566
|
command: readyCmd,
|
|
1550
1567
|
timeoutSecs: opts.readyTimeoutSecs ?? 120,
|
|
1551
1568
|
},
|
|
1552
|
-
setup: async (
|
|
1569
|
+
setup: async (ctx) => {
|
|
1570
|
+
const { name, helpers, exec } = ctx;
|
|
1571
|
+
const runtime = componentRuntime(ctx);
|
|
1572
|
+
if (images.builds.length && !runtime)
|
|
1573
|
+
throw new Error(`k3s(${name}): prepared image inputs are missing`);
|
|
1574
|
+
if (runtime?.caBundle && caPresent()) {
|
|
1575
|
+
const { cert, key } = await issueIngressCert([runtime.gateway]);
|
|
1576
|
+
await installPodTrust({ name, gateway: runtime.gateway, cert, key,
|
|
1577
|
+
ca: await readFile(CA_PATH, "utf8"), bundle: await readFile(runtime.caBundle, "utf8"),
|
|
1578
|
+
}, {
|
|
1579
|
+
apply: async (configuration) => { await helpers.apply(JSON.stringify(configuration)); },
|
|
1580
|
+
ensureBundle: async (namespace, configMap, bundle) => {
|
|
1581
|
+
try {
|
|
1582
|
+
await helpers.client.core.createNamespacedConfigMap({ namespace, body: {
|
|
1583
|
+
metadata: { name: configMap, namespace }, immutable: true, data: { "ca-bundle.crt": bundle },
|
|
1584
|
+
} });
|
|
1585
|
+
}
|
|
1586
|
+
catch (err) {
|
|
1587
|
+
// A simultaneous Pod may have created the same namespace's bundle.
|
|
1588
|
+
const existing = (await helpers.client.core.readNamespacedConfigMap({ namespace, name: configMap })).unwrap();
|
|
1589
|
+
if (existing.data?.["ca-bundle.crt"] !== bundle || existing.immutable !== true)
|
|
1590
|
+
throw err;
|
|
1591
|
+
}
|
|
1592
|
+
},
|
|
1593
|
+
});
|
|
1594
|
+
// The API server observes webhook registration asynchronously. A dry-run
|
|
1595
|
+
// Pod proves TLS reachability and admission before any project workload.
|
|
1596
|
+
const deadline = Date.now() + 30_000;
|
|
1597
|
+
for (;;) {
|
|
1598
|
+
try {
|
|
1599
|
+
const pod = (await helpers.client.core.createNamespacedPod({ namespace: "default", dryRun: "All", body: {
|
|
1600
|
+
metadata: { generateName: "spectest-trust-probe-" }, spec: { containers: [{ name: "probe", image: "registry.k8s.io/pause:3.10" }] },
|
|
1601
|
+
} })).unwrap();
|
|
1602
|
+
if (pod.spec?.containers[0].env?.some(e => e.name === "SSL_CERT_FILE" && e.value === "/spectest/ca-bundle.crt"))
|
|
1603
|
+
break;
|
|
1604
|
+
if (Date.now() >= deadline)
|
|
1605
|
+
throw new Error(`k3s(${name}): Pod CA mounts did not become ready`);
|
|
1606
|
+
}
|
|
1607
|
+
catch (err) {
|
|
1608
|
+
if (Date.now() >= deadline)
|
|
1609
|
+
throw err;
|
|
1610
|
+
}
|
|
1611
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (runtime)
|
|
1615
|
+
await loadK3sBuildImages(name, runtime.images, runtime.gateway, exec);
|
|
1553
1616
|
await setupK3sCluster(name, helpers, {
|
|
1554
1617
|
registry: registryEnabled,
|
|
1555
1618
|
ingressDomains,
|
|
1556
1619
|
traefik: traefikEnabled,
|
|
1557
1620
|
});
|
|
1621
|
+
await prePullK3sImages(name, images.pulls, exec);
|
|
1622
|
+
if (sticky)
|
|
1623
|
+
await registerStickyKubernetesStore(name, version);
|
|
1558
1624
|
},
|
|
1559
1625
|
helpers: async ({ name, exec, poll }) => {
|
|
1560
1626
|
// Read the cluster's kubeconfig and address the API server by its
|
package/dist/daemon.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { componentBuildTasks, COMPONENT_RUNTIME } from "./harness/component-images.js";
|
|
1
2
|
import { androidScope, androidRecordingChunk, releaseAndroidRecording, isAndroidApp } from "./android.js";
|
|
2
3
|
// Long-running HTTP daemon. Runs as a systemd unit on the VM host (not in
|
|
3
4
|
// a container). Owns:
|
|
@@ -24,7 +25,7 @@ import net from "node:net";
|
|
|
24
25
|
import path from "node:path";
|
|
25
26
|
import { pathToFileURL } from "node:url";
|
|
26
27
|
import { ENVIRONMENT_CA_PATH as CA_PATH } from "./environment-ca.js";
|
|
27
|
-
import { assert, expect, expectRaw, lowerIngress, dnsName as makeDnsDecl, isWildcard, validateServiceImage, proxy as makeProxyDecl, } from "./index.js";
|
|
28
|
+
import { assert, expect, expectRaw, lowerIngress, dnsName as makeDnsDecl, isWildcard, validateServiceImage, normalizeServiceImage, proxy as makeProxyDecl, } from "./index.js";
|
|
28
29
|
import { COVERAGE_CONTAINER_DIR, coverageBundleRef, coverageHostDir, encodeCoverageBundle, isEmptyReport, readCoverageDir, applyCoverageDelta, walkProjectFiles, ENVIRONMENT_SERVICE, REACH_REPORT, READS_REPORT, } from "./harness/coverage.js";
|
|
29
30
|
import { configureBrowserCoverage } from "./browser-coverage.js";
|
|
30
31
|
import { INVENTORY_REPORT, NODE_COVERAGE_MARKERS_SUBDIR, NODE_COVERAGE_SCRIPTS_SUBDIR, applyCoverageAdapters, coverageAdapters, coverageReportsMode, validateCoverage, } from "./coverage.js";
|
|
@@ -57,6 +58,7 @@ import { certCovers as hostmatchCertCovers, hostWithoutPort, matchRoute, selectC
|
|
|
57
58
|
import { INGRESS_HTTPS_PORT, INGRESS_HTTP_PORT, bindRoute, clearTables, emptyTables, planBind, registryTarget, routesFor, unbindRoute, } from "./harness/ingress-table.js";
|
|
58
59
|
import { startTlsTerminator } from "./harness/tls-terminator.js";
|
|
59
60
|
import { runContainerArgs } from "./harness/container-run.js";
|
|
61
|
+
import { isOnStickyDisk } from "./harness/sticky-paths.js";
|
|
60
62
|
import { BUILDKIT_CACHE_DIR, BUILDKIT_CACHE_UNION, IMAGE_CACHE_MANIFEST, imageCachePathsSync, isOnImageCache, mergeCacheIndex, } from "./harness/image-cache.js";
|
|
61
63
|
import { assertAbsolute, certificateHostnames, defaultKeyMode, expandServiceToken, isNoopChown, mountFlag, needsIdTables, numericId, resolveChownIds, } from "./harness/file-mounts.js";
|
|
62
64
|
import { badRequest, conflict, notFound, requireString, } from "./harness/methods.js";
|
|
@@ -70,7 +72,18 @@ import { deepUnwrap, readRaw, wrap } from "./inspect.js";
|
|
|
70
72
|
import { setRecordSecrets } from "./record-secrets.js";
|
|
71
73
|
import { encodeReplayBundle, replayChunk } from "./replay-bundle.js";
|
|
72
74
|
function namedServices(cfg) {
|
|
73
|
-
return Object.entries(cfg.services).map(([name, def]) => ({ name, ...def }));
|
|
75
|
+
return Object.entries(cfg.services).map(([name, def]) => ({ name, ...def, image: normalizeServiceImage(def.image) }));
|
|
76
|
+
}
|
|
77
|
+
/** Build tasks participate in image preparation, never container startup. */
|
|
78
|
+
function imageTasks(services) {
|
|
79
|
+
const tasks = componentBuildTasks(services).map(task => ({ ...task, image: validateServiceImage(task.name, task.image) }));
|
|
80
|
+
const names = new Set(services.map(s => s.name));
|
|
81
|
+
for (const task of tasks) {
|
|
82
|
+
if (names.has(task.name))
|
|
83
|
+
throw new Error(`image task name conflicts with service: ${task.name}`);
|
|
84
|
+
names.add(task.name);
|
|
85
|
+
}
|
|
86
|
+
return [...services, ...tasks];
|
|
74
87
|
}
|
|
75
88
|
const DEFAULT_TEST_TIMEOUT_MS = 60_000;
|
|
76
89
|
const NETWORK_NAME = process.env.SPECTEST_NETWORK ?? "spectest-net";
|
|
@@ -677,7 +690,7 @@ async function ensureVolumes(svc) {
|
|
|
677
690
|
// A directory on a cache disk is the same kind of thing: a nested
|
|
678
691
|
// runtime's containerd root (`k3s()`), kept as a cache by the
|
|
679
692
|
// lineage exactly as the container store one level up is.
|
|
680
|
-
const durable = host.startsWith("/var/cache/spectest/") || isOnImageCache(host, imageCachePathsSync(IMAGE_CACHE_MANIFEST));
|
|
693
|
+
const durable = host.startsWith("/var/cache/spectest/") || isOnStickyDisk(host) || isOnImageCache(host, imageCachePathsSync(IMAGE_CACHE_MANIFEST));
|
|
681
694
|
if (vol.source?.startsWith("/") && !durable) {
|
|
682
695
|
await recordAbsoluteVolumeDir(host);
|
|
683
696
|
}
|
|
@@ -1013,6 +1026,15 @@ async function resolveDockerfileBuild(svc) {
|
|
|
1013
1026
|
};
|
|
1014
1027
|
}
|
|
1015
1028
|
async function prepareServiceImage(svc, opts) {
|
|
1029
|
+
const result = await prepareServiceImageInner(svc, opts);
|
|
1030
|
+
if (svc.image.type === "dockerfile" && svc.image.reference) {
|
|
1031
|
+
const tagged = await docker(["tag", result.tag, svc.image.reference], 30_000);
|
|
1032
|
+
if (tagged.code !== 0)
|
|
1033
|
+
throw new Error(`cannot name built image ${svc.image.reference}: ${tagged.stderr}`);
|
|
1034
|
+
}
|
|
1035
|
+
return result;
|
|
1036
|
+
}
|
|
1037
|
+
async function prepareServiceImageInner(svc, opts) {
|
|
1016
1038
|
const tag = imageTag(svc.name);
|
|
1017
1039
|
if (svc.image.type === "registry") {
|
|
1018
1040
|
const ref = svc.image.reference;
|
|
@@ -1062,7 +1084,7 @@ async function prepareServiceImage(svc, opts) {
|
|
|
1062
1084
|
progressService(svc.name, { status: "failed" });
|
|
1063
1085
|
throw err;
|
|
1064
1086
|
}
|
|
1065
|
-
//
|
|
1087
|
+
// Reuse the same application image; runtime trust is added separately.
|
|
1066
1088
|
const tagr = await docker(["tag", first.tag, tag], 30_000);
|
|
1067
1089
|
if (tagr.code !== 0) {
|
|
1068
1090
|
throw new Error(`docker tag ${first.tag} ${tag} failed: ${tagr.stderr.trim()}`);
|
|
@@ -2640,7 +2662,8 @@ function specToNamedService(spec) {
|
|
|
2640
2662
|
// run here: the image is checked (a `path` Dockerfile and a `context`
|
|
2641
2663
|
// must exist), and coverage adapters get their configure step.
|
|
2642
2664
|
const svc = { ...rest, image: validateServiceImage(name, rest.image) };
|
|
2643
|
-
|
|
2665
|
+
const configured = applyCoverageAdapters(name, svc);
|
|
2666
|
+
return { name, ...configured, image: normalizeServiceImage(configured.image) };
|
|
2644
2667
|
}
|
|
2645
2668
|
/** Implementation behind `ctx.startService` / a fake's `ctx.startService`.
|
|
2646
2669
|
* Prepares the image (pulling on first use through the host cache), runs
|
|
@@ -2931,12 +2954,14 @@ async function bootstrapInner(prepared, identity) {
|
|
|
2931
2954
|
const bootStart = Date.now();
|
|
2932
2955
|
const cfg = requireLoaded().project.environment;
|
|
2933
2956
|
const services = namedServices(cfg);
|
|
2957
|
+
const tasks = imageTasks(services);
|
|
2958
|
+
const componentTasks = componentBuildTasks(services);
|
|
2934
2959
|
const timings = new Map();
|
|
2935
2960
|
if (prepared && (prepared.version !== 1 || !prepared.images ||
|
|
2936
|
-
Object.keys(prepared.images).sort().join("\n") !==
|
|
2961
|
+
Object.keys(prepared.images).sort().join("\n") !== tasks.map(s => s.name).sort().join("\n"))) {
|
|
2937
2962
|
throw new Error("prepared image manifest does not match the environment's services");
|
|
2938
2963
|
}
|
|
2939
|
-
progressInit(
|
|
2964
|
+
progressInit(tasks);
|
|
2940
2965
|
// Build dedup is only valid within one workspace generation — a fresh
|
|
2941
2966
|
// bootstrap may follow a workspace re-upload with the same dockerfile
|
|
2942
2967
|
// text but different build-context content.
|
|
@@ -2957,7 +2982,7 @@ async function bootstrapInner(prepared, identity) {
|
|
|
2957
2982
|
// that reads it.
|
|
2958
2983
|
if ((await readIgnoreFile(path.join(WORKSPACE, ".dockerignore"))) !== null)
|
|
2959
2984
|
return;
|
|
2960
|
-
await fs.writeFile(path.join(WORKSPACE, ".dockerignore"), unionDockerignore(
|
|
2985
|
+
await fs.writeFile(path.join(WORKSPACE, ".dockerignore"), unionDockerignore(tasks));
|
|
2961
2986
|
})(),
|
|
2962
2987
|
]);
|
|
2963
2988
|
// Image prep is DECOUPLED from container start: every service's image
|
|
@@ -2993,11 +3018,15 @@ async function bootstrapInner(prepared, identity) {
|
|
|
2993
3018
|
if (found.code !== 0 || found.stdout.trim() !== entry.image) {
|
|
2994
3019
|
throw new Error(`prepared image missing for ${svc.name}: ${entry.image}`);
|
|
2995
3020
|
}
|
|
2996
|
-
|
|
3021
|
+
const tag = imageTag(svc.name);
|
|
3022
|
+
const tagged = await docker(["tag", entry.image, tag], 30_000);
|
|
3023
|
+
if (tagged.code !== 0)
|
|
3024
|
+
throw new Error(`cannot restore prepared image name for ${svc.name}: ${tagged.stderr}`);
|
|
3025
|
+
return { tag, buildSteps: entry.buildSteps };
|
|
2997
3026
|
};
|
|
2998
3027
|
const prep = new Map();
|
|
2999
3028
|
const prepOffsets = new Map();
|
|
3000
|
-
for (const svc of
|
|
3029
|
+
for (const svc of tasks) {
|
|
3001
3030
|
const p = (async () => {
|
|
3002
3031
|
const t0 = Date.now();
|
|
3003
3032
|
prepOffsets.set(svc.name, Date.now() - bootStart);
|
|
@@ -3022,7 +3051,7 @@ async function bootstrapInner(prepared, identity) {
|
|
|
3022
3051
|
// No application services, setup hooks, ingress, or runtime state in the builder.
|
|
3023
3052
|
await Promise.all(prep.values());
|
|
3024
3053
|
const images = Object.create(null);
|
|
3025
|
-
for (const svc of
|
|
3054
|
+
for (const svc of tasks) {
|
|
3026
3055
|
const out = await docker(["image", "inspect", "--format", "{{.Id}}", tags.get(svc.name)], 30_000);
|
|
3027
3056
|
const image = out.stdout.trim();
|
|
3028
3057
|
if (out.code !== 0 || !/^sha256:[a-f0-9]{64}$/.test(image)) {
|
|
@@ -3073,6 +3102,8 @@ async function bootstrapInner(prepared, identity) {
|
|
|
3073
3102
|
progressService(svc.name, { status: "probing", detail: "ready check" });
|
|
3074
3103
|
await waitForReady(svc);
|
|
3075
3104
|
const tSetup = Date.now();
|
|
3105
|
+
const builds = componentTasks.filter(task => task.owner === svc.name);
|
|
3106
|
+
await Promise.all(builds.map(task => prep.get(task.name)));
|
|
3076
3107
|
if (svc.setup) {
|
|
3077
3108
|
progressService(svc.name, { status: "probing", detail: "running setup" });
|
|
3078
3109
|
const helpers = await ensureHelpers(svc.name, svc);
|
|
@@ -3081,12 +3112,19 @@ async function bootstrapInner(prepared, identity) {
|
|
|
3081
3112
|
name: svc.name,
|
|
3082
3113
|
helpers,
|
|
3083
3114
|
...(await spectestContext({ service: svc.name, includeSelf: true })),
|
|
3115
|
+
...{ [COMPONENT_RUNTIME]: {
|
|
3116
|
+
images: builds.map(task => ({ source: tags.get(task.name), reference: task.reference })),
|
|
3117
|
+
gateway: await bridgeGatewayIp(),
|
|
3118
|
+
caBundle: await ensureCaBundle(),
|
|
3119
|
+
} },
|
|
3084
3120
|
});
|
|
3085
3121
|
}
|
|
3086
3122
|
catch (err) {
|
|
3087
3123
|
throw await withContainerPostMortem(svc.name, err);
|
|
3088
3124
|
}
|
|
3089
3125
|
}
|
|
3126
|
+
for (const task of builds)
|
|
3127
|
+
progressService(task.name, { status: "ready", detail: `loaded ${task.reference}` });
|
|
3090
3128
|
progressService(svc.name, { status: "ready", detail: undefined });
|
|
3091
3129
|
const ti = timings.get(svc.name);
|
|
3092
3130
|
if (ti) {
|
|
@@ -3545,9 +3583,10 @@ async function computeProjectReach(all) {
|
|
|
3545
3583
|
async function computeProjectReachInner(all) {
|
|
3546
3584
|
const files = await walkProjectFiles(WORKSPACE, [".git"]);
|
|
3547
3585
|
const sources = [];
|
|
3548
|
-
|
|
3586
|
+
const owners = new Map(componentBuildTasks(all).map(t => [t.name, t.owner]));
|
|
3587
|
+
for (const svc of imageTasks(all)) {
|
|
3549
3588
|
const src = {
|
|
3550
|
-
service: svc.name,
|
|
3589
|
+
service: owners.get(svc.name) ?? svc.name,
|
|
3551
3590
|
volumeSources: (svc.volumes ?? []).flatMap((v) => (v.source && !v.reachExempt ? [v.source] : [])),
|
|
3552
3591
|
...(svc.inputs && svc.inputs.length > 0 ? { inputs: [...svc.inputs] } : {}),
|
|
3553
3592
|
};
|
|
@@ -5611,7 +5650,7 @@ export function harnessMethods(state) {
|
|
|
5611
5650
|
}
|
|
5612
5651
|
}),
|
|
5613
5652
|
bootstrap: async (params) => exclusive(state, "inFlightBootstrap", "bootstrap already in progress", async () => {
|
|
5614
|
-
const prepared = params.prepared === undefined ? undefined : preparedImages(params.prepared, params.identity, namedServices(requireLoaded().project.environment).map(s => s.name));
|
|
5653
|
+
const prepared = params.prepared === undefined ? undefined : preparedImages(params.prepared, params.identity, imageTasks(namedServices(requireLoaded().project.environment)).map(s => s.name));
|
|
5615
5654
|
return { ok: true, timings: await bootstrap(prepared) };
|
|
5616
5655
|
}),
|
|
5617
5656
|
projectSetup: async () => exclusive(state, "inFlightProjectSetup", "project-setup already in progress", () => runProjectSetup()),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BuildImage, ComponentImageBuild, ServiceConfig } from "../index.js";
|
|
2
|
+
export type KubernetesImage = string | (BuildImage & {
|
|
3
|
+
reference: string;
|
|
4
|
+
});
|
|
5
|
+
/** containerd/CRI use fully qualified names, including the default tag. */
|
|
6
|
+
export declare function canonicalImageReference(reference: string): string;
|
|
7
|
+
export declare function kubernetesImages(images: readonly KubernetesImage[]): {
|
|
8
|
+
pulls: string[];
|
|
9
|
+
builds: ComponentImageBuild[];
|
|
10
|
+
};
|
|
11
|
+
/** Stable internal task names, separate from the service graph. */
|
|
12
|
+
export declare function componentBuildTasks<T extends ServiceConfig & {
|
|
13
|
+
name: string;
|
|
14
|
+
}>(services: readonly T[]): {
|
|
15
|
+
name: string;
|
|
16
|
+
owner: string;
|
|
17
|
+
reference: string;
|
|
18
|
+
image: BuildImage;
|
|
19
|
+
}[];
|
|
20
|
+
/** Private handoff to a component's setup hook. Builds stay generic; the
|
|
21
|
+
* component decides how its runtime consumes them and establishes trust. */
|
|
22
|
+
export declare const COMPONENT_RUNTIME: unique symbol;
|
|
23
|
+
export interface ComponentRuntime {
|
|
24
|
+
images: readonly {
|
|
25
|
+
source: string;
|
|
26
|
+
reference: string;
|
|
27
|
+
}[];
|
|
28
|
+
gateway: string;
|
|
29
|
+
caBundle: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function componentRuntime(context: object): ComponentRuntime | undefined;
|