hammerkit 1.27.0-next.8 → 2.0.0-next.1
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 +76 -17
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +138 -18
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +212 -10
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/job-secrets.js +9 -6
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +16 -8
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +96 -11
- 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 +153 -26
- 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 +478 -70
- 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 +969 -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.
|
|
@@ -167,22 +242,33 @@ exports.getRunRoleBinding = getRunRoleBinding;
|
|
|
167
242
|
* process of the server as on a host (see HostExecutor); what changes is where its
|
|
168
243
|
* container tasks and services go: into the caller's namespace, on the nodes of the
|
|
169
244
|
* operator's pools, with the secrets of providers read in the Job itself. The runs
|
|
170
|
-
* of
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
245
|
+
* of a caller execute side by side in their namespace, up to `runs.limits.running`:
|
|
246
|
+
* the objects of a run are named and found by its run label (getItemKey), so two runs
|
|
247
|
+
* of the same task have their own. What two runs share are the claims the server keeps
|
|
248
|
+
* (`backend.keepVolumes`) of a scope (getVolumeScope): the runs of one scope go one at
|
|
249
|
+
* a time (exclusive). When a run ends, everything made for it is removed, but for the
|
|
250
|
+
* kept claims, which go once they expired.
|
|
174
251
|
*/
|
|
175
252
|
class KubernetesExecutor {
|
|
176
253
|
constructor(config, processEnvs, options) {
|
|
177
254
|
this.config = config;
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
// the run that executes, or the last one waiting, per caller namespace
|
|
255
|
+
// per caller namespace, what changes objects of the whole namespace: its setup, the
|
|
256
|
+
// removal of what a stopped server left, the sweeps of kept claims. The last one waiting.
|
|
181
257
|
this.queues = new Map();
|
|
258
|
+
// per caller namespace, the runs that execute in it: by run label, their volume scope
|
|
259
|
+
this.active = new Map();
|
|
182
260
|
// per caller namespace, when its kept claims are next looked at
|
|
183
261
|
this.sweeps = new Map();
|
|
184
262
|
// whether the kept claims a server left when it stopped were looked for
|
|
185
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();
|
|
186
272
|
this.access = getClusterAccess(config.backend, processEnvs);
|
|
187
273
|
this.instance = (0, kubernetes_instance_1.createKubernetesInstances)({
|
|
188
274
|
type: 'kubernetes',
|
|
@@ -200,20 +286,143 @@ class KubernetesExecutor {
|
|
|
200
286
|
this.namespace = namespace;
|
|
201
287
|
this.namespaces = new CallerNamespaces(this.instance, config.backend);
|
|
202
288
|
this.keep = config.backend.keepVolumes === false ? 0 : (0, units_1.parseDuration)(config.backend.keepVolumes);
|
|
289
|
+
this.keepServiceRuns = config.runs.up.keepVolumes;
|
|
203
290
|
this.log = options.log;
|
|
204
291
|
this.host = new executor_1.HostExecutor({ ...options, placement: this.placement() });
|
|
205
292
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}
|
|
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');
|
|
211
321
|
}
|
|
322
|
+
// in a namespace of its own, apart from the caller's runs and their claims
|
|
212
323
|
return this.host.execute(run, context);
|
|
213
|
-
}
|
|
324
|
+
}
|
|
325
|
+
const namespace = getCallerNamespace(this.config.backend.namespacePrefix, run.owner);
|
|
326
|
+
const label = (0, job_placement_1.getRunLabelValue)(run.id);
|
|
327
|
+
let runs = this.active.get(namespace);
|
|
328
|
+
if (!runs) {
|
|
329
|
+
runs = new Map();
|
|
330
|
+
this.active.set(namespace, runs);
|
|
331
|
+
}
|
|
332
|
+
runs.set(label, getVolumeScope(run));
|
|
333
|
+
try {
|
|
334
|
+
await this.host.execute(run, context);
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
runs.delete(label);
|
|
338
|
+
if (runs.size === 0 && this.active.get(namespace) === runs) {
|
|
339
|
+
this.active.delete(namespace);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
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
|
+
}
|
|
214
405
|
}
|
|
215
|
-
//
|
|
216
|
-
//
|
|
406
|
+
// The runs of a scope share its kept claims, which one of them may change at a time:
|
|
407
|
+
// what a task finds in one is what the last run left there. Without kept claims, runs
|
|
408
|
+
// share no object and nothing is held.
|
|
409
|
+
exclusive(run) {
|
|
410
|
+
if (this.keep === 0 || run.up) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
return `${getCallerNamespace(this.config.backend.namespacePrefix, run.owner)}/${getVolumeScope(run)}`;
|
|
414
|
+
}
|
|
415
|
+
// The scopes of the runs that execute in the namespace, but `except`'s: their kept claims
|
|
416
|
+
// are theirs to mount and stay. A server that keeps none mounts none.
|
|
417
|
+
scopesInUse(namespace, except) {
|
|
418
|
+
if (this.keep === 0) {
|
|
419
|
+
return new Set();
|
|
420
|
+
}
|
|
421
|
+
return new Set([...(this.active.get(namespace) ?? new Map()).entries()]
|
|
422
|
+
.filter(([label]) => label !== except)
|
|
423
|
+
.map(([, scope]) => scope));
|
|
424
|
+
}
|
|
425
|
+
// What changes objects of a whole namespace happens one at a time, not what its runs do
|
|
217
426
|
enqueue(namespace, fn) {
|
|
218
427
|
const previous = this.queues.get(namespace) ?? Promise.resolve();
|
|
219
428
|
const execution = previous.then(fn);
|
|
@@ -227,54 +436,107 @@ class KubernetesExecutor {
|
|
|
227
436
|
return execution;
|
|
228
437
|
}
|
|
229
438
|
placement() {
|
|
230
|
-
const backend = this.config.backend;
|
|
231
439
|
return {
|
|
232
440
|
prepare: async (run, context, work) => {
|
|
233
|
-
const namespace =
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 };
|
|
246
465
|
}
|
|
466
|
+
const label = (0, job_placement_1.getRunLabelValue)(run.id);
|
|
467
|
+
let capabilitiesProblem;
|
|
468
|
+
await this.enqueue(namespace, async () => {
|
|
469
|
+
if (context.signal.aborted) {
|
|
470
|
+
throw new Error('the run was cancelled');
|
|
471
|
+
}
|
|
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
|
+
}
|
|
481
|
+
// the claims earlier runs kept that expired; the others this run may mount
|
|
482
|
+
const { next } = await (0, kept_claims_1.sweepKeptClaims)(this.instance, namespace, {
|
|
483
|
+
now: new Date(),
|
|
484
|
+
keep: this.keep > 0,
|
|
485
|
+
timeout: RELEASE_TIMEOUT,
|
|
486
|
+
inUse: this.scopesInUse(namespace, label),
|
|
487
|
+
});
|
|
488
|
+
if (next) {
|
|
489
|
+
this.sweepLater(namespace, Math.max(0, next.getTime() - Date.now()));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
247
492
|
this.sweepLeftClaims();
|
|
248
493
|
context.emit({ type: 'log', task: 'hammerkit', stream: 'stdout', text: `runs in the namespace ${namespace}` });
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
kubeConfig: this.access.kubeConfig,
|
|
253
|
-
context: this.access.context,
|
|
254
|
-
httpRoutes: [],
|
|
255
|
-
storageClass: backend.storageClass,
|
|
256
|
-
volumeSize: backend.volumeSize,
|
|
257
|
-
maxVolumeSize: (0, server_config_1.getMaxVolumeSize)(backend),
|
|
258
|
-
networkPolicyEnforced: backend.networkPolicy === 'enforced',
|
|
259
|
-
server: {
|
|
260
|
-
run: run.id,
|
|
261
|
-
namespace: this.namespace,
|
|
262
|
-
pools: backend.pools,
|
|
263
|
-
providers: backend.providers,
|
|
264
|
-
accounts: Object.fromEntries(Object.entries(this.config.accounts).flatMap(([name, account]) => account.kubernetes ? [[name, account.kubernetes]] : [])),
|
|
265
|
-
...(work?.claim ? { workspace: getWorkspace(work.root, work.claim, namespace) } : {}),
|
|
266
|
-
...(this.keep > 0 ? { volumeScope: getVolumeScope(run) } : {}),
|
|
267
|
-
},
|
|
494
|
+
return {
|
|
495
|
+
kubernetes: this.environmentOf(run, namespace, work, capabilitiesProblem),
|
|
496
|
+
env: this.access.env,
|
|
268
497
|
};
|
|
269
|
-
return { kubernetes, env: this.access.env };
|
|
270
498
|
},
|
|
271
499
|
release: async (run, context) => {
|
|
272
|
-
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
|
+
}
|
|
273
533
|
try {
|
|
274
534
|
const selector = `${job_placement_1.RUN_LABEL}=${(0, job_placement_1.getRunLabelValue)(run.id)}`;
|
|
275
535
|
await removeRunObjects(this.instance, namespace, selector, RELEASE_TIMEOUT);
|
|
276
536
|
}
|
|
277
537
|
catch (e) {
|
|
538
|
+
// the next run removes what is left
|
|
539
|
+
this.tidy.delete(namespace);
|
|
278
540
|
context.emit({
|
|
279
541
|
type: 'log',
|
|
280
542
|
task: 'hammerkit',
|
|
@@ -300,8 +562,69 @@ class KubernetesExecutor {
|
|
|
300
562
|
},
|
|
301
563
|
};
|
|
302
564
|
}
|
|
303
|
-
// The
|
|
304
|
-
|
|
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
|
+
}
|
|
626
|
+
// The kept claims of a namespace that expired go at the soonest expiry the server knows
|
|
627
|
+
// of, but those of a scope a run of the caller executes in
|
|
305
628
|
sweepLater(namespace, delay) {
|
|
306
629
|
const at = Date.now() + delay + 1000;
|
|
307
630
|
const pending = this.sweeps.get(namespace);
|
|
@@ -321,6 +644,7 @@ class KubernetesExecutor {
|
|
|
321
644
|
now: new Date(),
|
|
322
645
|
keep: this.keep > 0,
|
|
323
646
|
timeout: RELEASE_TIMEOUT,
|
|
647
|
+
inUse: this.scopesInUse(namespace),
|
|
324
648
|
});
|
|
325
649
|
if (removed.length > 0) {
|
|
326
650
|
this.log?.(`removed ${removed.length} kept claims in ${namespace}`);
|
|
@@ -337,7 +661,7 @@ class KubernetesExecutor {
|
|
|
337
661
|
}
|
|
338
662
|
this.swept = true;
|
|
339
663
|
Promise.resolve()
|
|
340
|
-
.then(() => this.instance.coreApi.listNamespace({ labelSelector: `${MANAGED_LABEL}=true` }))
|
|
664
|
+
.then(() => this.instance.coreApi.listNamespace({ labelSelector: `${MANAGED_LABEL}=true,!${exports.SERVICE_RUN_LABEL}` }))
|
|
341
665
|
.then((namespaces) => {
|
|
342
666
|
for (const namespace of namespaces.items) {
|
|
343
667
|
const name = namespace.metadata?.name;
|
|
@@ -352,11 +676,55 @@ class KubernetesExecutor {
|
|
|
352
676
|
}
|
|
353
677
|
}
|
|
354
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;
|
|
355
723
|
// What may mount a claim a run kept: the runs of the same repository, as they share the
|
|
356
724
|
// cache, and on a sandbox the runs of that sandbox only, whose claim is on the same node or
|
|
357
725
|
// zone as the kept ones. The owner is the namespace's.
|
|
358
726
|
function getVolumeScope(run) {
|
|
359
|
-
if (run.source.type === 'sandbox') {
|
|
727
|
+
if (run.source.type === 'sandbox' && !run.source.check) {
|
|
360
728
|
return `sandbox-${(0, crypto_1.createHash)('sha256').update(run.source.id).digest('hex').slice(0, 24)}`;
|
|
361
729
|
}
|
|
362
730
|
return (0, executor_1.cacheNamespace)(run);
|
|
@@ -370,6 +738,8 @@ class CallerNamespaces {
|
|
|
370
738
|
constructor(instance, backend) {
|
|
371
739
|
this.instance = instance;
|
|
372
740
|
this.backend = backend;
|
|
741
|
+
// the namespaces this process set up, by their uid, and when
|
|
742
|
+
this.applied = new Map();
|
|
373
743
|
}
|
|
374
744
|
name(owner) {
|
|
375
745
|
return getCallerNamespace(this.backend.namespacePrefix, owner);
|
|
@@ -377,9 +747,15 @@ class CallerNamespaces {
|
|
|
377
747
|
// The caller's namespace: made by the server and kept apart from the others. The
|
|
378
748
|
// server binds its rights there, isolates the network of the pods, applies the
|
|
379
749
|
// operator's quota and gives containers without resources default requests, so they
|
|
380
|
-
// are admitted under a quota on compute. All of it is replaced
|
|
381
|
-
// namespace
|
|
382
|
-
|
|
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) {
|
|
383
759
|
const backend = this.backend;
|
|
384
760
|
let existing;
|
|
385
761
|
try {
|
|
@@ -393,17 +769,33 @@ class CallerNamespaces {
|
|
|
393
769
|
if (existing && existing.metadata?.labels?.[MANAGED_LABEL] !== 'true') {
|
|
394
770
|
throw new Error(`the namespace ${namespace} was not made by hammerkit, the server does not run tasks in it`);
|
|
395
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);
|
|
396
788
|
if (!existing) {
|
|
397
789
|
try {
|
|
398
|
-
await this.instance.coreApi.createNamespace({
|
|
790
|
+
existing = await this.instance.coreApi.createNamespace({
|
|
399
791
|
body: {
|
|
400
792
|
metadata: {
|
|
401
793
|
name: namespace,
|
|
402
794
|
labels: {
|
|
403
795
|
[MANAGED_LABEL]: 'true',
|
|
404
796
|
'hammerkit.dev/caller': namespace,
|
|
405
|
-
|
|
406
|
-
|
|
797
|
+
...(serviceRun ? { [exports.SERVICE_RUN_LABEL]: serviceRun.serviceRun } : {}),
|
|
798
|
+
...getPodSecurityLabels(backend),
|
|
407
799
|
},
|
|
408
800
|
},
|
|
409
801
|
},
|
|
@@ -419,7 +811,10 @@ class CallerNamespaces {
|
|
|
419
811
|
const [first, ...policies] = getNamespacePolicies(namespace, backend.deniedEgress, backend.allowedEgress);
|
|
420
812
|
// the API server may refuse what the binding allows for a moment after it was made
|
|
421
813
|
await untilAllowed(() => (0, apply_1.replace)(this.instance, first));
|
|
422
|
-
for (const policy of
|
|
814
|
+
for (const policy of [
|
|
815
|
+
...policies,
|
|
816
|
+
...(serviceRun ? [getServerIngressPolicy(namespace, serviceRun.server, serviceRun.ports)] : []),
|
|
817
|
+
]) {
|
|
423
818
|
await (0, apply_1.replace)(this.instance, policy);
|
|
424
819
|
}
|
|
425
820
|
const metadata = {
|
|
@@ -432,7 +827,7 @@ class CallerNamespaces {
|
|
|
432
827
|
apiVersion: 'v1',
|
|
433
828
|
kind: 'ResourceQuota',
|
|
434
829
|
metadata,
|
|
435
|
-
spec: { hard: backend.quota },
|
|
830
|
+
spec: { hard: serviceRun?.quota ?? backend.quota },
|
|
436
831
|
};
|
|
437
832
|
const defaults = backend.defaultResources;
|
|
438
833
|
const limits = {
|
|
@@ -450,7 +845,7 @@ class CallerNamespaces {
|
|
|
450
845
|
},
|
|
451
846
|
};
|
|
452
847
|
for (const [object, wanted] of [
|
|
453
|
-
[quota, !!backend.quota],
|
|
848
|
+
[quota, !!(serviceRun?.quota ?? backend.quota)],
|
|
454
849
|
[limits, !!defaults],
|
|
455
850
|
]) {
|
|
456
851
|
if (wanted) {
|
|
@@ -465,6 +860,11 @@ class CallerNamespaces {
|
|
|
465
860
|
});
|
|
466
861
|
}
|
|
467
862
|
}
|
|
863
|
+
const uid = existing?.metadata?.uid;
|
|
864
|
+
if (uid) {
|
|
865
|
+
this.applied.set(namespace, { uid, at: started });
|
|
866
|
+
}
|
|
867
|
+
return problem();
|
|
468
868
|
}
|
|
469
869
|
whoAmI() {
|
|
470
870
|
if (!this.subject) {
|
|
@@ -487,7 +887,7 @@ class CallerNamespaces {
|
|
|
487
887
|
exports.CallerNamespaces = CallerNamespaces;
|
|
488
888
|
// The claim of a sandbox as the Jobs of its run mount it: the run's copy of the files is
|
|
489
889
|
// `root`, a source below it is the same path below `subPath` in the claim.
|
|
490
|
-
function getWorkspace(root, claim, namespace) {
|
|
890
|
+
function getWorkspace(root, claim, namespace, runner) {
|
|
491
891
|
if (claim.namespace !== namespace) {
|
|
492
892
|
throw new Error(`the sandbox is in ${claim.namespace}, the run in ${namespace}`);
|
|
493
893
|
}
|
|
@@ -497,6 +897,7 @@ function getWorkspace(root, claim, namespace) {
|
|
|
497
897
|
subPath: claim.subPath,
|
|
498
898
|
label: claim.label,
|
|
499
899
|
...(claim.pod ? { pod: claim.pod } : {}),
|
|
900
|
+
...(runner ? { runner: true } : {}),
|
|
500
901
|
};
|
|
501
902
|
}
|
|
502
903
|
// Everything a run made in the caller's namespace, by the label selector of the run:
|
|
@@ -505,16 +906,21 @@ function getWorkspace(root, claim, namespace) {
|
|
|
505
906
|
// run outlives its policy.
|
|
506
907
|
// Deletes what a run made. The NetworkPolicies go last, once the pods are: a limited
|
|
507
908
|
// pod's own policy is all that limits it.
|
|
508
|
-
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;
|
|
509
913
|
const selector = { namespace, labelSelector };
|
|
510
914
|
await instance.batchApi.deleteCollectionNamespacedJob({ ...selector, propagationPolicy: 'Background' });
|
|
511
915
|
await instance.appsApi.deleteCollectionNamespacedDeployment({ ...selector, propagationPolicy: 'Background' });
|
|
512
916
|
await instance.coreApi.deleteCollectionNamespacedPod({ ...selector, gracePeriodSeconds: 0 });
|
|
513
917
|
await instance.coreApi.deleteCollectionNamespacedService(selector);
|
|
514
918
|
await instance.coreApi.deleteCollectionNamespacedSecret(selector);
|
|
515
|
-
|
|
919
|
+
if (claims) {
|
|
920
|
+
await instance.coreApi.deleteCollectionNamespacedPersistentVolumeClaim(selector);
|
|
921
|
+
}
|
|
516
922
|
// the next run of the caller also uses the same claim names
|
|
517
|
-
await awaitRunObjectsGone(instance, namespace, labelSelector, timeout);
|
|
923
|
+
await awaitRunObjectsGone(instance, namespace, labelSelector, timeout, claims);
|
|
518
924
|
const policies = await instance.objectApi.list('networking.k8s.io/v1', 'NetworkPolicy', namespace, undefined, undefined, undefined, undefined, labelSelector);
|
|
519
925
|
for (const policy of policies.items) {
|
|
520
926
|
// the items of a list do not say their kind
|
|
@@ -543,11 +949,13 @@ async function untilAllowed(fn, attempts = 20) {
|
|
|
543
949
|
}
|
|
544
950
|
}
|
|
545
951
|
// Until the pods and claims of a run are gone: a claim is kept while a pod uses it.
|
|
546
|
-
async function awaitRunObjectsGone(instance, namespace, labelSelector, timeout) {
|
|
952
|
+
async function awaitRunObjectsGone(instance, namespace, labelSelector, timeout, withClaims = true) {
|
|
547
953
|
const deadline = Date.now() + timeout;
|
|
548
954
|
for (;;) {
|
|
549
955
|
const pods = await instance.coreApi.listNamespacedPod({ namespace, labelSelector });
|
|
550
|
-
const claims =
|
|
956
|
+
const claims = withClaims
|
|
957
|
+
? await instance.coreApi.listNamespacedPersistentVolumeClaim({ namespace, labelSelector })
|
|
958
|
+
: { items: [] };
|
|
551
959
|
const left = [...pods.items, ...claims.items].map((item) => `${item.kind ?? ''} ${item.metadata?.name}`.trim());
|
|
552
960
|
if (left.length === 0) {
|
|
553
961
|
return;
|