hammerkit 1.27.0-next.9 → 2.0.0-next.2
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/build.schema.json +176 -6
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/cache-sync.js +1 -1
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/archive-task-entry.js +4 -2
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +11 -4
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +6 -3
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +194 -23
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +49 -1
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/kubernetes/ensure-persistent-data.js +211 -6
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +2 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +94 -9
- package/dist/main.js +2 -0
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-kubernetes.js +136 -13
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +415 -48
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +962 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.awaitRunObjectsGone = exports.removeRunObjects = exports.CallerNamespaces = exports.getVolumeScope = exports.KubernetesExecutor = exports.getRunRoleBinding = exports.getNamespacePolicies = exports.getClusterAccess = exports.isCallerNamespace = exports.getCallerNamespace = void 0;
|
|
3
|
+
exports.awaitRunObjectsGone = exports.removeRunObjects = exports.CallerNamespaces = exports.getVolumeScope = exports.getPrivilegedNamespaceProblem = exports.getCapabilitiesProblem = exports.getPodSecurityLabels = exports.KubernetesExecutor = exports.getRunRoleBinding = exports.removeServiceRunNamespace = exports.getServerIngressPolicy = exports.getNamespacePolicies = exports.getClusterAccess = exports.isCallerNamespace = exports.SERVICE_RUN_LABEL = exports.getServiceRunNamespace = exports.getCallerNamespace = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const os_1 = require("os");
|
|
@@ -8,6 +8,7 @@ const path_1 = require("path");
|
|
|
8
8
|
const net_1 = require("net");
|
|
9
9
|
const apply_1 = require("../kubernetes/apply");
|
|
10
10
|
const job_placement_1 = require("../kubernetes/job-placement");
|
|
11
|
+
const capacity_1 = require("./capacity");
|
|
11
12
|
const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
|
|
12
13
|
const log_1 = require("../log");
|
|
13
14
|
const network_policy_1 = require("../kubernetes/network-policy");
|
|
@@ -22,6 +23,8 @@ const POLICY_NAME = 'hammerkit';
|
|
|
22
23
|
const MANAGED_LABEL = 'hammerkit.dev/managed';
|
|
23
24
|
// how long the end of a run waits for its pods and claims to be gone
|
|
24
25
|
const RELEASE_TIMEOUT = 120000;
|
|
26
|
+
// how long the setup of a caller's namespace is taken as it is, before it is applied again
|
|
27
|
+
const NAMESPACE_SETUP_REUSE_MS = 5 * 60000;
|
|
25
28
|
// where a pod of the cluster finds the token and certificate of its ServiceAccount
|
|
26
29
|
const IN_CLUSTER_NAMESPACE = '/var/run/secrets/kubernetes.io/serviceaccount/namespace';
|
|
27
30
|
/**
|
|
@@ -32,6 +35,20 @@ function getCallerNamespace(prefix, owner) {
|
|
|
32
35
|
return `${prefix}${(0, crypto_1.createHash)('sha256').update(`${owner.iss}\0${owner.sub}`).digest('hex').slice(0, 16)}`;
|
|
33
36
|
}
|
|
34
37
|
exports.getCallerNamespace = getCallerNamespace;
|
|
38
|
+
/**
|
|
39
|
+
* The namespace of a service run: one of its own, as the services stay up for hours while
|
|
40
|
+
* the caller's other runs go on in theirs (one at a time, each removing what the one
|
|
41
|
+
* before left).
|
|
42
|
+
*/
|
|
43
|
+
function getServiceRunNamespace(prefix, run, wakes = 0) {
|
|
44
|
+
// a woken run starts in a namespace of its own, the one it slept in may still be removed;
|
|
45
|
+
// a run whose volumes are kept while it sleeps has one namespace (wakes 0) from start to end
|
|
46
|
+
const name = wakes > 0 ? `up\0${run}\0${wakes}` : `up\0${run}`;
|
|
47
|
+
return `${prefix}${(0, crypto_1.createHash)('sha256').update(name).digest('hex').slice(0, 16)}`;
|
|
48
|
+
}
|
|
49
|
+
exports.getServiceRunNamespace = getServiceRunNamespace;
|
|
50
|
+
// the label of the namespace of a service run: the run label value of its run
|
|
51
|
+
exports.SERVICE_RUN_LABEL = 'hammerkit.dev/service-run';
|
|
35
52
|
// Whether a namespace is one getCallerNamespace makes with this prefix: a server named `hk`
|
|
36
53
|
// passes over the namespaces of a server named `hk-test` on the same cluster.
|
|
37
54
|
function isCallerNamespace(prefix, name) {
|
|
@@ -143,6 +160,64 @@ function getNamespacePolicies(namespace, deniedEgress, allowedEgress = []) {
|
|
|
143
160
|
];
|
|
144
161
|
}
|
|
145
162
|
exports.getNamespacePolicies = getNamespacePolicies;
|
|
163
|
+
/**
|
|
164
|
+
* What lets the server into the namespace of a service run, to the ports of its endpoints:
|
|
165
|
+
* policies add up, so this one opens the pods to the server's namespace besides what the
|
|
166
|
+
* others let in, limited ones too.
|
|
167
|
+
*/
|
|
168
|
+
// The server reaches the ports of the endpoints, from its own namespace, and no other port:
|
|
169
|
+
// what else the run starts (a database) stays for the pods of the run.
|
|
170
|
+
function getServerIngressPolicy(namespace, server, ports) {
|
|
171
|
+
return {
|
|
172
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
173
|
+
kind: 'NetworkPolicy',
|
|
174
|
+
metadata: {
|
|
175
|
+
name: 'hammerkit-server-ingress',
|
|
176
|
+
namespace,
|
|
177
|
+
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
178
|
+
labels: { [MANAGED_LABEL]: 'true' },
|
|
179
|
+
},
|
|
180
|
+
spec: {
|
|
181
|
+
podSelector: {},
|
|
182
|
+
policyTypes: ['Ingress'],
|
|
183
|
+
ingress: ports.length > 0
|
|
184
|
+
? [
|
|
185
|
+
{
|
|
186
|
+
_from: [{ namespaceSelector: { matchLabels: { 'kubernetes.io/metadata.name': server } } }],
|
|
187
|
+
ports: [...new Set(ports)].map((port) => ({ protocol: 'TCP', port })),
|
|
188
|
+
},
|
|
189
|
+
]
|
|
190
|
+
: [],
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
exports.getServerIngressPolicy = getServerIngressPolicy;
|
|
195
|
+
// The namespace of a service run goes with all it holds; waits up to `timeout` for it.
|
|
196
|
+
async function removeServiceRunNamespace(instance, namespace, timeout) {
|
|
197
|
+
try {
|
|
198
|
+
await instance.coreApi.deleteNamespace({ name: namespace, propagationPolicy: 'Background' });
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
throw e;
|
|
205
|
+
}
|
|
206
|
+
const deadline = Date.now() + timeout;
|
|
207
|
+
while (Date.now() < deadline) {
|
|
208
|
+
try {
|
|
209
|
+
await instance.coreApi.readNamespace({ name: namespace });
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
await (0, sleep_1.sleep)(1000);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
exports.removeServiceRunNamespace = removeServiceRunNamespace;
|
|
146
221
|
/**
|
|
147
222
|
* The server's rights in a caller's namespace: the operator's ClusterRole, bound there
|
|
148
223
|
* only, so the server needs no rights on Secrets or pods anywhere else.
|
|
@@ -186,6 +261,14 @@ class KubernetesExecutor {
|
|
|
186
261
|
this.sweeps = new Map();
|
|
187
262
|
// whether the kept claims a server left when it stopped were looked for
|
|
188
263
|
this.swept = false;
|
|
264
|
+
// the namespaces where no run of this process left anything: what a run makes goes at its
|
|
265
|
+
// end, unless its release failed
|
|
266
|
+
this.tidy = new Set();
|
|
267
|
+
// the namespaces of the service runs that execute, with how many executions each (a run
|
|
268
|
+
// that moves executes beside itself), and whether those of earlier ones went
|
|
269
|
+
this.serviceRuns = new Map();
|
|
270
|
+
this.serviceRunsSwept = false;
|
|
271
|
+
this.asleep = new Set();
|
|
189
272
|
this.access = getClusterAccess(config.backend, processEnvs);
|
|
190
273
|
this.instance = (0, kubernetes_instance_1.createKubernetesInstances)({
|
|
191
274
|
type: 'kubernetes',
|
|
@@ -203,10 +286,42 @@ class KubernetesExecutor {
|
|
|
203
286
|
this.namespace = namespace;
|
|
204
287
|
this.namespaces = new CallerNamespaces(this.instance, config.backend);
|
|
205
288
|
this.keep = config.backend.keepVolumes === false ? 0 : (0, units_1.parseDuration)(config.backend.keepVolumes);
|
|
289
|
+
this.keepServiceRuns = config.runs.up.keepVolumes;
|
|
206
290
|
this.log = options.log;
|
|
207
291
|
this.host = new executor_1.HostExecutor({ ...options, placement: this.placement() });
|
|
208
292
|
}
|
|
293
|
+
// what the namespaces of the server's callers and service runs request and use
|
|
294
|
+
capacity() {
|
|
295
|
+
const prefix = this.config.backend.namespacePrefix;
|
|
296
|
+
return new capacity_1.KubernetesCapacity(this.instance, {
|
|
297
|
+
quotaName: POLICY_NAME,
|
|
298
|
+
namespaces: async () => (await this.instance.coreApi.listNamespace({ labelSelector: `${MANAGED_LABEL}=true` })).items
|
|
299
|
+
.filter((namespace) => isCallerNamespace(prefix, namespace.metadata?.name ?? ''))
|
|
300
|
+
.map((namespace) => ({
|
|
301
|
+
name: namespace.metadata?.name,
|
|
302
|
+
kind: namespace.metadata?.labels?.[exports.SERVICE_RUN_LABEL] !== undefined ? 'service-run' : 'caller',
|
|
303
|
+
})),
|
|
304
|
+
ownerOf: (namespace, runs) => {
|
|
305
|
+
for (const run of runs) {
|
|
306
|
+
if (run.up && getServiceRunNamespace(prefix, run.id, run.wakes) === namespace) {
|
|
307
|
+
return { owner: { iss: run.owner.iss, sub: run.owner.sub }, run: run.id };
|
|
308
|
+
}
|
|
309
|
+
if (getCallerNamespace(prefix, run.owner) === namespace) {
|
|
310
|
+
return { owner: { iss: run.owner.iss, sub: run.owner.sub } };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {};
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|
|
209
317
|
async execute(run, context) {
|
|
318
|
+
if (run.up) {
|
|
319
|
+
if (run.source.type === 'sandbox') {
|
|
320
|
+
throw new Error('on a cluster a service run starts from a branch, push the sandbox and start it from there');
|
|
321
|
+
}
|
|
322
|
+
// in a namespace of its own, apart from the caller's runs and their claims
|
|
323
|
+
return this.host.execute(run, context);
|
|
324
|
+
}
|
|
210
325
|
const namespace = getCallerNamespace(this.config.backend.namespacePrefix, run.owner);
|
|
211
326
|
const label = (0, job_placement_1.getRunLabelValue)(run.id);
|
|
212
327
|
let runs = this.active.get(namespace);
|
|
@@ -225,11 +340,74 @@ class KubernetesExecutor {
|
|
|
225
340
|
}
|
|
226
341
|
}
|
|
227
342
|
}
|
|
343
|
+
// A service run moves to another commit in its namespace: unchanged services stay, the
|
|
344
|
+
// others roll to their new pods (see kubernetesServiceRuntime)
|
|
345
|
+
movesBeside(run) {
|
|
346
|
+
return !!run.up;
|
|
347
|
+
}
|
|
348
|
+
namespaceOf(run) {
|
|
349
|
+
const prefix = this.config.backend.namespacePrefix;
|
|
350
|
+
return run.up
|
|
351
|
+
? getServiceRunNamespace(prefix, run.id, this.keepServiceRuns ? 0 : run.wakes)
|
|
352
|
+
: getCallerNamespace(prefix, run.owner);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* A service run that slept with its volumes ended without waking: its namespace goes, with
|
|
356
|
+
* the claims in it.
|
|
357
|
+
*/
|
|
358
|
+
async forget(run) {
|
|
359
|
+
if (!run.up) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// a namespace is kept only for a run that has one from start to end
|
|
363
|
+
const namespace = getServiceRunNamespace(this.config.backend.namespacePrefix, run.id);
|
|
364
|
+
this.asleep.delete(namespace);
|
|
365
|
+
await this.enqueue(namespace, () => this.removeServiceRun(namespace, (text) => this.log?.(`run=${run.id} ${text}`)));
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* The service runs of a previous start that sleep with their volumes: their namespaces stay,
|
|
369
|
+
* without what runs in them (a server that stopped without putting the run to sleep left
|
|
370
|
+
* it). The namespaces of any other service run go.
|
|
371
|
+
*/
|
|
372
|
+
restored(runs) {
|
|
373
|
+
if (this.keepServiceRuns) {
|
|
374
|
+
for (const run of runs.filter((candidate) => candidate.up)) {
|
|
375
|
+
const namespace = getServiceRunNamespace(this.config.backend.namespacePrefix, run.id);
|
|
376
|
+
this.asleep.add(namespace);
|
|
377
|
+
this.enqueue(namespace, () => removeRunObjects(this.instance, namespace, job_placement_1.RUN_LABEL, RELEASE_TIMEOUT, { claims: false })).catch((e) => this.log?.(`what runs in the namespace ${namespace} of run ${run.id} could not be removed: ${(0, log_1.getErrorMessage)(e)}`));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
this.sweepServiceRuns();
|
|
381
|
+
}
|
|
382
|
+
// The end of a service run: its namespace goes with all it holds. A server that may not
|
|
383
|
+
// delete it removes what runs in it, and looks again at the next service run.
|
|
384
|
+
async removeServiceRun(namespace, say) {
|
|
385
|
+
try {
|
|
386
|
+
await removeServiceRunNamespace(this.instance, namespace, RELEASE_TIMEOUT);
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
// at least nothing of it runs on: its objects are the server's to remove (runRole)
|
|
390
|
+
const forbidden = (0, apply_1.statusCodeOf)(e) === 403;
|
|
391
|
+
const text = forbidden
|
|
392
|
+
? `the server may not delete the namespace ${namespace}, its ClusterRole needs delete on namespaces (see the backend docs); removing what runs in it`
|
|
393
|
+
: `the namespace ${namespace} could not be removed: ${(0, log_1.getErrorMessage)(e)}`;
|
|
394
|
+
this.log?.(text);
|
|
395
|
+
say(text);
|
|
396
|
+
try {
|
|
397
|
+
await removeRunObjects(this.instance, namespace, job_placement_1.RUN_LABEL, RELEASE_TIMEOUT);
|
|
398
|
+
}
|
|
399
|
+
catch (inner) {
|
|
400
|
+
say(`what the run made in ${namespace} could not be removed: ${(0, log_1.getErrorMessage)(inner)}`);
|
|
401
|
+
}
|
|
402
|
+
// the next look removes the namespace
|
|
403
|
+
this.serviceRunsSwept = false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
228
406
|
// The runs of a scope share its kept claims, which one of them may change at a time:
|
|
229
407
|
// what a task finds in one is what the last run left there. Without kept claims, runs
|
|
230
408
|
// share no object and nothing is held.
|
|
231
409
|
exclusive(run) {
|
|
232
|
-
if (this.keep === 0) {
|
|
410
|
+
if (this.keep === 0 || run.up) {
|
|
233
411
|
return undefined;
|
|
234
412
|
}
|
|
235
413
|
return `${getCallerNamespace(this.config.backend.namespacePrefix, run.owner)}/${getVolumeScope(run)}`;
|
|
@@ -258,21 +436,48 @@ class KubernetesExecutor {
|
|
|
258
436
|
return execution;
|
|
259
437
|
}
|
|
260
438
|
placement() {
|
|
261
|
-
const backend = this.config.backend;
|
|
262
439
|
return {
|
|
263
440
|
prepare: async (run, context, work) => {
|
|
264
|
-
const namespace =
|
|
441
|
+
const namespace = this.namespaceOf(run);
|
|
442
|
+
if (run.up) {
|
|
443
|
+
if (context.signal.aborted) {
|
|
444
|
+
throw new Error('the run was cancelled');
|
|
445
|
+
}
|
|
446
|
+
this.serviceRuns.set(namespace, (this.serviceRuns.get(namespace) ?? 0) + 1);
|
|
447
|
+
this.asleep.delete(namespace);
|
|
448
|
+
this.sweepServiceRuns();
|
|
449
|
+
const up = run.up;
|
|
450
|
+
// after what a stop of its sleep left in it went (see restored)
|
|
451
|
+
await this.enqueue(namespace, async () => {
|
|
452
|
+
if (context.signal.aborted) {
|
|
453
|
+
throw new Error('the run was cancelled');
|
|
454
|
+
}
|
|
455
|
+
// reached by the server at the Services of its endpoints
|
|
456
|
+
await this.namespaces.prepare(namespace, {
|
|
457
|
+
serviceRun: (0, job_placement_1.getRunLabelValue)(run.id),
|
|
458
|
+
server: this.namespace,
|
|
459
|
+
ports: (up.endpoints ?? []).map((endpoint) => endpoint.port),
|
|
460
|
+
quota: this.config.runs.up.quota,
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
context.emit({ type: 'log', task: 'hammerkit', stream: 'stdout', text: `runs in the namespace ${namespace}` });
|
|
464
|
+
return { kubernetes: this.environmentOf(run, namespace), env: this.access.env };
|
|
465
|
+
}
|
|
265
466
|
const label = (0, job_placement_1.getRunLabelValue)(run.id);
|
|
467
|
+
let capabilitiesProblem;
|
|
266
468
|
await this.enqueue(namespace, async () => {
|
|
267
469
|
if (context.signal.aborted) {
|
|
268
470
|
throw new Error('the run was cancelled');
|
|
269
471
|
}
|
|
270
|
-
await this.namespaces.prepare(namespace);
|
|
271
|
-
// what an earlier run left when the server stopped before its end
|
|
272
|
-
// a run that does not execute. One that starts
|
|
273
|
-
// before it makes any.
|
|
274
|
-
|
|
275
|
-
|
|
472
|
+
capabilitiesProblem = await this.namespaces.prepare(namespace);
|
|
473
|
+
// what an earlier run left when the server stopped before its end, or when its
|
|
474
|
+
// release failed: the objects of a run that does not execute. One that starts
|
|
475
|
+
// after this waits for it here before it makes any.
|
|
476
|
+
if (!this.tidy.has(namespace)) {
|
|
477
|
+
const running = [...new Set([label, ...(this.active.get(namespace)?.keys() ?? [])])];
|
|
478
|
+
await removeRunObjects(this.instance, namespace, `${job_placement_1.RUN_LABEL},${job_placement_1.RUN_LABEL} notin (${running.join(',')})`, RELEASE_TIMEOUT);
|
|
479
|
+
this.tidy.add(namespace);
|
|
480
|
+
}
|
|
276
481
|
// the claims earlier runs kept that expired; the others this run may mount
|
|
277
482
|
const { next } = await (0, kept_claims_1.sweepKeptClaims)(this.instance, namespace, {
|
|
278
483
|
now: new Date(),
|
|
@@ -286,35 +491,52 @@ class KubernetesExecutor {
|
|
|
286
491
|
});
|
|
287
492
|
this.sweepLeftClaims();
|
|
288
493
|
context.emit({ type: 'log', task: 'hammerkit', stream: 'stdout', text: `runs in the namespace ${namespace}` });
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
kubeConfig: this.access.kubeConfig,
|
|
293
|
-
context: this.access.context,
|
|
294
|
-
httpRoutes: [],
|
|
295
|
-
storageClass: backend.storageClass,
|
|
296
|
-
volumeSize: backend.volumeSize,
|
|
297
|
-
maxVolumeSize: (0, server_config_1.getMaxVolumeSize)(backend),
|
|
298
|
-
networkPolicyEnforced: backend.networkPolicy === 'enforced',
|
|
299
|
-
server: {
|
|
300
|
-
run: run.id,
|
|
301
|
-
namespace: this.namespace,
|
|
302
|
-
pools: backend.pools,
|
|
303
|
-
providers: backend.providers,
|
|
304
|
-
accounts: Object.fromEntries(Object.entries(this.config.accounts).flatMap(([name, account]) => account.kubernetes ? [[name, account.kubernetes]] : [])),
|
|
305
|
-
...(work?.claim ? { workspace: getWorkspace(work.root, work.claim, namespace) } : {}),
|
|
306
|
-
...(this.keep > 0 ? { volumeScope: getVolumeScope(run) } : {}),
|
|
307
|
-
},
|
|
494
|
+
return {
|
|
495
|
+
kubernetes: this.environmentOf(run, namespace, work, capabilitiesProblem),
|
|
496
|
+
env: this.access.env,
|
|
308
497
|
};
|
|
309
|
-
return { kubernetes, env: this.access.env };
|
|
310
498
|
},
|
|
311
499
|
release: async (run, context) => {
|
|
312
|
-
const namespace =
|
|
500
|
+
const namespace = this.namespaceOf(run);
|
|
501
|
+
if (run.up) {
|
|
502
|
+
const say = (text) => context.emit({ type: 'log', task: 'hammerkit', stream: 'stderr', text });
|
|
503
|
+
try {
|
|
504
|
+
if (context.keep?.() === 'everything') {
|
|
505
|
+
// the services are another execution's of the run now: the one it moved to, or
|
|
506
|
+
// the one a move that did not get there leaves them to
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (context.keep?.() === 'volumes' && this.keepServiceRuns) {
|
|
510
|
+
// it sleeps: what runs goes, the namespace stays with its claims and policies
|
|
511
|
+
this.asleep.add(namespace);
|
|
512
|
+
await this.enqueue(namespace, () => removeRunObjects(this.instance, namespace, job_placement_1.RUN_LABEL, RELEASE_TIMEOUT, { claims: false })).catch((e) => {
|
|
513
|
+
// the wake applies its services again over what is left, the end removes it
|
|
514
|
+
const text = `what runs in the namespace ${namespace} could not be stopped: ${(0, log_1.getErrorMessage)(e)}`;
|
|
515
|
+
this.log?.(text);
|
|
516
|
+
say(text);
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
await this.enqueue(namespace, () => this.removeServiceRun(namespace, say));
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
const executions = (this.serviceRuns.get(namespace) ?? 1) - 1;
|
|
524
|
+
if (executions > 0) {
|
|
525
|
+
this.serviceRuns.set(namespace, executions);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
this.serviceRuns.delete(namespace);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
313
533
|
try {
|
|
314
534
|
const selector = `${job_placement_1.RUN_LABEL}=${(0, job_placement_1.getRunLabelValue)(run.id)}`;
|
|
315
535
|
await removeRunObjects(this.instance, namespace, selector, RELEASE_TIMEOUT);
|
|
316
536
|
}
|
|
317
537
|
catch (e) {
|
|
538
|
+
// the next run removes what is left
|
|
539
|
+
this.tidy.delete(namespace);
|
|
318
540
|
context.emit({
|
|
319
541
|
type: 'log',
|
|
320
542
|
task: 'hammerkit',
|
|
@@ -340,6 +562,67 @@ class KubernetesExecutor {
|
|
|
340
562
|
},
|
|
341
563
|
};
|
|
342
564
|
}
|
|
565
|
+
// The cluster as the process of a run sees it: the namespace its tasks and services go in.
|
|
566
|
+
environmentOf(run, namespace, work, capabilitiesProblem) {
|
|
567
|
+
const backend = this.config.backend;
|
|
568
|
+
return {
|
|
569
|
+
type: 'kubernetes',
|
|
570
|
+
namespace,
|
|
571
|
+
kubeConfig: this.access.kubeConfig,
|
|
572
|
+
context: this.access.context,
|
|
573
|
+
httpRoutes: [],
|
|
574
|
+
storageClass: backend.storageClass,
|
|
575
|
+
volumeSize: backend.volumeSize,
|
|
576
|
+
maxVolumeSize: (0, server_config_1.getMaxVolumeSize)(backend),
|
|
577
|
+
networkPolicyEnforced: backend.networkPolicy === 'enforced',
|
|
578
|
+
server: {
|
|
579
|
+
run: run.id,
|
|
580
|
+
namespace: this.namespace,
|
|
581
|
+
pools: backend.pools,
|
|
582
|
+
providers: backend.providers,
|
|
583
|
+
accounts: Object.fromEntries(Object.entries(this.config.accounts).flatMap(([name, account]) => account.kubernetes ? [[name, account.kubernetes]] : [])),
|
|
584
|
+
...(work?.claim ? { workspace: getWorkspace(work.root, work.claim, namespace, backend.sandboxes.runner) } : {}),
|
|
585
|
+
// the claims of a service run go with its namespace
|
|
586
|
+
...(this.keep > 0 && !run.up ? { volumeScope: getVolumeScope(run) } : {}),
|
|
587
|
+
...(run.up ? { serviceRun: true } : {}),
|
|
588
|
+
...(backend.capabilities
|
|
589
|
+
? {
|
|
590
|
+
capabilities: {
|
|
591
|
+
runtimeClass: backend.capabilities.runtimeClass,
|
|
592
|
+
...(capabilitiesProblem ? { unavailable: capabilitiesProblem } : {}),
|
|
593
|
+
},
|
|
594
|
+
}
|
|
595
|
+
: {}),
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// Once, on the first service run: the namespaces of service runs the server left when it
|
|
600
|
+
// stopped, whose services went with it
|
|
601
|
+
// Once when the server starts, and on the next service run after a namespace could not be
|
|
602
|
+
// removed: the namespaces of service runs that none of the server's runs is in, whose
|
|
603
|
+
// services went with the process of a server that stopped
|
|
604
|
+
sweepServiceRuns() {
|
|
605
|
+
if (this.serviceRunsSwept) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
this.serviceRunsSwept = true;
|
|
609
|
+
Promise.resolve()
|
|
610
|
+
.then(() => this.instance.coreApi.listNamespace({ labelSelector: `${MANAGED_LABEL}=true,${exports.SERVICE_RUN_LABEL}` }))
|
|
611
|
+
.then(async (namespaces) => {
|
|
612
|
+
for (const namespace of namespaces.items) {
|
|
613
|
+
const name = namespace.metadata?.name;
|
|
614
|
+
if (name &&
|
|
615
|
+
isCallerNamespace(this.config.backend.namespacePrefix, name) &&
|
|
616
|
+
!this.serviceRuns.has(name) &&
|
|
617
|
+
!this.asleep.has(name) &&
|
|
618
|
+
!namespace.metadata?.deletionTimestamp) {
|
|
619
|
+
this.log?.(`removing the namespace ${name} of a service run from before the server started`);
|
|
620
|
+
await removeServiceRunNamespace(this.instance, name, 0);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
.catch((e) => this.log?.(`the namespaces of service runs could not be removed: ${(0, log_1.getErrorMessage)(e)}`));
|
|
625
|
+
}
|
|
343
626
|
// The kept claims of a namespace that expired go at the soonest expiry the server knows
|
|
344
627
|
// of, but those of a scope a run of the caller executes in
|
|
345
628
|
sweepLater(namespace, delay) {
|
|
@@ -378,7 +661,7 @@ class KubernetesExecutor {
|
|
|
378
661
|
}
|
|
379
662
|
this.swept = true;
|
|
380
663
|
Promise.resolve()
|
|
381
|
-
.then(() => this.instance.coreApi.listNamespace({ labelSelector: `${MANAGED_LABEL}=true` }))
|
|
664
|
+
.then(() => this.instance.coreApi.listNamespace({ labelSelector: `${MANAGED_LABEL}=true,!${exports.SERVICE_RUN_LABEL}` }))
|
|
382
665
|
.then((namespaces) => {
|
|
383
666
|
for (const namespace of namespaces.items) {
|
|
384
667
|
const name = namespace.metadata?.name;
|
|
@@ -393,11 +676,55 @@ class KubernetesExecutor {
|
|
|
393
676
|
}
|
|
394
677
|
}
|
|
395
678
|
exports.KubernetesExecutor = KubernetesExecutor;
|
|
679
|
+
const ENFORCE_LABEL = 'pod-security.kubernetes.io/enforce';
|
|
680
|
+
const AUDIT_LABEL = 'pod-security.kubernetes.io/audit';
|
|
681
|
+
/**
|
|
682
|
+
* The Pod Security level a caller's namespace is made with: `baseline`, no privileged pods,
|
|
683
|
+
* host paths or host network, whatever a build asks. `baseline` also refuses the capabilities
|
|
684
|
+
* of `backend.capabilities`, sandboxed or not: with them the namespace enforces `privileged`
|
|
685
|
+
* and audits `baseline`, and the operator's admission policy (see the docs on capabilities)
|
|
686
|
+
* refuses what `baseline` would to a pod outside the sandbox's RuntimeClass.
|
|
687
|
+
*/
|
|
688
|
+
function getPodSecurityLabels(backend) {
|
|
689
|
+
return backend.capabilities
|
|
690
|
+
? { [ENFORCE_LABEL]: 'privileged', [AUDIT_LABEL]: 'baseline' }
|
|
691
|
+
: { [ENFORCE_LABEL]: 'baseline' };
|
|
692
|
+
}
|
|
693
|
+
exports.getPodSecurityLabels = getPodSecurityLabels;
|
|
694
|
+
/**
|
|
695
|
+
* The server never changes the level of a namespace it made (that would take `patch` on every
|
|
696
|
+
* namespace of the cluster): one made before `backend.capabilities` is relabelled by the
|
|
697
|
+
* operator. Until then its tasks that add capabilities fail with this, the others run.
|
|
698
|
+
*/
|
|
699
|
+
function getCapabilitiesProblem(namespace, labels, backend) {
|
|
700
|
+
const enforce = labels?.[ENFORCE_LABEL];
|
|
701
|
+
if (!backend.capabilities || enforce === 'privileged') {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
return (`the namespace ${namespace} was made before backend.capabilities and enforces ` +
|
|
705
|
+
`${enforce ? `Pod Security ${enforce}` : 'no Pod Security level of its own'}, not privileged: kubectl label namespace ${namespace} ` +
|
|
706
|
+
`${ENFORCE_LABEL}=privileged ${AUDIT_LABEL}=baseline --overwrite`);
|
|
707
|
+
}
|
|
708
|
+
exports.getCapabilitiesProblem = getCapabilitiesProblem;
|
|
709
|
+
/**
|
|
710
|
+
* A namespace made while the server had `backend.capabilities` admits privileged pods but for
|
|
711
|
+
* the operator's admission policy. Without capabilities the server runs nothing in it until the
|
|
712
|
+
* operator sets it back.
|
|
713
|
+
*/
|
|
714
|
+
function getPrivilegedNamespaceProblem(namespace, labels, backend) {
|
|
715
|
+
if (backend.capabilities || labels?.[ENFORCE_LABEL] !== 'privileged') {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
return (`the namespace ${namespace} enforces Pod Security privileged, made while the server had ` +
|
|
719
|
+
`backend.capabilities, and the server runs nothing in it without them: kubectl label namespace ` +
|
|
720
|
+
`${namespace} ${ENFORCE_LABEL}=baseline ${AUDIT_LABEL}- --overwrite`);
|
|
721
|
+
}
|
|
722
|
+
exports.getPrivilegedNamespaceProblem = getPrivilegedNamespaceProblem;
|
|
396
723
|
// What may mount a claim a run kept: the runs of the same repository, as they share the
|
|
397
724
|
// cache, and on a sandbox the runs of that sandbox only, whose claim is on the same node or
|
|
398
725
|
// zone as the kept ones. The owner is the namespace's.
|
|
399
726
|
function getVolumeScope(run) {
|
|
400
|
-
if (run.source.type === 'sandbox') {
|
|
727
|
+
if (run.source.type === 'sandbox' && !run.source.check) {
|
|
401
728
|
return `sandbox-${(0, crypto_1.createHash)('sha256').update(run.source.id).digest('hex').slice(0, 24)}`;
|
|
402
729
|
}
|
|
403
730
|
return (0, executor_1.cacheNamespace)(run);
|
|
@@ -411,6 +738,8 @@ class CallerNamespaces {
|
|
|
411
738
|
constructor(instance, backend) {
|
|
412
739
|
this.instance = instance;
|
|
413
740
|
this.backend = backend;
|
|
741
|
+
// the namespaces this process set up, by their uid, and when
|
|
742
|
+
this.applied = new Map();
|
|
414
743
|
}
|
|
415
744
|
name(owner) {
|
|
416
745
|
return getCallerNamespace(this.backend.namespacePrefix, owner);
|
|
@@ -418,9 +747,15 @@ class CallerNamespaces {
|
|
|
418
747
|
// The caller's namespace: made by the server and kept apart from the others. The
|
|
419
748
|
// server binds its rights there, isolates the network of the pods, applies the
|
|
420
749
|
// operator's quota and gives containers without resources default requests, so they
|
|
421
|
-
// are admitted under a quota on compute. All of it is replaced
|
|
422
|
-
// namespace
|
|
423
|
-
|
|
750
|
+
// are admitted under a quota on compute. All of it is replaced when the server sets up
|
|
751
|
+
// a namespace it did not set up in the last `NAMESPACE_SETUP_REUSE_MS` (it reads the
|
|
752
|
+
// config once, at its start), so the namespace follows the config; a namespace made anew
|
|
753
|
+
// since (deleted and made again) is set up again at once. Returns what keeps the tasks that
|
|
754
|
+
// add capabilities from running in it (getCapabilitiesProblem), if anything.
|
|
755
|
+
async prepare(namespace,
|
|
756
|
+
// a service run's own namespace: its label, the namespace of the server that reaches its
|
|
757
|
+
// endpoints, and its quota
|
|
758
|
+
serviceRun) {
|
|
424
759
|
const backend = this.backend;
|
|
425
760
|
let existing;
|
|
426
761
|
try {
|
|
@@ -434,17 +769,33 @@ class CallerNamespaces {
|
|
|
434
769
|
if (existing && existing.metadata?.labels?.[MANAGED_LABEL] !== 'true') {
|
|
435
770
|
throw new Error(`the namespace ${namespace} was not made by hammerkit, the server does not run tasks in it`);
|
|
436
771
|
}
|
|
772
|
+
const privileged = getPrivilegedNamespaceProblem(namespace, existing?.metadata?.labels, backend);
|
|
773
|
+
if (privileged) {
|
|
774
|
+
throw new Error(privileged);
|
|
775
|
+
}
|
|
776
|
+
// one made by another run of this server at the same time has the labels of its config
|
|
777
|
+
const problem = () => getCapabilitiesProblem(namespace, existing?.metadata?.labels ?? getPodSecurityLabels(backend), backend);
|
|
778
|
+
const started = Date.now();
|
|
779
|
+
const applied = this.applied.get(namespace);
|
|
780
|
+
if (existing &&
|
|
781
|
+
!existing.metadata?.deletionTimestamp &&
|
|
782
|
+
applied &&
|
|
783
|
+
applied.uid === existing.metadata?.uid &&
|
|
784
|
+
started - applied.at < NAMESPACE_SETUP_REUSE_MS) {
|
|
785
|
+
return problem();
|
|
786
|
+
}
|
|
787
|
+
this.applied.delete(namespace);
|
|
437
788
|
if (!existing) {
|
|
438
789
|
try {
|
|
439
|
-
await this.instance.coreApi.createNamespace({
|
|
790
|
+
existing = await this.instance.coreApi.createNamespace({
|
|
440
791
|
body: {
|
|
441
792
|
metadata: {
|
|
442
793
|
name: namespace,
|
|
443
794
|
labels: {
|
|
444
795
|
[MANAGED_LABEL]: 'true',
|
|
445
796
|
'hammerkit.dev/caller': namespace,
|
|
446
|
-
|
|
447
|
-
|
|
797
|
+
...(serviceRun ? { [exports.SERVICE_RUN_LABEL]: serviceRun.serviceRun } : {}),
|
|
798
|
+
...getPodSecurityLabels(backend),
|
|
448
799
|
},
|
|
449
800
|
},
|
|
450
801
|
},
|
|
@@ -460,7 +811,10 @@ class CallerNamespaces {
|
|
|
460
811
|
const [first, ...policies] = getNamespacePolicies(namespace, backend.deniedEgress, backend.allowedEgress);
|
|
461
812
|
// the API server may refuse what the binding allows for a moment after it was made
|
|
462
813
|
await untilAllowed(() => (0, apply_1.replace)(this.instance, first));
|
|
463
|
-
for (const policy of
|
|
814
|
+
for (const policy of [
|
|
815
|
+
...policies,
|
|
816
|
+
...(serviceRun ? [getServerIngressPolicy(namespace, serviceRun.server, serviceRun.ports)] : []),
|
|
817
|
+
]) {
|
|
464
818
|
await (0, apply_1.replace)(this.instance, policy);
|
|
465
819
|
}
|
|
466
820
|
const metadata = {
|
|
@@ -473,7 +827,7 @@ class CallerNamespaces {
|
|
|
473
827
|
apiVersion: 'v1',
|
|
474
828
|
kind: 'ResourceQuota',
|
|
475
829
|
metadata,
|
|
476
|
-
spec: { hard: backend.quota },
|
|
830
|
+
spec: { hard: serviceRun?.quota ?? backend.quota },
|
|
477
831
|
};
|
|
478
832
|
const defaults = backend.defaultResources;
|
|
479
833
|
const limits = {
|
|
@@ -491,7 +845,7 @@ class CallerNamespaces {
|
|
|
491
845
|
},
|
|
492
846
|
};
|
|
493
847
|
for (const [object, wanted] of [
|
|
494
|
-
[quota, !!backend.quota],
|
|
848
|
+
[quota, !!(serviceRun?.quota ?? backend.quota)],
|
|
495
849
|
[limits, !!defaults],
|
|
496
850
|
]) {
|
|
497
851
|
if (wanted) {
|
|
@@ -506,6 +860,11 @@ class CallerNamespaces {
|
|
|
506
860
|
});
|
|
507
861
|
}
|
|
508
862
|
}
|
|
863
|
+
const uid = existing?.metadata?.uid;
|
|
864
|
+
if (uid) {
|
|
865
|
+
this.applied.set(namespace, { uid, at: started });
|
|
866
|
+
}
|
|
867
|
+
return problem();
|
|
509
868
|
}
|
|
510
869
|
whoAmI() {
|
|
511
870
|
if (!this.subject) {
|
|
@@ -528,7 +887,7 @@ class CallerNamespaces {
|
|
|
528
887
|
exports.CallerNamespaces = CallerNamespaces;
|
|
529
888
|
// The claim of a sandbox as the Jobs of its run mount it: the run's copy of the files is
|
|
530
889
|
// `root`, a source below it is the same path below `subPath` in the claim.
|
|
531
|
-
function getWorkspace(root, claim, namespace) {
|
|
890
|
+
function getWorkspace(root, claim, namespace, runner) {
|
|
532
891
|
if (claim.namespace !== namespace) {
|
|
533
892
|
throw new Error(`the sandbox is in ${claim.namespace}, the run in ${namespace}`);
|
|
534
893
|
}
|
|
@@ -538,6 +897,7 @@ function getWorkspace(root, claim, namespace) {
|
|
|
538
897
|
subPath: claim.subPath,
|
|
539
898
|
label: claim.label,
|
|
540
899
|
...(claim.pod ? { pod: claim.pod } : {}),
|
|
900
|
+
...(runner ? { runner: true } : {}),
|
|
541
901
|
};
|
|
542
902
|
}
|
|
543
903
|
// Everything a run made in the caller's namespace, by the label selector of the run:
|
|
@@ -546,16 +906,21 @@ function getWorkspace(root, claim, namespace) {
|
|
|
546
906
|
// run outlives its policy.
|
|
547
907
|
// Deletes what a run made. The NetworkPolicies go last, once the pods are: a limited
|
|
548
908
|
// pod's own policy is all that limits it.
|
|
549
|
-
async function removeRunObjects(instance, namespace, labelSelector, timeout
|
|
909
|
+
async function removeRunObjects(instance, namespace, labelSelector, timeout,
|
|
910
|
+
// `claims: false` leaves the claims: a service run that sleeps keeps its volumes
|
|
911
|
+
options = {}) {
|
|
912
|
+
const claims = options.claims ?? true;
|
|
550
913
|
const selector = { namespace, labelSelector };
|
|
551
914
|
await instance.batchApi.deleteCollectionNamespacedJob({ ...selector, propagationPolicy: 'Background' });
|
|
552
915
|
await instance.appsApi.deleteCollectionNamespacedDeployment({ ...selector, propagationPolicy: 'Background' });
|
|
553
916
|
await instance.coreApi.deleteCollectionNamespacedPod({ ...selector, gracePeriodSeconds: 0 });
|
|
554
917
|
await instance.coreApi.deleteCollectionNamespacedService(selector);
|
|
555
918
|
await instance.coreApi.deleteCollectionNamespacedSecret(selector);
|
|
556
|
-
|
|
919
|
+
if (claims) {
|
|
920
|
+
await instance.coreApi.deleteCollectionNamespacedPersistentVolumeClaim(selector);
|
|
921
|
+
}
|
|
557
922
|
// the next run of the caller also uses the same claim names
|
|
558
|
-
await awaitRunObjectsGone(instance, namespace, labelSelector, timeout);
|
|
923
|
+
await awaitRunObjectsGone(instance, namespace, labelSelector, timeout, claims);
|
|
559
924
|
const policies = await instance.objectApi.list('networking.k8s.io/v1', 'NetworkPolicy', namespace, undefined, undefined, undefined, undefined, labelSelector);
|
|
560
925
|
for (const policy of policies.items) {
|
|
561
926
|
// the items of a list do not say their kind
|
|
@@ -584,11 +949,13 @@ async function untilAllowed(fn, attempts = 20) {
|
|
|
584
949
|
}
|
|
585
950
|
}
|
|
586
951
|
// Until the pods and claims of a run are gone: a claim is kept while a pod uses it.
|
|
587
|
-
async function awaitRunObjectsGone(instance, namespace, labelSelector, timeout) {
|
|
952
|
+
async function awaitRunObjectsGone(instance, namespace, labelSelector, timeout, withClaims = true) {
|
|
588
953
|
const deadline = Date.now() + timeout;
|
|
589
954
|
for (;;) {
|
|
590
955
|
const pods = await instance.coreApi.listNamespacedPod({ namespace, labelSelector });
|
|
591
|
-
const claims =
|
|
956
|
+
const claims = withClaims
|
|
957
|
+
? await instance.coreApi.listNamespacedPersistentVolumeClaim({ namespace, labelSelector })
|
|
958
|
+
: { items: [] };
|
|
592
959
|
const left = [...pods.items, ...claims.items].map((item) => `${item.kind ?? ''} ${item.metadata?.name}`.trim());
|
|
593
960
|
if (left.length === 0) {
|
|
594
961
|
return;
|