@cat-factory/integrations 0.30.0 → 0.32.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/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/modules/environments/EnvironmentConnectionService.d.ts +24 -6
- package/dist/modules/environments/EnvironmentConnectionService.d.ts.map +1 -1
- package/dist/modules/environments/EnvironmentConnectionService.js +50 -12
- package/dist/modules/environments/EnvironmentConnectionService.js.map +1 -1
- package/dist/modules/environments/environments.logic.d.ts.map +1 -1
- package/dist/modules/environments/environments.logic.js +2 -99
- package/dist/modules/environments/environments.logic.js.map +1 -1
- package/dist/modules/kubernetes/KubernetesRunnerTransport.d.ts +29 -0
- package/dist/modules/kubernetes/KubernetesRunnerTransport.d.ts.map +1 -0
- package/dist/modules/kubernetes/KubernetesRunnerTransport.js +202 -0
- package/dist/modules/kubernetes/KubernetesRunnerTransport.js.map +1 -0
- package/dist/modules/kubernetes/kubernetes.logic.d.ts +54 -0
- package/dist/modules/kubernetes/kubernetes.logic.d.ts.map +1 -0
- package/dist/modules/kubernetes/kubernetes.logic.js +169 -0
- package/dist/modules/kubernetes/kubernetes.logic.js.map +1 -0
- package/dist/modules/runners/RunnerPoolConnectionService.d.ts +35 -23
- package/dist/modules/runners/RunnerPoolConnectionService.d.ts.map +1 -1
- package/dist/modules/runners/RunnerPoolConnectionService.js +94 -50
- package/dist/modules/runners/RunnerPoolConnectionService.js.map +1 -1
- package/dist/modules/runners/runner-backends.d.ts +51 -0
- package/dist/modules/runners/runner-backends.d.ts.map +1 -0
- package/dist/modules/runners/runner-backends.js +101 -0
- package/dist/modules/runners/runner-backends.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { apiBase, buildPodManifest, classifyPodReadiness, KUBERNETES_TOKEN_KEY, podName, podUrl, podsUrl, proxyUrl, } from './kubernetes.logic.js';
|
|
2
|
+
// Native Kubernetes runner transport (target k8s 1.35+). One bare Pod per RUN,
|
|
3
|
+
// named deterministically from `ref.runId`; every step of the run re-attaches to
|
|
4
|
+
// that pod by `ref.jobId` — mirroring CloudflareContainerTransport's per-run model
|
|
5
|
+
// and the harness's per-run-container assumption. The orchestrator reaches the
|
|
6
|
+
// per-pod executor-harness HTTP server through the kube-apiserver POD-PROXY
|
|
7
|
+
// subresource, so it needs only HTTPS to the apiserver (no in-cluster networking,
|
|
8
|
+
// no per-run Service/Ingress) and the full RunnerJobView fidelity is preserved
|
|
9
|
+
// verbatim — the harness is unchanged.
|
|
10
|
+
//
|
|
11
|
+
// Auth to the apiserver is a Bearer ServiceAccount token (secret key `apiToken`),
|
|
12
|
+
// needing RBAC `create/get/delete` on `pods` and `create/get` on `pods/proxy` in
|
|
13
|
+
// the namespace. The pod itself has no Service, so its harness is reachable only
|
|
14
|
+
// via the RBAC-gated proxy — no inbound harness shared secret is required.
|
|
15
|
+
// The eviction marker the engine classifies (job.logic `isContainerEvictionError`):
|
|
16
|
+
// a 404 from the proxy means the pod vanished (deleted/crashed/evicted).
|
|
17
|
+
const EVICTION_ERROR = 'Job not found (container evicted or crashed)';
|
|
18
|
+
const DISPATCH_TIMEOUT_MS = 30_000;
|
|
19
|
+
const POLL_TIMEOUT_MS = 30_000;
|
|
20
|
+
// Bounded readiness wait inside dispatch. The engine treats `dispatch` as blocking
|
|
21
|
+
// until the runner has accepted the job (a plain dispatch throw hard-fails the run as
|
|
22
|
+
// `failureKind: 'dispatch'`), exactly like the Cloudflare container backend, so we
|
|
23
|
+
// must wait here rather than fail fast. The window is generous enough to cover a cold
|
|
24
|
+
// first image pull in one shot; on a readiness failure we surface a RECOVERABLE
|
|
25
|
+
// eviction (see EVICTION_ERROR) so the durable driver re-drives — by then the pod is
|
|
26
|
+
// created (ensurePod 409s) and its image is cached, so the re-drive proceeds.
|
|
27
|
+
const READY_WAIT_MS = 120_000;
|
|
28
|
+
const READY_POLL_INTERVAL_MS = 1_500;
|
|
29
|
+
export class KubernetesRunnerTransport {
|
|
30
|
+
config;
|
|
31
|
+
resolveSecret;
|
|
32
|
+
constructor(config, resolveSecret) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
this.resolveSecret = resolveSecret;
|
|
35
|
+
}
|
|
36
|
+
async dispatch(ref, spec, kind = 'agent', options) {
|
|
37
|
+
const name = podName(ref.runId);
|
|
38
|
+
await this.ensurePod(name, ref.runId, options);
|
|
39
|
+
await this.waitForPodReady(name);
|
|
40
|
+
const res = await this.proxyFetch('POST', name, '/jobs', { ...spec, kind }, DISPATCH_TIMEOUT_MS);
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`Container dispatch failed (HTTP ${res.status}): ${await safeText(res)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async poll(ref) {
|
|
46
|
+
const name = podName(ref.runId);
|
|
47
|
+
const res = await this.proxyFetch('GET', name, `/jobs/${encodeURIComponent(ref.jobId)}`, undefined, POLL_TIMEOUT_MS);
|
|
48
|
+
if (res.status === 404) {
|
|
49
|
+
// The pod-proxy 404s when the pod is gone (deleted/crashed/evicted) — the
|
|
50
|
+
// harness keeps a finished job's view, so a 404 is the pod vanishing, not a
|
|
51
|
+
// forgotten job. Report it as the eviction the engine recovers from.
|
|
52
|
+
return { state: 'failed', error: EVICTION_ERROR };
|
|
53
|
+
}
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new Error(`Container job poll failed (HTTP ${res.status}): ${await safeText(res)}`);
|
|
56
|
+
}
|
|
57
|
+
return (await res.json());
|
|
58
|
+
}
|
|
59
|
+
/** Reclaim the run's pod (idempotent — a missing pod is a no-op). */
|
|
60
|
+
async release(ref) {
|
|
61
|
+
const name = podName(ref.runId);
|
|
62
|
+
const res = await this.apiFetch('DELETE', podUrl(this.config, name), undefined, DISPATCH_TIMEOUT_MS);
|
|
63
|
+
// A 404 means the pod is already gone — idempotent success. Any other failure
|
|
64
|
+
// (e.g. a 403 from a token lacking `delete`, or a transient 5xx) must NOT be
|
|
65
|
+
// swallowed: a bare Pod (restartPolicy: Never, no owner ref / Job TTL) is not
|
|
66
|
+
// garbage-collected, so a silently-dropped delete leaks the pod (and its node
|
|
67
|
+
// slot) indefinitely. Throw so the caller's best-effort wrapper records it (the
|
|
68
|
+
// LoggingRunnerTransport logs a `release` failure instead of a false success).
|
|
69
|
+
if (!res.ok && res.status !== 404) {
|
|
70
|
+
throw new Error(`Failed to release runner pod '${name}' (HTTP ${res.status}): ${await safeText(res)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Probe the apiserver with the configured token (lists pods; nothing created). */
|
|
74
|
+
async testConnection() {
|
|
75
|
+
try {
|
|
76
|
+
const res = await this.apiFetch('GET', `${podsUrl(this.config)}?limit=1`, undefined, DISPATCH_TIMEOUT_MS);
|
|
77
|
+
if (res.ok) {
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
message: `Reached ${apiBase(this.config)} (namespace ${this.config.namespace}).`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
message: `apiserver responded ${res.status}: ${await safeText(res)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// --- internals ----------------------------------------------------------
|
|
93
|
+
async ensurePod(name, runId, options) {
|
|
94
|
+
const manifest = buildPodManifest(this.config, runId, name, options);
|
|
95
|
+
const res = await this.apiFetch('POST', podsUrl(this.config), manifest, DISPATCH_TIMEOUT_MS);
|
|
96
|
+
// 409 AlreadyExists ⇒ the run's pod is already up (a later step or a replay):
|
|
97
|
+
// idempotent re-attach, exactly like CloudflareContainerTransport.
|
|
98
|
+
if (res.ok || res.status === 409)
|
|
99
|
+
return;
|
|
100
|
+
throw new Error(`Failed to create runner pod (HTTP ${res.status}): ${await safeText(res)}`);
|
|
101
|
+
}
|
|
102
|
+
async waitForPodReady(name) {
|
|
103
|
+
const deadline = Date.now() + READY_WAIT_MS;
|
|
104
|
+
// Every readiness failure carries the eviction marker so the engine recovers it by
|
|
105
|
+
// re-driving the step (the re-drive re-attaches to the existing pod, by then ready
|
|
106
|
+
// / image-cached) instead of hard-failing the run on a cold pull or a transient
|
|
107
|
+
// pod blip. See EVICTION_ERROR and job.logic `isContainerEvictionError`.
|
|
108
|
+
const recoverable = (reason) => new Error(`${reason} (container evicted or crashed)`);
|
|
109
|
+
for (;;) {
|
|
110
|
+
const res = await this.apiFetch('GET', podUrl(this.config, name), undefined, POLL_TIMEOUT_MS);
|
|
111
|
+
if (res.status === 404) {
|
|
112
|
+
throw recoverable(`Runner pod '${name}' vanished before it became ready`);
|
|
113
|
+
}
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
const readiness = classifyPodReadiness(await res.json());
|
|
116
|
+
if (readiness === 'ready')
|
|
117
|
+
return;
|
|
118
|
+
if (readiness === 'gone') {
|
|
119
|
+
throw recoverable(`Runner pod '${name}' terminated before serving`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (Date.now() >= deadline) {
|
|
123
|
+
throw recoverable(`Runner pod '${name}' not ready within ${READY_WAIT_MS}ms`);
|
|
124
|
+
}
|
|
125
|
+
await sleep(READY_POLL_INTERVAL_MS);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
proxyFetch(method, name, path, body, timeoutMs) {
|
|
129
|
+
return this.apiFetch(method, proxyUrl(this.config, name, path), body, timeoutMs);
|
|
130
|
+
}
|
|
131
|
+
async apiFetch(method, url, body, timeoutMs) {
|
|
132
|
+
const token = this.resolveSecret(KUBERNETES_TOKEN_KEY);
|
|
133
|
+
if (!token)
|
|
134
|
+
throw new Error(`Missing Kubernetes ServiceAccount token ('${KUBERNETES_TOKEN_KEY}')`);
|
|
135
|
+
const headers = {
|
|
136
|
+
authorization: `Bearer ${token}`,
|
|
137
|
+
accept: 'application/json',
|
|
138
|
+
};
|
|
139
|
+
let payload;
|
|
140
|
+
if (body !== undefined && method !== 'GET' && method !== 'DELETE') {
|
|
141
|
+
payload = JSON.stringify(body);
|
|
142
|
+
headers['content-type'] = 'application/json';
|
|
143
|
+
}
|
|
144
|
+
const init = {
|
|
145
|
+
method,
|
|
146
|
+
headers,
|
|
147
|
+
body: payload,
|
|
148
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
149
|
+
};
|
|
150
|
+
const dispatcher = await this.tlsDispatcher();
|
|
151
|
+
if (dispatcher)
|
|
152
|
+
init.dispatcher = dispatcher;
|
|
153
|
+
return fetch(url, init);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Build the undici dispatcher carrying the cluster CA / insecure-skip flag, when
|
|
157
|
+
* configured. A kube-apiserver usually presents a private CA, which `fetch` can't
|
|
158
|
+
* verify without this. Loaded lazily (Node only) so the Worker bundle never pulls
|
|
159
|
+
* in `undici`; on a runtime without it, a custom-CA/insecure config fails clearly.
|
|
160
|
+
*
|
|
161
|
+
* The Agent is cached at MODULE scope keyed by the CA/insecure pair, not per
|
|
162
|
+
* instance: the wiring builds a fresh transport on every dispatch/poll resolve, so
|
|
163
|
+
* a per-instance cache would create (and abandon) one Agent — a TLS connection pool
|
|
164
|
+
* — per poll tick, defeating keep-alive and leaking sockets.
|
|
165
|
+
*/
|
|
166
|
+
async tlsDispatcher() {
|
|
167
|
+
if (!this.config.caCertPem && !this.config.insecureSkipTlsVerify)
|
|
168
|
+
return undefined;
|
|
169
|
+
const key = `${this.config.insecureSkipTlsVerify ? 'insecure' : 'verify'}:${this.config.caCertPem ?? ''}`;
|
|
170
|
+
const existing = tlsDispatcherCache.get(key);
|
|
171
|
+
if (existing)
|
|
172
|
+
return existing;
|
|
173
|
+
// Variable specifier so bundlers don't statically resolve `undici`.
|
|
174
|
+
const moduleName = 'undici';
|
|
175
|
+
const undici = (await import(moduleName).catch(() => null));
|
|
176
|
+
if (!undici) {
|
|
177
|
+
throw new Error('Kubernetes custom CA / insecure TLS requires the Node runtime (undici is unavailable).');
|
|
178
|
+
}
|
|
179
|
+
const agent = new undici.Agent({
|
|
180
|
+
connect: {
|
|
181
|
+
ca: this.config.caCertPem,
|
|
182
|
+
rejectUnauthorized: !this.config.insecureSkipTlsVerify,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
tlsDispatcherCache.set(key, agent);
|
|
186
|
+
return agent;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Module-scoped undici Agent cache, keyed by the CA/insecure pair (see tlsDispatcher). */
|
|
190
|
+
const tlsDispatcherCache = new Map();
|
|
191
|
+
async function safeText(res) {
|
|
192
|
+
try {
|
|
193
|
+
return (await res.text()).slice(0, 300);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return '(no body)';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function sleep(ms) {
|
|
200
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=KubernetesRunnerTransport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KubernetesRunnerTransport.js","sourceRoot":"","sources":["../../../src/modules/kubernetes/KubernetesRunnerTransport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { KubernetesRunnerConfig, RunnerDispatchOptions } from '@cat-factory/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* The secret-bundle key the Kubernetes backend reads the ServiceAccount token from.
|
|
4
|
+
* Re-exported from the wire contract (the single source of truth shared with the SPA
|
|
5
|
+
* connect form) so the key is defined once.
|
|
6
|
+
*/
|
|
7
|
+
export declare const KUBERNETES_TOKEN_KEY = "apiToken";
|
|
8
|
+
/** Default port the executor-harness HTTP server listens on inside the pod. */
|
|
9
|
+
export declare const DEFAULT_HARNESS_PORT = 8080;
|
|
10
|
+
/** Deterministic per-RUN pod name (one pod per run; steps re-attach to it). */
|
|
11
|
+
export declare function podName(runId: string): string;
|
|
12
|
+
/** kube-apiserver root with any trailing slash stripped. */
|
|
13
|
+
export declare function apiBase(config: KubernetesRunnerConfig): string;
|
|
14
|
+
/** Collection URL for pods in the configured namespace. */
|
|
15
|
+
export declare function podsUrl(config: KubernetesRunnerConfig): string;
|
|
16
|
+
/** A single pod's URL. */
|
|
17
|
+
export declare function podUrl(config: KubernetesRunnerConfig, name: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* The apiserver POD-PROXY subresource URL for the pod's harness HTTP server:
|
|
20
|
+
* `…/pods/<name>:<port>/proxy<path>`. Reaching this requires only HTTPS to the
|
|
21
|
+
* apiserver (RBAC `pods/proxy`), so no in-cluster networking / per-run Service is
|
|
22
|
+
* needed. `path` must begin with `/`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function proxyUrl(config: KubernetesRunnerConfig, name: string, path: string): string;
|
|
25
|
+
/** Resolve the image variant a dispatch needs (the heavier UI image when asked + configured). */
|
|
26
|
+
export declare function resolveImage(config: KubernetesRunnerConfig, options?: RunnerDispatchOptions): string;
|
|
27
|
+
/** Resolve the pod resource block for a dispatch (per-size override, else the default). */
|
|
28
|
+
export declare function resolveResources(config: KubernetesRunnerConfig, options?: RunnerDispatchOptions): {
|
|
29
|
+
requests?: Record<string, string>;
|
|
30
|
+
limits?: Record<string, string>;
|
|
31
|
+
} | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Build the bare-Pod manifest for a run. A bare Pod (not a Job) because the harness
|
|
34
|
+
* is a long-lived HTTP server we own the lifecycle of (create on first dispatch,
|
|
35
|
+
* delete on release) — Job completion semantics would fight that. The pod is
|
|
36
|
+
* reachable ONLY through the apiserver pod-proxy (no Service), so the harness needs
|
|
37
|
+
* no inbound shared secret here: access is gated by the SA's `pods/proxy` RBAC.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildPodManifest(config: KubernetesRunnerConfig, runId: string, name: string, options?: RunnerDispatchOptions): Record<string, unknown>;
|
|
40
|
+
/** The readiness verdict from a pod's `status`. */
|
|
41
|
+
export type PodReadiness = 'ready' | 'pending' | 'gone';
|
|
42
|
+
/** Classify a pod's status JSON: ready to serve, still pending, or terminally gone. */
|
|
43
|
+
export declare function classifyPodReadiness(pod: unknown): PodReadiness;
|
|
44
|
+
/**
|
|
45
|
+
* Validate the apiserver URL at the write boundary. Unlike the manifest pool's
|
|
46
|
+
* STRICT policy (no private hosts), a kube-apiserver is routinely a private IP or
|
|
47
|
+
* a cluster DNS name, so private hosts are ALLOWED here — the operator is
|
|
48
|
+
* explicitly pointing at their cluster. We still require https and reject the
|
|
49
|
+
* cloud-metadata endpoints (anti-SSRF), including their obfuscated IP encodings
|
|
50
|
+
* (bare integer, IPv4-mapped IPv6) and the full link-local range — see the shared
|
|
51
|
+
* {@link isCloudMetadataHost} classifier.
|
|
52
|
+
*/
|
|
53
|
+
export declare function assertApiServerUrlSafe(rawUrl: string): void;
|
|
54
|
+
//# sourceMappingURL=kubernetes.logic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kubernetes.logic.d.ts","sourceRoot":"","sources":["../../../src/modules/kubernetes/kubernetes.logic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AASxF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,aAAqC,CAAA;AAEtE,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,OAAO,CAAA;AAExC,+EAA+E;AAC/E,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS7C;AAED,4DAA4D;AAC5D,wBAAgB,OAAO,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAE9D;AAED,2DAA2D;AAC3D,wBAAgB,OAAO,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAE9D;AAED,0BAA0B;AAC1B,wBAAgB,MAAM,CAAC,MAAM,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQ3F;AAED,iGAAiG;AACjG,wBAAgB,YAAY,CAC1B,MAAM,EAAE,sBAAsB,EAC9B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,MAAM,CAGR;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,sBAAsB,EAC9B,OAAO,CAAC,EAAE,qBAAqB,GAC9B;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,SAAS,CAcpF;AASD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,sBAAsB,EAC9B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,qBAAqB,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA+BzB;AAUD,mDAAmD;AACnD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;AAEvD,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAU/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAa3D"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { isCloudMetadataHost } from '@cat-factory/kernel';
|
|
2
|
+
import { KUBERNETES_RUNNER_TOKEN_SECRET_KEY } from '@cat-factory/contracts';
|
|
3
|
+
// Pure helpers for the native Kubernetes runner backend. No I/O here — URL
|
|
4
|
+
// building, the per-run pod-name derivation, the pod manifest, and the readiness
|
|
5
|
+
// classification are all pure so they unit-test in isolation. The transport
|
|
6
|
+
// (KubernetesRunnerTransport) does the actual kube-apiserver `fetch`es.
|
|
7
|
+
/**
|
|
8
|
+
* The secret-bundle key the Kubernetes backend reads the ServiceAccount token from.
|
|
9
|
+
* Re-exported from the wire contract (the single source of truth shared with the SPA
|
|
10
|
+
* connect form) so the key is defined once.
|
|
11
|
+
*/
|
|
12
|
+
export const KUBERNETES_TOKEN_KEY = KUBERNETES_RUNNER_TOKEN_SECRET_KEY;
|
|
13
|
+
/** Default port the executor-harness HTTP server listens on inside the pod. */
|
|
14
|
+
export const DEFAULT_HARNESS_PORT = 8080;
|
|
15
|
+
/** Deterministic per-RUN pod name (one pod per run; steps re-attach to it). */
|
|
16
|
+
export function podName(runId) {
|
|
17
|
+
const sanitized = runId
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
20
|
+
.replace(/-+/g, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '');
|
|
22
|
+
// RFC1123 label: <=63 chars, starts/ends alphanumeric. Reserve room for the prefix.
|
|
23
|
+
const body = sanitized.slice(0, 63 - 'cf-run-'.length).replace(/-+$/g, '') || 'run';
|
|
24
|
+
return `cf-run-${body}`;
|
|
25
|
+
}
|
|
26
|
+
/** kube-apiserver root with any trailing slash stripped. */
|
|
27
|
+
export function apiBase(config) {
|
|
28
|
+
return config.apiServerUrl.trim().replace(/\/+$/, '');
|
|
29
|
+
}
|
|
30
|
+
/** Collection URL for pods in the configured namespace. */
|
|
31
|
+
export function podsUrl(config) {
|
|
32
|
+
return `${apiBase(config)}/api/v1/namespaces/${config.namespace}/pods`;
|
|
33
|
+
}
|
|
34
|
+
/** A single pod's URL. */
|
|
35
|
+
export function podUrl(config, name) {
|
|
36
|
+
return `${podsUrl(config)}/${encodeURIComponent(name)}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The apiserver POD-PROXY subresource URL for the pod's harness HTTP server:
|
|
40
|
+
* `…/pods/<name>:<port>/proxy<path>`. Reaching this requires only HTTPS to the
|
|
41
|
+
* apiserver (RBAC `pods/proxy`), so no in-cluster networking / per-run Service is
|
|
42
|
+
* needed. `path` must begin with `/`.
|
|
43
|
+
*/
|
|
44
|
+
export function proxyUrl(config, name, path) {
|
|
45
|
+
const port = config.harnessPort ?? DEFAULT_HARNESS_PORT;
|
|
46
|
+
const p = path.startsWith('/') ? path : `/${path}`;
|
|
47
|
+
// The apiserver pod-proxy subresource addresses the target as a literal
|
|
48
|
+
// `pods/<name>:<port>/proxy` path segment — kubectl/client-go send the colon
|
|
49
|
+
// UNENCODED. Encode the name (RFC1123, so a no-op in practice) but keep the
|
|
50
|
+
// `:<port>` literal so the apiserver parses the name:port pair.
|
|
51
|
+
return `${podsUrl(config)}/${encodeURIComponent(name)}:${port}/proxy${p}`;
|
|
52
|
+
}
|
|
53
|
+
/** Resolve the image variant a dispatch needs (the heavier UI image when asked + configured). */
|
|
54
|
+
export function resolveImage(config, options) {
|
|
55
|
+
if (options?.image === 'ui' && config.imageUi)
|
|
56
|
+
return config.imageUi;
|
|
57
|
+
return config.image;
|
|
58
|
+
}
|
|
59
|
+
/** Resolve the pod resource block for a dispatch (per-size override, else the default). */
|
|
60
|
+
export function resolveResources(config, options) {
|
|
61
|
+
const sizeOverride = options?.instanceSize
|
|
62
|
+
? config.resourcesBySize?.[options.instanceSize]
|
|
63
|
+
: undefined;
|
|
64
|
+
// A per-size override is the t-shirt size for this run: it sets BOTH the request and
|
|
65
|
+
// the limit (requests == limits ⇒ Guaranteed QoS). Applying it to the limit alone
|
|
66
|
+
// while keeping a larger default request produces requests > limits, which the
|
|
67
|
+
// apiserver rejects with a 422 — so a smaller size could never start.
|
|
68
|
+
const requests = sizeOverride ?? config.resources?.requests;
|
|
69
|
+
const limits = sizeOverride ?? config.resources?.limits;
|
|
70
|
+
const out = {};
|
|
71
|
+
if (requests)
|
|
72
|
+
out.requests = quantities(requests);
|
|
73
|
+
if (limits)
|
|
74
|
+
out.limits = quantities(limits);
|
|
75
|
+
return out.requests || out.limits ? out : undefined;
|
|
76
|
+
}
|
|
77
|
+
function quantities(q) {
|
|
78
|
+
const out = {};
|
|
79
|
+
if (q.cpu)
|
|
80
|
+
out.cpu = q.cpu;
|
|
81
|
+
if (q.memory)
|
|
82
|
+
out.memory = q.memory;
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build the bare-Pod manifest for a run. A bare Pod (not a Job) because the harness
|
|
87
|
+
* is a long-lived HTTP server we own the lifecycle of (create on first dispatch,
|
|
88
|
+
* delete on release) — Job completion semantics would fight that. The pod is
|
|
89
|
+
* reachable ONLY through the apiserver pod-proxy (no Service), so the harness needs
|
|
90
|
+
* no inbound shared secret here: access is gated by the SA's `pods/proxy` RBAC.
|
|
91
|
+
*/
|
|
92
|
+
export function buildPodManifest(config, runId, name, options) {
|
|
93
|
+
const port = config.harnessPort ?? DEFAULT_HARNESS_PORT;
|
|
94
|
+
const resources = resolveResources(config, options);
|
|
95
|
+
const container = {
|
|
96
|
+
name: 'executor',
|
|
97
|
+
image: resolveImage(config, options),
|
|
98
|
+
ports: [{ containerPort: port }],
|
|
99
|
+
env: [{ name: 'PORT', value: String(port) }],
|
|
100
|
+
...(resources ? { resources } : {}),
|
|
101
|
+
};
|
|
102
|
+
const spec = {
|
|
103
|
+
restartPolicy: 'Never',
|
|
104
|
+
containers: [container],
|
|
105
|
+
...(config.serviceAccountName ? { serviceAccountName: config.serviceAccountName } : {}),
|
|
106
|
+
...(config.imagePullSecretName
|
|
107
|
+
? { imagePullSecrets: [{ name: config.imagePullSecretName }] }
|
|
108
|
+
: {}),
|
|
109
|
+
...(config.nodeSelector ? { nodeSelector: config.nodeSelector } : {}),
|
|
110
|
+
...(config.tolerations ? { tolerations: config.tolerations } : {}),
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
apiVersion: 'v1',
|
|
114
|
+
kind: 'Pod',
|
|
115
|
+
metadata: {
|
|
116
|
+
name,
|
|
117
|
+
namespace: config.namespace,
|
|
118
|
+
labels: { 'cat-factory.runId': labelValue(runId), ...config.labels },
|
|
119
|
+
...(config.annotations ? { annotations: config.annotations } : {}),
|
|
120
|
+
},
|
|
121
|
+
spec,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Coerce an arbitrary id into a valid label value (<=63 chars, alnum/._-). */
|
|
125
|
+
function labelValue(value) {
|
|
126
|
+
return value
|
|
127
|
+
.replace(/[^A-Za-z0-9._-]/g, '-')
|
|
128
|
+
.replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, '')
|
|
129
|
+
.slice(0, 63);
|
|
130
|
+
}
|
|
131
|
+
/** Classify a pod's status JSON: ready to serve, still pending, or terminally gone. */
|
|
132
|
+
export function classifyPodReadiness(pod) {
|
|
133
|
+
const status = pod?.status;
|
|
134
|
+
const phase = typeof status?.phase === 'string' ? status.phase : undefined;
|
|
135
|
+
if (phase === 'Succeeded' || phase === 'Failed')
|
|
136
|
+
return 'gone';
|
|
137
|
+
if (phase !== 'Running')
|
|
138
|
+
return 'pending';
|
|
139
|
+
const conditions = Array.isArray(status?.conditions)
|
|
140
|
+
? status.conditions
|
|
141
|
+
: [];
|
|
142
|
+
const ready = conditions.find((c) => c.type === 'Ready');
|
|
143
|
+
return ready?.status === 'True' ? 'ready' : 'pending';
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Validate the apiserver URL at the write boundary. Unlike the manifest pool's
|
|
147
|
+
* STRICT policy (no private hosts), a kube-apiserver is routinely a private IP or
|
|
148
|
+
* a cluster DNS name, so private hosts are ALLOWED here — the operator is
|
|
149
|
+
* explicitly pointing at their cluster. We still require https and reject the
|
|
150
|
+
* cloud-metadata endpoints (anti-SSRF), including their obfuscated IP encodings
|
|
151
|
+
* (bare integer, IPv4-mapped IPv6) and the full link-local range — see the shared
|
|
152
|
+
* {@link isCloudMetadataHost} classifier.
|
|
153
|
+
*/
|
|
154
|
+
export function assertApiServerUrlSafe(rawUrl) {
|
|
155
|
+
let url;
|
|
156
|
+
try {
|
|
157
|
+
url = new URL(rawUrl);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
throw new Error(`Invalid Kubernetes apiserver URL: ${rawUrl}`);
|
|
161
|
+
}
|
|
162
|
+
if (url.protocol !== 'https:') {
|
|
163
|
+
throw new Error('Kubernetes apiserver URL must use https.');
|
|
164
|
+
}
|
|
165
|
+
if (isCloudMetadataHost(url.hostname)) {
|
|
166
|
+
throw new Error('Kubernetes apiserver URL must not target the cloud metadata endpoint.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=kubernetes.logic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kubernetes.logic.js","sourceRoot":"","sources":["../../../src/modules/kubernetes/kubernetes.logic.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,57 +1,69 @@
|
|
|
1
1
|
import type { Clock } from '@cat-factory/kernel';
|
|
2
2
|
import type { RunnerPoolConnectionRecord, RunnerPoolConnectionRepository } from '@cat-factory/kernel';
|
|
3
3
|
import type { SecretCipher } from '@cat-factory/kernel';
|
|
4
|
-
import type {
|
|
5
|
-
import type { ConnectionTestResult, ProviderDescriptor,
|
|
4
|
+
import type { UrlSafetyPolicy } from '@cat-factory/kernel';
|
|
5
|
+
import type { ConnectionTestResult, ProviderDescriptor, RunnerBackendConfig, RunnerPoolConnection, RunnerPoolProvider, RunnerTransport, TestRunnerPoolConnectionInput } from '@cat-factory/kernel';
|
|
6
6
|
import type { WorkspaceRepository } from '@cat-factory/kernel';
|
|
7
7
|
export interface RunnerPoolConnectionServiceDependencies {
|
|
8
8
|
runnerPoolConnectionRepository: RunnerPoolConnectionRepository;
|
|
9
9
|
workspaceRepository: WorkspaceRepository;
|
|
10
10
|
secretCipher: SecretCipher;
|
|
11
11
|
clock: Clock;
|
|
12
|
-
/** URL/host safety policy applied to a
|
|
12
|
+
/** URL/host safety policy applied to a manifest backend. Defaults to strict. */
|
|
13
13
|
urlPolicy?: UrlSafetyPolicy;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Whether this deployment runtime can honor a backend's custom TLS trust material
|
|
16
|
+
* (a private CA / insecure-skip). The Cloudflare Worker cannot, so it sets `false`
|
|
17
|
+
* and a Kubernetes config with a CA is rejected at registration. Absent ⇒ supported.
|
|
18
|
+
*/
|
|
19
|
+
customTlsSupported?: boolean;
|
|
20
|
+
/** Injected manifest HTTP provider (its OAuth cache shared / a native pool adapter). */
|
|
15
21
|
runnerPoolProvider?: RunnerPoolProvider;
|
|
16
|
-
/** What the injected provider is (see EnvironmentConnectionService). Defaults `manifest`. */
|
|
17
|
-
providerKind?: 'native' | 'manifest';
|
|
18
|
-
providerId?: string;
|
|
19
|
-
providerLabel?: string;
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
/** A resolved runner backend: the live transport + its identity (for provisioning logs). */
|
|
24
|
+
export interface ResolvedRunnerBackend {
|
|
25
|
+
transport: RunnerTransport;
|
|
26
|
+
kind: string;
|
|
27
|
+
providerId: string;
|
|
24
28
|
}
|
|
25
29
|
export declare class RunnerPoolConnectionService {
|
|
26
30
|
private readonly deps;
|
|
27
31
|
constructor(deps: RunnerPoolConnectionServiceDependencies);
|
|
28
|
-
/**
|
|
32
|
+
/** The per-call context a backend provider needs to build/test a transport. */
|
|
33
|
+
private context;
|
|
34
|
+
private provider;
|
|
35
|
+
/** The write-boundary safety options (URL policy + this runtime's TLS capability). */
|
|
36
|
+
private safetyOptions;
|
|
37
|
+
/** Register (or replace) a workspace's runner backend. */
|
|
29
38
|
register(workspaceId: string, input: {
|
|
30
|
-
|
|
39
|
+
config: RunnerBackendConfig;
|
|
31
40
|
secrets: Record<string, string>;
|
|
32
41
|
}): Promise<RunnerPoolConnection>;
|
|
33
|
-
/** Rotate/replace the secret bundle without re-sending the
|
|
42
|
+
/** Rotate/replace the secret bundle without re-sending the config. */
|
|
34
43
|
updateSecrets(workspaceId: string, secrets: Record<string, string>): Promise<RunnerPoolConnection>;
|
|
35
|
-
/** Describe the
|
|
44
|
+
/** Describe the backend's config fields + test availability for the UI. */
|
|
36
45
|
describeProvider(workspaceId: string): Promise<ProviderDescriptor>;
|
|
37
|
-
/** Probe a candidate
|
|
46
|
+
/** Probe a candidate backend connection before saving (nothing is persisted). */
|
|
38
47
|
testConnection(workspaceId: string, input: TestRunnerPoolConnectionInput): Promise<ConnectionTestResult>;
|
|
39
48
|
/** The workspace's current connection (safe metadata), or null. */
|
|
40
49
|
getConnection(workspaceId: string): Promise<RunnerPoolConnection | null>;
|
|
41
|
-
/** Resolve the live connection + parsed
|
|
50
|
+
/** Resolve the live connection + parsed config, or throw if not registered. */
|
|
42
51
|
requireConnection(workspaceId: string): Promise<{
|
|
43
52
|
record: RunnerPoolConnectionRecord;
|
|
44
|
-
|
|
53
|
+
config: RunnerBackendConfig;
|
|
45
54
|
}>;
|
|
46
55
|
/**
|
|
47
|
-
* Resolve the workspace's
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* Resolve the workspace's runner backend into a live {@link RunnerTransport} (the
|
|
57
|
+
* provider builds it from the stored config + a secret resolver over its decrypted
|
|
58
|
+
* bundle), or null when it has no live backend registered / its kind is no longer
|
|
59
|
+
* registered. Used by the wiring to pick the dispatch backend per job.
|
|
50
60
|
*/
|
|
51
|
-
resolve(workspaceId: string): Promise<
|
|
52
|
-
/** Unregister the
|
|
61
|
+
resolve(workspaceId: string): Promise<ResolvedRunnerBackend | null>;
|
|
62
|
+
/** Unregister the backend (tombstones the binding). */
|
|
53
63
|
unregister(workspaceId: string): Promise<void>;
|
|
54
64
|
private decryptSecrets;
|
|
55
65
|
private toConnection;
|
|
66
|
+
/** Parse the stored discriminated config, tolerating a malformed/legacy blob. */
|
|
67
|
+
private parseConfig;
|
|
56
68
|
}
|
|
57
69
|
//# sourceMappingURL=RunnerPoolConnectionService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RunnerPoolConnectionService.d.ts","sourceRoot":"","sources":["../../../src/modules/runners/RunnerPoolConnectionService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EACV,0BAA0B,EAC1B,8BAA8B,EAC/B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"RunnerPoolConnectionService.d.ts","sourceRoot":"","sources":["../../../src/modules/runners/RunnerPoolConnectionService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EACV,0BAA0B,EAC1B,8BAA8B,EAC/B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,6BAA6B,EAC9B,MAAM,qBAAqB,CAAA;AAG5B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAiB9D,MAAM,WAAW,uCAAuC;IACtD,8BAA8B,EAAE,8BAA8B,CAAA;IAC9D,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ,gFAAgF;IAChF,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,kBAAkB,CAAA;CACxC;AAED,4FAA4F;AAC5F,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,eAAe,CAAA;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,2BAA2B;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,uCAAuC,EAAI;IAE9E,+EAA+E;IAC/E,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,QAAQ;IAMhB,sFAAsF;IACtF,OAAO,CAAC,aAAa;IASrB,0DAA0D;IACpD,QAAQ,CACZ,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE;QAAE,MAAM,EAAE,mBAAmB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GACtE,OAAO,CAAC,oBAAoB,CAAC,CA2B/B;IAED,sEAAsE;IAChE,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CAW/B;IAED,2EAA2E;IACrE,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA6BvE;IAED,iFAAiF;IAC3E,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAU/B;IAED,mEAAmE;IAC7D,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAK7E;IAED,+EAA+E;IACzE,iBAAiB,CACrB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,MAAM,EAAE,0BAA0B,CAAC;QAAC,MAAM,EAAE,mBAAmB,CAAA;KAAE,CAAC,CAO9E;IAED;;;;;OAKG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAYxE;IAED,uDAAuD;IACjD,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAInD;YAEa,cAAc;IAQ5B,OAAO,CAAC,YAAY;IAmBpB,iFAAiF;IACjF,OAAO,CAAC,WAAW;CAOpB"}
|