@kici-dev/shared 0.4.0 → 0.6.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/cold-store/chunk-id.js +3 -2
- package/dist/cold-store/config.js +3 -3
- package/dist/container-runtime.d.ts +148 -0
- package/dist/container-runtime.js +298 -0
- package/dist/container-runtime.test.d.ts +2 -0
- package/dist/db-admin.d.ts +0 -1015
- package/dist/db-admin.js +1 -1744
- package/dist/db.js +2 -1
- package/dist/diagnostics/bundle-archive.d.ts +7 -9
- package/dist/diagnostics/bundle-archive.js +12 -85
- package/dist/diagnostics/bundle-chunks.js +1 -1
- package/dist/env/define-env.d.ts +7 -6
- package/dist/env/define-env.js +8 -7
- package/dist/graceful-shutdown.d.ts +6 -2
- package/dist/graceful-shutdown.js +1 -1
- package/dist/idempotency-files.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/telemetry/init.js +2 -1
- package/package.json +13 -7
- package/sbom.spdx.json +395 -390
|
@@ -14,14 +14,15 @@ import { sha256 } from "@kici-dev/core";
|
|
|
14
14
|
* at a trillion chunks, birthday probability is ~5e-8.
|
|
15
15
|
*/
|
|
16
16
|
function computeChunkId(args) {
|
|
17
|
-
|
|
17
|
+
const input = [
|
|
18
18
|
args.db,
|
|
19
19
|
args.table,
|
|
20
20
|
args.tenantId,
|
|
21
21
|
args.partitionDate,
|
|
22
22
|
String(args.minRowId),
|
|
23
23
|
String(args.maxRowId)
|
|
24
|
-
].join("|")
|
|
24
|
+
].join("|");
|
|
25
|
+
return sha256(input).slice(0, 16);
|
|
25
26
|
}
|
|
26
27
|
//#endregion
|
|
27
28
|
export { computeChunkId };
|
|
@@ -6,9 +6,9 @@ import "../rolldown-runtime-ClRpJifh.js";
|
|
|
6
6
|
*/
|
|
7
7
|
const DEFAULT_TABLE_CONFIG = {
|
|
8
8
|
warmTtlDays: 30,
|
|
9
|
-
minWarmTenantBytes:
|
|
10
|
-
minChunkBytes:
|
|
11
|
-
maxChunkBytes:
|
|
9
|
+
minWarmTenantBytes: 5242880,
|
|
10
|
+
minChunkBytes: 1048576,
|
|
11
|
+
maxChunkBytes: 52428800,
|
|
12
12
|
maxRowsPerCycle: 5e4,
|
|
13
13
|
enabled: true
|
|
14
14
|
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container-runtime primitives shared by every site that starts a container for
|
|
3
|
+
* KiCI: the orchestrator's container scaler backend, its bare-metal backend in
|
|
4
|
+
* container mode, and the AGENT's own job-container sandbox.
|
|
5
|
+
*
|
|
6
|
+
* They live in `@kici-dev/shared` rather than in the orchestrator because the
|
|
7
|
+
* agent needs the same runtime injection: when an ordinary agent nests a job
|
|
8
|
+
* container from a customer's image, that image is no more likely to ship Node
|
|
9
|
+
* than the one a scaler spawns. Two copies of "materialize the KiCI runtime"
|
|
10
|
+
* would be two chances to disagree on the parts that are easy to get wrong —
|
|
11
|
+
* the volume key, the root-owned copy, and the self-verification.
|
|
12
|
+
*
|
|
13
|
+
* They are separate composable functions rather than one `spawnAgentContainer`
|
|
14
|
+
* that owns the whole sequence, because the create/start step is genuinely
|
|
15
|
+
* caller-specific — network isolation, label sets, bind lists and log capture
|
|
16
|
+
* differ per caller — while the steps below are identical everywhere.
|
|
17
|
+
*
|
|
18
|
+
* `dockerode` is a TYPE-ONLY import here: every function takes an already-built
|
|
19
|
+
* client, so `@kici-dev/shared` does not depend on it at runtime and a consumer
|
|
20
|
+
* that never imports this module never pulls it in.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import type Docker from 'dockerode';
|
|
24
|
+
/**
|
|
25
|
+
* When to pull an image.
|
|
26
|
+
*
|
|
27
|
+
* Defined here rather than in the orchestrator's scaler types because
|
|
28
|
+
* `pullImageIfMissing` lives here and both the orchestrator and the agent call
|
|
29
|
+
* it. The orchestrator's `scaler/types.ts` re-exports it, so the scaler config
|
|
30
|
+
* schema and every operator-facing value stay exactly as they were.
|
|
31
|
+
*/
|
|
32
|
+
export declare const ImagePullPolicy: z.ZodEnum<{
|
|
33
|
+
Always: "Always";
|
|
34
|
+
IfNotPresent: "IfNotPresent";
|
|
35
|
+
Never: "Never";
|
|
36
|
+
}>;
|
|
37
|
+
export type ImagePullPolicy = z.infer<typeof ImagePullPolicy>;
|
|
38
|
+
/** Registry credentials in the shape a container runtime expects. */
|
|
39
|
+
export interface RegistryAuthconfig {
|
|
40
|
+
username: string;
|
|
41
|
+
password: string;
|
|
42
|
+
serveraddress: string;
|
|
43
|
+
}
|
|
44
|
+
export interface PullImageOptions {
|
|
45
|
+
docker: Docker;
|
|
46
|
+
image: string;
|
|
47
|
+
/** Absent means an anonymous pull. */
|
|
48
|
+
authconfig?: RegistryAuthconfig | undefined;
|
|
49
|
+
signal?: AbortSignal | undefined;
|
|
50
|
+
/** Progress sink; callers surface this as a scaler event. */
|
|
51
|
+
onProgress?: ((message: string) => void) | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* When to pull. Defaults to `IfNotPresent`, which is right for KiCI's own
|
|
54
|
+
* agent images (pinned and immutable, so re-pulling every spawn only storms
|
|
55
|
+
* the registry). A label set on a moving tag sets `Always`.
|
|
56
|
+
*/
|
|
57
|
+
pullPolicy?: ImagePullPolicy | undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Pull `image` according to the pull policy. Returns whether a pull ran.
|
|
61
|
+
*
|
|
62
|
+
* Authenticated when an authconfig is supplied — a private registry otherwise
|
|
63
|
+
* fails the pull with a 401 that reads like a missing image.
|
|
64
|
+
*/
|
|
65
|
+
export declare function pullImageIfMissing(opts: PullImageOptions): Promise<boolean>;
|
|
66
|
+
/** Read-only mount point of the KiCI-provisioned runtime inside a container. */
|
|
67
|
+
export declare const RUNTIME_MOUNT = "/opt/kici";
|
|
68
|
+
/** Read-only mount point of the injected Node tree inside a job container. */
|
|
69
|
+
export declare const RUNTIME_NODE_MOUNT = "/opt/kici/node";
|
|
70
|
+
/**
|
|
71
|
+
* Bind spec that injects the KiCI runtime into a container.
|
|
72
|
+
*
|
|
73
|
+
* Read-only is load-bearing: a job that could rewrite the runtime would control
|
|
74
|
+
* the interpreter every later step runs under.
|
|
75
|
+
*/
|
|
76
|
+
export declare function runtimeInjectBind(hostRuntimeDir: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Command that starts the agent from the INJECTED runtime.
|
|
79
|
+
*
|
|
80
|
+
* Only meaningful when the runtime is mounted. A spawned job image has its own
|
|
81
|
+
* default CMD — python's shell, a node REPL, whatever the customer's image
|
|
82
|
+
* declares — so without overriding it the container starts that instead and no
|
|
83
|
+
* agent ever registers. The job then waits for an agent that will never arrive.
|
|
84
|
+
*
|
|
85
|
+
* Absolute on both halves: the image is not required to ship Node, and the
|
|
86
|
+
* agent lives inside the runtime tree rather than at the image's own /app.
|
|
87
|
+
*/
|
|
88
|
+
export declare function injectedAgentCommand(): string[];
|
|
89
|
+
/**
|
|
90
|
+
* Which part of an agent image's `/opt/kici` tree a caller needs.
|
|
91
|
+
*
|
|
92
|
+
* - `all` — node PLUS the agent application. What a spawn that runs the AGENT
|
|
93
|
+
* ITSELF out of the volume needs (the per-job-image topologies), because the
|
|
94
|
+
* entrypoint resolves inside the mounted tree.
|
|
95
|
+
* - `node` — the Node tree alone, mounted at `/opt/kici/node`. What an agent
|
|
96
|
+
* nesting a job container needs: the runner bundle is bind-mounted from that
|
|
97
|
+
* agent's OWN build, so the runner and the agent driving it can never be two
|
|
98
|
+
* different versions.
|
|
99
|
+
*
|
|
100
|
+
* They are separate volumes rather than one tree mounted twice because the
|
|
101
|
+
* sandbox binds its runner and loader hook at fixed paths under `/opt/kici`.
|
|
102
|
+
* Mounting the whole tree read-only at `/opt/kici` would put those two binds
|
|
103
|
+
* INSIDE a read-only mount, whose mountpoints cannot be created.
|
|
104
|
+
*/
|
|
105
|
+
export declare const RuntimeSubtree: z.ZodEnum<{
|
|
106
|
+
all: "all";
|
|
107
|
+
node: "node";
|
|
108
|
+
}>;
|
|
109
|
+
export type RuntimeSubtree = z.infer<typeof RuntimeSubtree>;
|
|
110
|
+
/**
|
|
111
|
+
* Name of the shared volume holding the KiCI runtime, keyed by image IDENTITY.
|
|
112
|
+
*
|
|
113
|
+
* Keyed by the image's content id, never its name:tag. A tag moves — `:stg`,
|
|
114
|
+
* `:latest`, and every E2E tag are rebuilt in place — so a name-keyed volume is
|
|
115
|
+
* reused after the image it was copied from has been replaced, which is exactly
|
|
116
|
+
* the "silently reusing the previous version's binaries" failure this is
|
|
117
|
+
* supposed to prevent. It bit: a volume populated from a pre-/opt/kici image was
|
|
118
|
+
* reused after the rebuild, and every spawned container failed to start because
|
|
119
|
+
* the runtime it mounted was empty.
|
|
120
|
+
*
|
|
121
|
+
* The subtree is part of the name for the same reason: an `all` volume and a
|
|
122
|
+
* `node` volume have different roots, so sharing one name would mount a tree
|
|
123
|
+
* whose `bin/node` is one level off.
|
|
124
|
+
*/
|
|
125
|
+
export declare function runtimeVolumeName(imageId: string, subtree?: RuntimeSubtree): string;
|
|
126
|
+
export interface EnsureRuntimeVolumeOptions {
|
|
127
|
+
docker: Docker;
|
|
128
|
+
/** The published kici-agent image, which carries /opt/kici. */
|
|
129
|
+
agentImage: string;
|
|
130
|
+
/** Which part of the tree to materialize. Defaults to the whole runtime. */
|
|
131
|
+
subtree?: RuntimeSubtree | undefined;
|
|
132
|
+
signal?: AbortSignal | undefined;
|
|
133
|
+
onProgress?: ((message: string) => void) | undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Materialize the KiCI runtime into a named volume that job containers mount.
|
|
137
|
+
*
|
|
138
|
+
* The runtime lives inside the `kici-agent` image, but a bind mount needs a
|
|
139
|
+
* HOST path — and the orchestrator (or the agent) may itself be containerized,
|
|
140
|
+
* so it cannot assume /opt/kici exists on the host filesystem. Copying the tree
|
|
141
|
+
* out of the image into a named volume once, then mounting that volume, works
|
|
142
|
+
* the same whether the caller runs on bare metal or in a container.
|
|
143
|
+
*
|
|
144
|
+
* Idempotent: the volume is created once per (agent image, subtree) and reused.
|
|
145
|
+
* Returns the volume name to mount.
|
|
146
|
+
*/
|
|
147
|
+
export declare function ensureRuntimeVolume(opts: EnsureRuntimeVolumeOptions): Promise<string>;
|
|
148
|
+
//# sourceMappingURL=container-runtime.d.ts.map
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import "./rolldown-runtime-ClRpJifh.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/container-runtime.ts
|
|
4
|
+
/**
|
|
5
|
+
* Container-runtime primitives shared by every site that starts a container for
|
|
6
|
+
* KiCI: the orchestrator's container scaler backend, its bare-metal backend in
|
|
7
|
+
* container mode, and the AGENT's own job-container sandbox.
|
|
8
|
+
*
|
|
9
|
+
* They live in `@kici-dev/shared` rather than in the orchestrator because the
|
|
10
|
+
* agent needs the same runtime injection: when an ordinary agent nests a job
|
|
11
|
+
* container from a customer's image, that image is no more likely to ship Node
|
|
12
|
+
* than the one a scaler spawns. Two copies of "materialize the KiCI runtime"
|
|
13
|
+
* would be two chances to disagree on the parts that are easy to get wrong —
|
|
14
|
+
* the volume key, the root-owned copy, and the self-verification.
|
|
15
|
+
*
|
|
16
|
+
* They are separate composable functions rather than one `spawnAgentContainer`
|
|
17
|
+
* that owns the whole sequence, because the create/start step is genuinely
|
|
18
|
+
* caller-specific — network isolation, label sets, bind lists and log capture
|
|
19
|
+
* differ per caller — while the steps below are identical everywhere.
|
|
20
|
+
*
|
|
21
|
+
* `dockerode` is a TYPE-ONLY import here: every function takes an already-built
|
|
22
|
+
* client, so `@kici-dev/shared` does not depend on it at runtime and a consumer
|
|
23
|
+
* that never imports this module never pulls it in.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* When to pull an image.
|
|
27
|
+
*
|
|
28
|
+
* Defined here rather than in the orchestrator's scaler types because
|
|
29
|
+
* `pullImageIfMissing` lives here and both the orchestrator and the agent call
|
|
30
|
+
* it. The orchestrator's `scaler/types.ts` re-exports it, so the scaler config
|
|
31
|
+
* schema and every operator-facing value stay exactly as they were.
|
|
32
|
+
*/
|
|
33
|
+
const ImagePullPolicy = z.enum([
|
|
34
|
+
"Always",
|
|
35
|
+
"IfNotPresent",
|
|
36
|
+
"Never"
|
|
37
|
+
]);
|
|
38
|
+
/**
|
|
39
|
+
* Pull `image` according to the pull policy. Returns whether a pull ran.
|
|
40
|
+
*
|
|
41
|
+
* Authenticated when an authconfig is supplied — a private registry otherwise
|
|
42
|
+
* fails the pull with a 401 that reads like a missing image.
|
|
43
|
+
*/
|
|
44
|
+
async function pullImageIfMissing(opts) {
|
|
45
|
+
const { docker, image, authconfig, signal } = opts;
|
|
46
|
+
const policy = opts.pullPolicy ?? ImagePullPolicy.enum.IfNotPresent;
|
|
47
|
+
if (policy === ImagePullPolicy.enum.Never) return false;
|
|
48
|
+
if (policy === ImagePullPolicy.enum.IfNotPresent) try {
|
|
49
|
+
await docker.getImage(image).inspect({ ...signal ? { abortSignal: signal } : {} });
|
|
50
|
+
return false;
|
|
51
|
+
} catch {}
|
|
52
|
+
if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("container spawn aborted");
|
|
53
|
+
opts.onProgress?.(`pulling image ${image}`);
|
|
54
|
+
const stream = await docker.pull(image, {
|
|
55
|
+
...signal ? { abortSignal: signal } : {},
|
|
56
|
+
...authconfig ? { authconfig } : {}
|
|
57
|
+
});
|
|
58
|
+
await new Promise((resolve, reject) => {
|
|
59
|
+
docker.modem.followProgress(stream, (err) => err ? reject(err) : resolve());
|
|
60
|
+
});
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/** Read-only mount point of the KiCI-provisioned runtime inside a container. */
|
|
64
|
+
const RUNTIME_MOUNT = "/opt/kici";
|
|
65
|
+
/** Read-only mount point of the injected Node tree inside a job container. */
|
|
66
|
+
const RUNTIME_NODE_MOUNT = `${RUNTIME_MOUNT}/node`;
|
|
67
|
+
/**
|
|
68
|
+
* Bind spec that injects the KiCI runtime into a container.
|
|
69
|
+
*
|
|
70
|
+
* Read-only is load-bearing: a job that could rewrite the runtime would control
|
|
71
|
+
* the interpreter every later step runs under.
|
|
72
|
+
*/
|
|
73
|
+
function runtimeInjectBind(hostRuntimeDir) {
|
|
74
|
+
return `${hostRuntimeDir}:${RUNTIME_MOUNT}:ro`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Command that starts the agent from the INJECTED runtime.
|
|
78
|
+
*
|
|
79
|
+
* Only meaningful when the runtime is mounted. A spawned job image has its own
|
|
80
|
+
* default CMD — python's shell, a node REPL, whatever the customer's image
|
|
81
|
+
* declares — so without overriding it the container starts that instead and no
|
|
82
|
+
* agent ever registers. The job then waits for an agent that will never arrive.
|
|
83
|
+
*
|
|
84
|
+
* Absolute on both halves: the image is not required to ship Node, and the
|
|
85
|
+
* agent lives inside the runtime tree rather than at the image's own /app.
|
|
86
|
+
*/
|
|
87
|
+
function injectedAgentCommand() {
|
|
88
|
+
return [`${RUNTIME_MOUNT}/node/bin/node`, `${RUNTIME_MOUNT}/app/packages/agent/dist/server.js`];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Which part of an agent image's `/opt/kici` tree a caller needs.
|
|
92
|
+
*
|
|
93
|
+
* - `all` — node PLUS the agent application. What a spawn that runs the AGENT
|
|
94
|
+
* ITSELF out of the volume needs (the per-job-image topologies), because the
|
|
95
|
+
* entrypoint resolves inside the mounted tree.
|
|
96
|
+
* - `node` — the Node tree alone, mounted at `/opt/kici/node`. What an agent
|
|
97
|
+
* nesting a job container needs: the runner bundle is bind-mounted from that
|
|
98
|
+
* agent's OWN build, so the runner and the agent driving it can never be two
|
|
99
|
+
* different versions.
|
|
100
|
+
*
|
|
101
|
+
* They are separate volumes rather than one tree mounted twice because the
|
|
102
|
+
* sandbox binds its runner and loader hook at fixed paths under `/opt/kici`.
|
|
103
|
+
* Mounting the whole tree read-only at `/opt/kici` would put those two binds
|
|
104
|
+
* INSIDE a read-only mount, whose mountpoints cannot be created.
|
|
105
|
+
*/
|
|
106
|
+
const RuntimeSubtree = z.enum(["all", "node"]);
|
|
107
|
+
/**
|
|
108
|
+
* Name of the shared volume holding the KiCI runtime, keyed by image IDENTITY.
|
|
109
|
+
*
|
|
110
|
+
* Keyed by the image's content id, never its name:tag. A tag moves — `:stg`,
|
|
111
|
+
* `:latest`, and every E2E tag are rebuilt in place — so a name-keyed volume is
|
|
112
|
+
* reused after the image it was copied from has been replaced, which is exactly
|
|
113
|
+
* the "silently reusing the previous version's binaries" failure this is
|
|
114
|
+
* supposed to prevent. It bit: a volume populated from a pre-/opt/kici image was
|
|
115
|
+
* reused after the rebuild, and every spawned container failed to start because
|
|
116
|
+
* the runtime it mounted was empty.
|
|
117
|
+
*
|
|
118
|
+
* The subtree is part of the name for the same reason: an `all` volume and a
|
|
119
|
+
* `node` volume have different roots, so sharing one name would mount a tree
|
|
120
|
+
* whose `bin/node` is one level off.
|
|
121
|
+
*/
|
|
122
|
+
function runtimeVolumeName(imageId, subtree = RuntimeSubtree.enum.all) {
|
|
123
|
+
const slug = imageId.replace(/^sha256:/, "").replace(/[^a-zA-Z0-9_.-]/g, "-").slice(0, 32);
|
|
124
|
+
return `kici-runtime-${subtree === RuntimeSubtree.enum.node ? "node-" : ""}${slug}`;
|
|
125
|
+
}
|
|
126
|
+
/** Where the populator and the marker probe mount the volume being filled. */
|
|
127
|
+
const RUNTIME_OUT = "/kici-runtime-out";
|
|
128
|
+
/**
|
|
129
|
+
* Marker file proving the volume holds a COMPLETE runtime.
|
|
130
|
+
*
|
|
131
|
+
* Volume existence is not population. A volume is created before the copy runs,
|
|
132
|
+
* so a concurrent caller's `inspect()` succeeds against a volume that is still
|
|
133
|
+
* being filled, and it mounts a half-copied tree — which surfaces far from the
|
|
134
|
+
* cause, typically as a zod ESM SyntaxError from a partially written module.
|
|
135
|
+
* The marker is written LAST, after the populator's own `test -x` checks, so it
|
|
136
|
+
* can only appear on a tree that already verified.
|
|
137
|
+
*/
|
|
138
|
+
const RUNTIME_COMPLETE_MARKER = `${RUNTIME_OUT}/.kici-runtime-complete`;
|
|
139
|
+
/**
|
|
140
|
+
* The self-verifying copy each subtree runs in the populator container.
|
|
141
|
+
*
|
|
142
|
+
* The verification is not decoration. `cp -a` of an absent or empty source
|
|
143
|
+
* exits 0, leaving an EMPTY volume — and the spawn then creates a container
|
|
144
|
+
* whose runtime mount has no node, which dies at start with no error recorded
|
|
145
|
+
* anywhere near the cause.
|
|
146
|
+
*
|
|
147
|
+
* The completion marker is the LAST command in both branches: written any
|
|
148
|
+
* earlier it would land on a tree that has not been verified, which is the
|
|
149
|
+
* exact failure it exists to prevent.
|
|
150
|
+
*/
|
|
151
|
+
function populatorCommand(subtree) {
|
|
152
|
+
const out = RUNTIME_OUT;
|
|
153
|
+
if (subtree === RuntimeSubtree.enum.node) return `set -e; cp -a ${RUNTIME_MOUNT}/node/. ${out}/; test -x ${out}/bin/node; touch ${RUNTIME_COMPLETE_MARKER}`;
|
|
154
|
+
return `set -e; cp -a ${RUNTIME_MOUNT}/. ${out}/; test -x ${out}/node/bin/node; test -f ${out}/app/packages/agent/dist/server.js; touch ${RUNTIME_COMPLETE_MARKER}`;
|
|
155
|
+
}
|
|
156
|
+
/** What the populator container printed, for an error that has to explain itself. */
|
|
157
|
+
async function populatorOutput(container) {
|
|
158
|
+
try {
|
|
159
|
+
const buf = await container.logs({
|
|
160
|
+
stdout: true,
|
|
161
|
+
stderr: true,
|
|
162
|
+
tail: 20
|
|
163
|
+
});
|
|
164
|
+
return Buffer.isBuffer(buf) ? buf.toString("utf-8").trim() : String(buf).trim();
|
|
165
|
+
} catch {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Materialize the KiCI runtime into a named volume that job containers mount.
|
|
171
|
+
*
|
|
172
|
+
* The runtime lives inside the `kici-agent` image, but a bind mount needs a
|
|
173
|
+
* HOST path — and the orchestrator (or the agent) may itself be containerized,
|
|
174
|
+
* so it cannot assume /opt/kici exists on the host filesystem. Copying the tree
|
|
175
|
+
* out of the image into a named volume once, then mounting that volume, works
|
|
176
|
+
* the same whether the caller runs on bare metal or in a container.
|
|
177
|
+
*
|
|
178
|
+
* Idempotent: the volume is created once per (agent image, subtree) and reused.
|
|
179
|
+
* Returns the volume name to mount.
|
|
180
|
+
*/
|
|
181
|
+
async function ensureRuntimeVolume(opts) {
|
|
182
|
+
const { docker, agentImage, signal } = opts;
|
|
183
|
+
const subtree = opts.subtree ?? RuntimeSubtree.enum.all;
|
|
184
|
+
await pullImageIfMissing({
|
|
185
|
+
docker,
|
|
186
|
+
image: agentImage,
|
|
187
|
+
...signal ? { signal } : {}
|
|
188
|
+
});
|
|
189
|
+
const name = runtimeVolumeName((await docker.getImage(agentImage).inspect()).Id ?? agentImage, subtree);
|
|
190
|
+
const inFlight = inFlightRuntimeVolumes.get(name);
|
|
191
|
+
if (inFlight) return await inFlight;
|
|
192
|
+
const populating = materializeRuntimeVolume({
|
|
193
|
+
...opts,
|
|
194
|
+
subtree,
|
|
195
|
+
name
|
|
196
|
+
}).finally(() => {
|
|
197
|
+
inFlightRuntimeVolumes.delete(name);
|
|
198
|
+
});
|
|
199
|
+
inFlightRuntimeVolumes.set(name, populating);
|
|
200
|
+
return await populating;
|
|
201
|
+
}
|
|
202
|
+
/** Per-volume populate in flight right now, so concurrent callers share one. */
|
|
203
|
+
const inFlightRuntimeVolumes = /* @__PURE__ */ new Map();
|
|
204
|
+
/**
|
|
205
|
+
* Does this volume carry the completion marker?
|
|
206
|
+
*
|
|
207
|
+
* A short-lived container is the only way to read a named volume's contents:
|
|
208
|
+
* the daemon owns the mount, and the caller may itself be containerized, so
|
|
209
|
+
* there is no host path to stat. Mounted the same way the populator mounts it
|
|
210
|
+
* (root, same bind) so the probe sees exactly what the populator wrote.
|
|
211
|
+
*
|
|
212
|
+
* Any failure to answer reads as NOT populated. Repopulating a good volume
|
|
213
|
+
* costs one redundant copy; mounting an unverified one is the bug.
|
|
214
|
+
*/
|
|
215
|
+
async function runtimeVolumeIsPopulated(args) {
|
|
216
|
+
const { docker, name, agentImage } = args;
|
|
217
|
+
let probe;
|
|
218
|
+
try {
|
|
219
|
+
probe = await docker.createContainer({
|
|
220
|
+
Image: agentImage,
|
|
221
|
+
User: "0:0",
|
|
222
|
+
Cmd: [
|
|
223
|
+
"sh",
|
|
224
|
+
"-c",
|
|
225
|
+
`test -f ${RUNTIME_COMPLETE_MARKER}`
|
|
226
|
+
],
|
|
227
|
+
Labels: { "kici-managed": "true" },
|
|
228
|
+
HostConfig: {
|
|
229
|
+
Binds: [`${name}:${RUNTIME_OUT}`],
|
|
230
|
+
AutoRemove: false
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
await probe.start();
|
|
234
|
+
return (await probe.wait()).StatusCode === 0;
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
} finally {
|
|
238
|
+
await probe?.remove({ force: true }).catch(() => void 0);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Populate the volume, reusing it only when the marker proves it is complete. */
|
|
242
|
+
async function materializeRuntimeVolume(opts) {
|
|
243
|
+
const { docker, agentImage, subtree, name } = opts;
|
|
244
|
+
const exists = await docker.getVolume(name).inspect().then(() => true).catch(() => false);
|
|
245
|
+
if (exists && await runtimeVolumeIsPopulated({
|
|
246
|
+
docker,
|
|
247
|
+
name,
|
|
248
|
+
agentImage
|
|
249
|
+
})) return name;
|
|
250
|
+
opts.onProgress?.(`materializing the KiCI runtime from ${agentImage}`);
|
|
251
|
+
await docker.createVolume({
|
|
252
|
+
Name: name,
|
|
253
|
+
Labels: { "kici-managed": "true" }
|
|
254
|
+
});
|
|
255
|
+
const populator = await docker.createContainer({
|
|
256
|
+
Image: agentImage,
|
|
257
|
+
User: "0:0",
|
|
258
|
+
Cmd: [
|
|
259
|
+
"sh",
|
|
260
|
+
"-c",
|
|
261
|
+
populatorCommand(subtree)
|
|
262
|
+
],
|
|
263
|
+
Labels: { "kici-managed": "true" },
|
|
264
|
+
HostConfig: {
|
|
265
|
+
Binds: [`${name}:${RUNTIME_OUT}`],
|
|
266
|
+
AutoRemove: false
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
let populatorDropped = false;
|
|
270
|
+
const dropPopulator = async () => {
|
|
271
|
+
if (populatorDropped) return;
|
|
272
|
+
populatorDropped = true;
|
|
273
|
+
await populator.remove({ force: true }).catch(() => void 0);
|
|
274
|
+
};
|
|
275
|
+
try {
|
|
276
|
+
await populator.start();
|
|
277
|
+
const result = await populator.wait();
|
|
278
|
+
if (result.StatusCode !== 0) {
|
|
279
|
+
const output = await populatorOutput(populator);
|
|
280
|
+
throw new Error(`runtime copy exited ${result.StatusCode ?? "unknown"}` + (output ? `: ${output}` : " with no output"));
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
await dropPopulator();
|
|
284
|
+
if (!exists && !await runtimeVolumeIsPopulated({
|
|
285
|
+
docker,
|
|
286
|
+
name,
|
|
287
|
+
agentImage
|
|
288
|
+
})) await docker.getVolume(name).remove({ force: true }).catch(() => void 0);
|
|
289
|
+
throw new Error(`Failed to materialize the KiCI runtime from ${agentImage}: ${err instanceof Error ? err.message : String(err)}. The image must carry /opt/kici/node and /opt/kici/app.`);
|
|
290
|
+
} finally {
|
|
291
|
+
await dropPopulator();
|
|
292
|
+
}
|
|
293
|
+
return name;
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
296
|
+
export { ImagePullPolicy, RUNTIME_MOUNT, RUNTIME_NODE_MOUNT, RuntimeSubtree, ensureRuntimeVolume, injectedAgentCommand, pullImageIfMissing, runtimeInjectBind, runtimeVolumeName };
|
|
297
|
+
|
|
298
|
+
//# sourceMappingURL=container-runtime.js.map
|