@cat-factory/integrations 0.31.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/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,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"}
|
|
@@ -1,29 +1,55 @@
|
|
|
1
|
-
import { ConflictError,
|
|
1
|
+
import { ConflictError, ValidationError } from '@cat-factory/kernel';
|
|
2
2
|
import { requireWorkspace } from '@cat-factory/kernel';
|
|
3
3
|
import { missingRequiredConfigKeys } from '../environments/environments.logic.js';
|
|
4
|
-
import {
|
|
4
|
+
import { runnerBackend } from './runner-backends.js';
|
|
5
5
|
export class RunnerPoolConnectionService {
|
|
6
6
|
deps;
|
|
7
7
|
constructor(deps) {
|
|
8
8
|
this.deps = deps;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/** The per-call context a backend provider needs to build/test a transport. */
|
|
11
|
+
context(resolveSecret) {
|
|
12
|
+
return {
|
|
13
|
+
resolveSecret,
|
|
14
|
+
...(this.deps.urlPolicy ? { urlPolicy: this.deps.urlPolicy } : {}),
|
|
15
|
+
...(this.deps.runnerPoolProvider ? { runnerPoolProvider: this.deps.runnerPoolProvider } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
provider(kind) {
|
|
19
|
+
const provider = runnerBackend(kind);
|
|
20
|
+
if (!provider)
|
|
21
|
+
throw new ValidationError(`Unknown runner backend kind: '${kind}'`);
|
|
22
|
+
return provider;
|
|
23
|
+
}
|
|
24
|
+
/** The write-boundary safety options (URL policy + this runtime's TLS capability). */
|
|
25
|
+
safetyOptions() {
|
|
26
|
+
return {
|
|
27
|
+
...(this.deps.urlPolicy ? { urlPolicy: this.deps.urlPolicy } : {}),
|
|
28
|
+
...(this.deps.customTlsSupported !== undefined
|
|
29
|
+
? { customTlsSupported: this.deps.customTlsSupported }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Register (or replace) a workspace's runner backend. */
|
|
11
34
|
async register(workspaceId, input) {
|
|
12
35
|
await requireWorkspace(this.deps.workspaceRepository, workspaceId);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
const config = input.config;
|
|
37
|
+
const provider = this.provider(config.kind);
|
|
38
|
+
provider.assertConfigSafe(config, this.safetyOptions());
|
|
39
|
+
const missing = provider.referencedSecretKeys(config).filter((key) => !(key in input.secrets));
|
|
16
40
|
if (missing.length) {
|
|
17
41
|
throw new ValidationError(`Missing secret values for: ${missing.join(', ')}`);
|
|
18
42
|
}
|
|
19
43
|
const existing = await this.deps.runnerPoolConnectionRepository.getByWorkspace(workspaceId);
|
|
44
|
+
const meta = provider.connectionMeta(config);
|
|
20
45
|
const secretsCipher = await this.deps.secretCipher.encrypt(JSON.stringify(input.secrets));
|
|
21
46
|
const record = {
|
|
22
47
|
workspaceId,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
48
|
+
kind: config.kind,
|
|
49
|
+
providerId: meta.providerId,
|
|
50
|
+
label: meta.label,
|
|
51
|
+
baseUrl: meta.baseUrl,
|
|
52
|
+
configJson: JSON.stringify(config),
|
|
27
53
|
secretsCipher,
|
|
28
54
|
createdAt: existing?.createdAt ?? this.deps.clock.now(),
|
|
29
55
|
deletedAt: null,
|
|
@@ -31,10 +57,11 @@ export class RunnerPoolConnectionService {
|
|
|
31
57
|
await this.deps.runnerPoolConnectionRepository.upsert(record);
|
|
32
58
|
return this.toConnection(record, Object.keys(input.secrets));
|
|
33
59
|
}
|
|
34
|
-
/** Rotate/replace the secret bundle without re-sending the
|
|
60
|
+
/** Rotate/replace the secret bundle without re-sending the config. */
|
|
35
61
|
async updateSecrets(workspaceId, secrets) {
|
|
36
|
-
const { record,
|
|
37
|
-
const
|
|
62
|
+
const { record, config } = await this.requireConnection(workspaceId);
|
|
63
|
+
const provider = this.provider(record.kind);
|
|
64
|
+
const missing = provider.referencedSecretKeys(config).filter((key) => !(key in secrets));
|
|
38
65
|
if (missing.length) {
|
|
39
66
|
throw new ValidationError(`Missing secret values for: ${missing.join(', ')}`);
|
|
40
67
|
}
|
|
@@ -43,50 +70,47 @@ export class RunnerPoolConnectionService {
|
|
|
43
70
|
await this.deps.runnerPoolConnectionRepository.upsert(updated);
|
|
44
71
|
return this.toConnection(updated, Object.keys(secrets));
|
|
45
72
|
}
|
|
46
|
-
/** Describe the
|
|
73
|
+
/** Describe the backend's config fields + test availability for the UI. */
|
|
47
74
|
async describeProvider(workspaceId) {
|
|
48
75
|
const record = await this.deps.runnerPoolConnectionRepository.getByWorkspace(workspaceId);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
const config = record ? JSON.parse(record.configJson) : undefined;
|
|
77
|
+
// The manifest backend renders a flat field form (its secret-ref keys + baseUrl);
|
|
78
|
+
// the native (kubernetes) backend uses an explicit UI form, so it has no flat fields.
|
|
79
|
+
const manifest = config?.kind === 'manifest' ? config.manifest : undefined;
|
|
80
|
+
const configFields = manifest
|
|
81
|
+
? (this.deps.runnerPoolProvider?.describeConfig?.(manifest) ?? [])
|
|
82
|
+
: [];
|
|
55
83
|
const storedKeys = record ? Object.keys(await this.decryptSecrets(record)) : [];
|
|
56
84
|
if (manifest?.baseUrl)
|
|
57
85
|
storedKeys.push('baseUrl');
|
|
58
86
|
const provider = this.deps.runnerPoolProvider;
|
|
59
87
|
return {
|
|
60
|
-
providerId:
|
|
61
|
-
label:
|
|
62
|
-
kind
|
|
88
|
+
providerId: record?.providerId ?? 'http',
|
|
89
|
+
label: record?.label ?? 'Agent runner backend',
|
|
90
|
+
// `kind` here is the UI FORM-STYLE discriminator (manifest editor vs native flat
|
|
91
|
+
// form), not the runner-backend kind: only the manifest backend uses this
|
|
92
|
+
// descriptor-driven form, so it stays 'manifest'. The actual backend kind is
|
|
93
|
+
// surfaced on the connection (`connection.kind` + the non-secret `config`), which
|
|
94
|
+
// is what the tab's backend selector + the Kubernetes form read.
|
|
95
|
+
kind: 'manifest',
|
|
63
96
|
configFields,
|
|
64
|
-
supportsTest:
|
|
97
|
+
supportsTest: true,
|
|
65
98
|
missingRequired: missingRequiredConfigKeys(configFields, storedKeys),
|
|
66
|
-
// The current saved manifest (non-secret), so the native connect form overlays edits
|
|
67
|
-
// onto the real stored manifest instead of the bare scaffold (mirrors the env service).
|
|
68
99
|
...(manifest ? { savedManifest: manifest } : {}),
|
|
69
|
-
...(provider?.describeManifestTemplate
|
|
100
|
+
...(manifest && provider?.describeManifestTemplate
|
|
70
101
|
? { manifestTemplate: provider.describeManifestTemplate() }
|
|
71
102
|
: {}),
|
|
72
103
|
};
|
|
73
104
|
}
|
|
74
|
-
/** Probe a candidate
|
|
105
|
+
/** Probe a candidate backend connection before saving (nothing is persisted). */
|
|
75
106
|
async testConnection(workspaceId, input) {
|
|
76
107
|
await requireWorkspace(this.deps.workspaceRepository, workspaceId);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (input.manifest) {
|
|
82
|
-
assertManifestUrlsSafe(input.manifest, this.deps.urlPolicy ?? STRICT_URL_SAFETY_POLICY);
|
|
83
|
-
}
|
|
108
|
+
if (!input.config)
|
|
109
|
+
return { ok: true, message: 'Nothing to test.' };
|
|
110
|
+
const provider = this.provider(input.config.kind);
|
|
111
|
+
provider.assertConfigSafe(input.config, this.safetyOptions());
|
|
84
112
|
const secrets = input.secrets ?? {};
|
|
85
|
-
return provider.testConnection(
|
|
86
|
-
manifest: input.manifest,
|
|
87
|
-
config: input.config ?? {},
|
|
88
|
-
resolveSecret: (key) => secrets[key],
|
|
89
|
-
});
|
|
113
|
+
return provider.testConnection(input.config, this.context((key) => secrets[key]));
|
|
90
114
|
}
|
|
91
115
|
/** The workspace's current connection (safe metadata), or null. */
|
|
92
116
|
async getConnection(workspaceId) {
|
|
@@ -96,29 +120,34 @@ export class RunnerPoolConnectionService {
|
|
|
96
120
|
const keys = Object.keys(await this.decryptSecrets(record));
|
|
97
121
|
return this.toConnection(record, keys);
|
|
98
122
|
}
|
|
99
|
-
/** Resolve the live connection + parsed
|
|
123
|
+
/** Resolve the live connection + parsed config, or throw if not registered. */
|
|
100
124
|
async requireConnection(workspaceId) {
|
|
101
125
|
const record = await this.deps.runnerPoolConnectionRepository.getByWorkspace(workspaceId);
|
|
102
126
|
if (!record) {
|
|
103
|
-
throw new ConflictError(`Workspace '${workspaceId}' has no runner
|
|
127
|
+
throw new ConflictError(`Workspace '${workspaceId}' has no runner backend registered`);
|
|
104
128
|
}
|
|
105
|
-
const
|
|
106
|
-
return { record,
|
|
129
|
+
const config = JSON.parse(record.configJson);
|
|
130
|
+
return { record, config };
|
|
107
131
|
}
|
|
108
132
|
/**
|
|
109
|
-
* Resolve the workspace's
|
|
110
|
-
*
|
|
111
|
-
*
|
|
133
|
+
* Resolve the workspace's runner backend into a live {@link RunnerTransport} (the
|
|
134
|
+
* provider builds it from the stored config + a secret resolver over its decrypted
|
|
135
|
+
* bundle), or null when it has no live backend registered / its kind is no longer
|
|
136
|
+
* registered. Used by the wiring to pick the dispatch backend per job.
|
|
112
137
|
*/
|
|
113
138
|
async resolve(workspaceId) {
|
|
114
139
|
const record = await this.deps.runnerPoolConnectionRepository.getByWorkspace(workspaceId);
|
|
115
140
|
if (!record)
|
|
116
141
|
return null;
|
|
117
|
-
const
|
|
142
|
+
const provider = runnerBackend(record.kind);
|
|
143
|
+
if (!provider)
|
|
144
|
+
return null;
|
|
145
|
+
const config = JSON.parse(record.configJson);
|
|
118
146
|
const bundle = await this.decryptSecrets(record);
|
|
119
|
-
|
|
147
|
+
const transport = provider.buildTransport(config, this.context((key) => bundle[key]));
|
|
148
|
+
return { transport, kind: record.kind, providerId: record.providerId };
|
|
120
149
|
}
|
|
121
|
-
/** Unregister the
|
|
150
|
+
/** Unregister the backend (tombstones the binding). */
|
|
122
151
|
async unregister(workspaceId) {
|
|
123
152
|
const record = await this.deps.runnerPoolConnectionRepository.getByWorkspace(workspaceId);
|
|
124
153
|
if (!record)
|
|
@@ -132,13 +161,28 @@ export class RunnerPoolConnectionService {
|
|
|
132
161
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
133
162
|
}
|
|
134
163
|
toConnection(record, secretKeys) {
|
|
164
|
+
// The stored config holds NO secrets (those live in the separate encrypted bundle),
|
|
165
|
+
// so it is safe to expose so the connect form can prefill the non-secret fields on
|
|
166
|
+
// edit (namespace/image/… for kubernetes) instead of forcing a full re-entry.
|
|
167
|
+
const config = this.parseConfig(record);
|
|
135
168
|
return {
|
|
169
|
+
kind: record.kind,
|
|
136
170
|
providerId: record.providerId,
|
|
137
171
|
label: record.label,
|
|
138
172
|
baseUrl: record.baseUrl,
|
|
139
173
|
connectedAt: record.createdAt,
|
|
140
174
|
secretKeys,
|
|
175
|
+
...(config ? { config } : {}),
|
|
141
176
|
};
|
|
142
177
|
}
|
|
178
|
+
/** Parse the stored discriminated config, tolerating a malformed/legacy blob. */
|
|
179
|
+
parseConfig(record) {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(record.configJson);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
143
187
|
}
|
|
144
188
|
//# sourceMappingURL=RunnerPoolConnectionService.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RunnerPoolConnectionService.js","sourceRoot":"","sources":["../../../src/modules/runners/RunnerPoolConnectionService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RunnerPoolConnectionService.js","sourceRoot":"","sources":["../../../src/modules/runners/RunnerPoolConnectionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ConnectionTestResult, RunnerBackendConfig, RunnerPoolProvider, RunnerTransport, SecretResolver, UrlSafetyPolicy } from '@cat-factory/kernel';
|
|
2
|
+
/** Per-call dependencies a provider may need to build/test its transport. */
|
|
3
|
+
export interface RunnerBackendContext {
|
|
4
|
+
resolveSecret: SecretResolver;
|
|
5
|
+
/** Manifest SSRF policy (the Kubernetes backend does its own apiserver-URL check). */
|
|
6
|
+
urlPolicy?: UrlSafetyPolicy;
|
|
7
|
+
/**
|
|
8
|
+
* A shared manifest HTTP provider (its OAuth cache reused), injectable for tests
|
|
9
|
+
* and for native pool adapters. Used by the `manifest` backend only.
|
|
10
|
+
*/
|
|
11
|
+
runnerPoolProvider?: RunnerPoolProvider;
|
|
12
|
+
}
|
|
13
|
+
/** Capabilities/policies a backend validates its config against at the write boundary. */
|
|
14
|
+
export interface RunnerBackendSafetyOptions {
|
|
15
|
+
/** Manifest SSRF policy. Absent ⇒ strict. */
|
|
16
|
+
urlPolicy?: UrlSafetyPolicy;
|
|
17
|
+
/**
|
|
18
|
+
* Whether THIS deployment runtime can honor a backend's custom TLS trust material
|
|
19
|
+
* (a private CA / insecure-skip). The Cloudflare Worker cannot (no undici / no
|
|
20
|
+
* custom-CA fetch), so it sets this `false` and the kubernetes backend rejects such
|
|
21
|
+
* a config up front instead of letting it save and then die at first dispatch.
|
|
22
|
+
* Absent/`true` ⇒ supported (Node/local).
|
|
23
|
+
*/
|
|
24
|
+
customTlsSupported?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface RunnerBackendProvider {
|
|
27
|
+
readonly kind: RunnerBackendConfig['kind'];
|
|
28
|
+
/** Every secret-bundle key the config references (validated present at registration). */
|
|
29
|
+
referencedSecretKeys(config: RunnerBackendConfig): string[];
|
|
30
|
+
/** Non-secret metadata persisted on the connection row + shown in the UI. */
|
|
31
|
+
connectionMeta(config: RunnerBackendConfig): {
|
|
32
|
+
providerId: string;
|
|
33
|
+
label: string;
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
};
|
|
36
|
+
/** Validate the config at the write boundary (SSRF / URL + runtime safety). Throws if unsafe. */
|
|
37
|
+
assertConfigSafe(config: RunnerBackendConfig, opts?: RunnerBackendSafetyOptions): void;
|
|
38
|
+
/** Build the live transport the execution engine dispatches/polls/releases through. */
|
|
39
|
+
buildTransport(config: RunnerBackendConfig, ctx: RunnerBackendContext): RunnerTransport;
|
|
40
|
+
/** Probe the backend without persisting anything. */
|
|
41
|
+
testConnection(config: RunnerBackendConfig, ctx: RunnerBackendContext): Promise<ConnectionTestResult>;
|
|
42
|
+
}
|
|
43
|
+
/** Register a runner-backend provider (built-ins on import; third-party for side effect). */
|
|
44
|
+
export declare function registerRunnerBackend(provider: RunnerBackendProvider): void;
|
|
45
|
+
/** The provider for a backend kind, or undefined when unregistered. */
|
|
46
|
+
export declare function runnerBackend(kind: string): RunnerBackendProvider | undefined;
|
|
47
|
+
/** All registered backend kinds (for diagnostics / a UI capabilities list). */
|
|
48
|
+
export declare function registeredRunnerBackendKinds(): string[];
|
|
49
|
+
export declare const manifestRunnerBackend: RunnerBackendProvider;
|
|
50
|
+
export declare const kubernetesRunnerBackend: RunnerBackendProvider;
|
|
51
|
+
//# sourceMappingURL=runner-backends.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner-backends.d.ts","sourceRoot":"","sources":["../../../src/modules/runners/runner-backends.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAqB5B,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,cAAc,CAAA;IAC7B,sFAAsF;IACtF,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAA;CACxC;AAED,0FAA0F;AAC1F,MAAM,WAAW,0BAA0B;IACzC,6CAA6C;IAC7C,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAA;IAC1C,yFAAyF;IACzF,oBAAoB,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAAA;IAC3D,6EAA6E;IAC7E,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG;QAC3C,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,iGAAiG;IACjG,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,EAAE,IAAI,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAA;IACtF,uFAAuF;IACvF,cAAc,CAAC,MAAM,EAAE,mBAAmB,EAAE,GAAG,EAAE,oBAAoB,GAAG,eAAe,CAAA;IACvF,qDAAqD;IACrD,cAAc,CACZ,MAAM,EAAE,mBAAmB,EAC3B,GAAG,EAAE,oBAAoB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAAA;CACjC;AAID,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAE3E;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAE7E;AAED,+EAA+E;AAC/E,wBAAgB,4BAA4B,IAAI,MAAM,EAAE,CAEvD;AASD,eAAO,MAAM,qBAAqB,EAAE,qBAmCnC,CAAA;AAID,eAAO,MAAM,uBAAuB,EAAE,qBAqCrC,CAAA"}
|