hammerkit 1.27.0-next.2 → 1.27.0-next.20
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 +9 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +1 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-task.js +5 -3
- package/dist/executer/execution-steps.js +5 -2
- package/dist/executer/scheduler/enqueue-next.js +7 -0
- package/dist/executer/scheduler/state-key.js +42 -5
- 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 +207 -65
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +626 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/network/egress-proxy.js +45 -6
- 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 +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +1 -0
- package/dist/planner/utils/append-work-task.js +15 -2
- 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/utils/task-arguments.js +3 -0
- 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-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +97 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +14 -4
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +64 -3
- package/dist/remote/remote-commands.js +41 -9
- package/dist/remote/remote-work-commands.js +246 -1
- 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 +5 -1
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/reference-parser.js +9 -1
- package/dist/schema/work-scope-parser.js +6 -0
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +56 -5
- package/dist/server/kubernetes-backend.js +303 -64
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +126 -62
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +64 -3
- package/dist/server/run-worker.js +146 -10
- package/dist/server/runs.js +347 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +213 -14
- package/dist/server/server.js +268 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/package.json +1 -1
package/dist/server/sandboxes.js
CHANGED
|
@@ -412,6 +412,14 @@ class SandboxRegistry {
|
|
|
412
412
|
async state(id, signal) {
|
|
413
413
|
return this.options.storage.state(keyOf(this.existing(id)), signal);
|
|
414
414
|
}
|
|
415
|
+
// The files of `tree`, which the sandbox's files must be now, written into `into`: what a
|
|
416
|
+
// check runs on.
|
|
417
|
+
async exportTree(id, tree, into, signal) {
|
|
418
|
+
const sandbox = this.existing(id);
|
|
419
|
+
sandbox.lastUsedAt = this.timestamp();
|
|
420
|
+
await this.save(sandbox);
|
|
421
|
+
await this.options.storage.exportTree(keyOf(sandbox), tree, into, signal);
|
|
422
|
+
}
|
|
415
423
|
// What a run works on, `into` a directory of the run when the storage copies the files;
|
|
416
424
|
// the run is the sandbox's use.
|
|
417
425
|
async attach(id, into) {
|
|
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const log_1 = require("../log");
|
|
6
6
|
const server_config_1 = require("./server-config");
|
|
7
7
|
const server_1 = require("./server");
|
|
8
|
+
const abort_on_signals_1 = require("../utils/abort-on-signals");
|
|
8
9
|
// Runs this machine as a hammerkit server until interrupted. Like `remote`, it
|
|
9
10
|
// only needs its own config file, not a build file.
|
|
10
11
|
function registerServerCommand(program, environment) {
|
|
@@ -13,31 +14,46 @@ function registerServerCommand(program, environment) {
|
|
|
13
14
|
.description('serve this machine to remote runs (experimental)')
|
|
14
15
|
.addOption(new commander_1.Option('-c, --config <file>', 'server config file').env('HAMMERKIT_SERVER_CONFIG'))
|
|
15
16
|
.action(async (options) => {
|
|
17
|
+
// SIGHUP reads the entitlements again, so who may do what changes without a
|
|
18
|
+
// restart; the rest of the config needs one. The server takes it over from the
|
|
19
|
+
// abort of the CLI, which stops other commands on it, before it starts: a SIGHUP
|
|
20
|
+
// while it reads its config and starts waits for it. Windows sends SIGHUP when the
|
|
21
|
+
// console closes, which stops the server there too.
|
|
22
|
+
let server;
|
|
23
|
+
let reloadWhenStarted = false;
|
|
24
|
+
const reload = async (running) => {
|
|
25
|
+
try {
|
|
26
|
+
const result = running.reload(await (0, server_config_1.readServerConfig)(environment, options.config));
|
|
27
|
+
if (result.restart.length > 0) {
|
|
28
|
+
environment.stdout.write(`changed but not reloaded, restart for: ${result.restart.join(', ')}\n`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
environment.stdout.write(`entitlements were not reloaded: ${(0, log_1.getErrorMessage)(e)}\n`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const releaseReload = process.platform !== 'win32'
|
|
36
|
+
? (0, abort_on_signals_1.takeOverSignal)(process, 'SIGHUP', () => {
|
|
37
|
+
if (server) {
|
|
38
|
+
void reload(server);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
reloadWhenStarted = true;
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
: () => undefined;
|
|
16
45
|
try {
|
|
17
46
|
if (!options.config) {
|
|
18
47
|
throw new Error('pass the server config with --config or HAMMERKIT_SERVER_CONFIG');
|
|
19
48
|
}
|
|
20
49
|
const config = await (0, server_config_1.readServerConfig)(environment, options.config);
|
|
21
|
-
const
|
|
50
|
+
const running = await (0, server_1.startServer)(config, environment, {
|
|
22
51
|
log: (line) => environment.stdout.write(`${line}\n`),
|
|
23
52
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const result = server.reload(await (0, server_config_1.readServerConfig)(environment, options.config));
|
|
30
|
-
if (result.restart.length > 0) {
|
|
31
|
-
environment.stdout.write(`changed but not reloaded, restart for: ${result.restart.join(', ')}\n`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch (e) {
|
|
35
|
-
environment.stdout.write(`entitlements were not reloaded: ${(0, log_1.getErrorMessage)(e)}\n`);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
const onReload = () => void reload();
|
|
39
|
-
if (process.platform !== 'win32') {
|
|
40
|
-
process.on('SIGHUP', onReload);
|
|
53
|
+
server = running;
|
|
54
|
+
environment.stdout.write(`hammerkit server listening on ${running.url}\n`);
|
|
55
|
+
if (reloadWhenStarted) {
|
|
56
|
+
void reload(running);
|
|
41
57
|
}
|
|
42
58
|
await new Promise((resolve) => {
|
|
43
59
|
if (environment.abortCtrl.signal.aborted) {
|
|
@@ -45,8 +61,8 @@ function registerServerCommand(program, environment) {
|
|
|
45
61
|
}
|
|
46
62
|
environment.abortCtrl.signal.addEventListener('abort', () => resolve(), { once: true });
|
|
47
63
|
});
|
|
48
|
-
|
|
49
|
-
await
|
|
64
|
+
releaseReload();
|
|
65
|
+
await running.close();
|
|
50
66
|
environment.stdout.write('hammerkit server stopped\n');
|
|
51
67
|
}
|
|
52
68
|
catch (e) {
|
|
@@ -55,6 +71,9 @@ function registerServerCommand(program, environment) {
|
|
|
55
71
|
}
|
|
56
72
|
program.error(`Server was not successful: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
|
|
57
73
|
}
|
|
74
|
+
finally {
|
|
75
|
+
releaseReload();
|
|
76
|
+
}
|
|
58
77
|
});
|
|
59
78
|
}
|
|
60
79
|
exports.registerServerCommand = registerServerCommand;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.detectPlatform = exports.getSecretCatalog = exports.readServerConfig = exports.
|
|
3
|
+
exports.detectPlatform = exports.getSecretCatalog = exports.readServerConfig = exports.getServerAccounts = exports.getSandboxRuntimes = exports.serverConfigSchema = exports.sourceSchema = exports.entitlementSchema = exports.endpointGrantSchema = exports.endpointLevelSchema = exports.sandboxGrantSchema = exports.sandboxBranchTypeSchema = exports.buildsLevelSchema = exports.accountLevelSchema = exports.getMaxVolumeSize = exports.DEFAULT_MAX_VOLUME_SIZE = exports.DEFAULT_DENIED_EGRESS = exports.kubernetesPoolSchema = exports.accountSchema = exports.issuerSchema = exports.serverPlatformSchema = void 0;
|
|
4
4
|
const net_1 = require("net");
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const yaml_1 = require("yaml");
|
|
@@ -188,8 +188,12 @@ const kubernetesSandboxesSchema = zod_1.z
|
|
|
188
188
|
// when left out. With a class of their own, a ResourceQuota on that class in each
|
|
189
189
|
// caller's namespace holds `sandboxes.limits`.
|
|
190
190
|
storageClass: zod_1.z.string().min(1).optional(),
|
|
191
|
-
// how long the pod of a sandbox's files stays after the last request
|
|
191
|
+
// how long the pod of a sandbox's files stays after the last request; the runner of the
|
|
192
|
+
// sandbox goes with it
|
|
192
193
|
idle: duration.default('1m'),
|
|
194
|
+
// exec the commands of a task shaped like a shell (it mounts the sandbox only, has no
|
|
195
|
+
// needs, outputs or secrets) into one pod per sandbox, in place of a Job each
|
|
196
|
+
runner: zod_1.z.boolean().default(true),
|
|
193
197
|
// of the pod of a sandbox's files, which runs git and ripgrep over all of them
|
|
194
198
|
resources: zod_1.z
|
|
195
199
|
.object({ requests: quantitiesSchema.optional(), limits: quantitiesSchema.optional() })
|
|
@@ -238,8 +242,22 @@ const kubernetesBackendSchema = zod_1.z
|
|
|
238
242
|
.min(1)
|
|
239
243
|
.default([{ name: 'default', platform: { os: 'linux', arch: 'amd64' }, nodeSelector: {}, tolerations: [] }]),
|
|
240
244
|
// per secret provider, the image its command runs in: secrets are read in the
|
|
241
|
-
// Job of the task, not by the server
|
|
242
|
-
|
|
245
|
+
// Job of the task, not by the server. `allow` are the hosts its command reaches, such
|
|
246
|
+
// as secretmanager.googleapis.com: the egress proxy of a task with a limited network
|
|
247
|
+
// lets the init containers that read its secrets reach them, and nothing else does
|
|
248
|
+
providers: zod_1.z
|
|
249
|
+
.record(zod_1.z
|
|
250
|
+
.object({
|
|
251
|
+
image: zod_1.z.string().min(1),
|
|
252
|
+
allow: zod_1.z
|
|
253
|
+
.array(zod_1.z
|
|
254
|
+
.string()
|
|
255
|
+
.refine((host) => (0, network_schema_1.isAllowEntry)(host) && !host.includes(':'), 'expected a host name such as secretmanager.googleapis.com or *.example.com'))
|
|
256
|
+
.min(1)
|
|
257
|
+
.optional(),
|
|
258
|
+
})
|
|
259
|
+
.strict())
|
|
260
|
+
.default({}),
|
|
243
261
|
// the ResourceQuota of every caller's namespace, such as `requests.cpu: '8'`
|
|
244
262
|
quota: zod_1.z.record(zod_1.z.union([zod_1.z.string().min(1), zod_1.z.number()]).transform(String)).optional(),
|
|
245
263
|
// what a container without `resources` requests (a LimitRange), so it is
|
|
@@ -309,6 +327,11 @@ exports.sandboxGrantSchema = zod_1.z
|
|
|
309
327
|
.refine((branches) => Object.keys(branches).length > 0, 'name the branches a sandbox may start from'),
|
|
310
328
|
})
|
|
311
329
|
.strict();
|
|
330
|
+
// `open`: open the endpoints of the service runs of a repository, whoever started them, and
|
|
331
|
+
// extend those runs; `publish`: also name endpoints in one's own service runs.
|
|
332
|
+
exports.endpointLevelSchema = zod_1.z.enum(['open', 'publish']);
|
|
333
|
+
// One repository (`host/path` pattern) whose endpoints an entitlement opens or publishes.
|
|
334
|
+
exports.endpointGrantSchema = zod_1.z.object({ repo: repoPatternSchema, level: exports.endpointLevelSchema }).strict();
|
|
312
335
|
exports.entitlementSchema = zod_1.z
|
|
313
336
|
.object({
|
|
314
337
|
...whoShape,
|
|
@@ -318,19 +341,33 @@ exports.entitlementSchema = zod_1.z
|
|
|
318
341
|
// the repositories (`host/path` patterns) the caller may run from a git ref
|
|
319
342
|
repos: zod_1.z.array(repoPatternSchema).default([]),
|
|
320
343
|
builds: exports.buildsLevelSchema.optional(),
|
|
344
|
+
// `read`: may look up which tasks runs passed for files of the `repos` (remote verify),
|
|
345
|
+
// and nothing more; a builds level includes it
|
|
346
|
+
checks: zod_1.z.literal('read').optional(),
|
|
321
347
|
// the repositories the caller may keep sandboxes of, checkouts on the server that runs
|
|
322
348
|
// work on: a list of its own, the `repos` above are for runs from a ref
|
|
323
349
|
sandboxes: zod_1.z.array(exports.sandboxGrantSchema).optional(),
|
|
350
|
+
// the repositories whose service runs' endpoints the caller may open, or publish
|
|
351
|
+
endpoints: zod_1.z.array(exports.endpointGrantSchema).optional(),
|
|
324
352
|
})
|
|
325
353
|
.strict()
|
|
326
354
|
.superRefine((entitlement, ctx) => {
|
|
327
355
|
checkWho(entitlement, ctx);
|
|
328
356
|
if (Object.keys(entitlement.accounts).length === 0 &&
|
|
329
357
|
entitlement.builds === undefined &&
|
|
330
|
-
|
|
358
|
+
entitlement.checks === undefined &&
|
|
359
|
+
!entitlement.sandboxes?.length &&
|
|
360
|
+
!entitlement.endpoints?.length) {
|
|
361
|
+
ctx.addIssue({
|
|
362
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
363
|
+
message: 'name accounts to use or request, a builds level, checks, sandboxes or endpoints',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (entitlement.checks && entitlement.repos.length === 0) {
|
|
331
367
|
ctx.addIssue({
|
|
332
368
|
code: zod_1.z.ZodIssueCode.custom,
|
|
333
|
-
|
|
369
|
+
path: ['repos'],
|
|
370
|
+
message: 'checks are read for repositories: name them in repos',
|
|
334
371
|
});
|
|
335
372
|
}
|
|
336
373
|
});
|
|
@@ -451,6 +488,40 @@ exports.serverConfigSchema = zod_1.z
|
|
|
451
488
|
// the most any task or service of a run reaches besides its needs: none, open
|
|
452
489
|
// or { allow: [hosts] }. A version that asks for more is refused at publish.
|
|
453
490
|
network: network_schema_1.networkSchema.default('open'),
|
|
491
|
+
// the most a task or service that reads a secret, or a variable of the server
|
|
492
|
+
// other than PATH, HOME and CI, reaches, under `network`: so a tool it runs can't
|
|
493
|
+
// send the value anywhere. Checked like `network`.
|
|
494
|
+
secretNetwork: network_schema_1.networkSchema.default('open'),
|
|
495
|
+
// which tasks take arguments: `any` but those that say `args: false`, or
|
|
496
|
+
// `declared`, only those that say `args: true`
|
|
497
|
+
args: zod_1.z.enum(['any', 'declared']).default('any'),
|
|
498
|
+
// runs of services (`services` in a run), kept up until their ttl ends
|
|
499
|
+
up: zod_1.z
|
|
500
|
+
.object({
|
|
501
|
+
// how long the services stay once ready, when the run names no ttl; then they are removed
|
|
502
|
+
ttl: duration.default('8h'),
|
|
503
|
+
// how long the services stay up without a request to an endpoint before they sleep,
|
|
504
|
+
// when the run names none: the next request wakes them. Unset, they never sleep.
|
|
505
|
+
sleepTtl: duration.optional(),
|
|
506
|
+
// the furthest a run, or an extend, sets the end from now
|
|
507
|
+
maxTtl: duration.default('72h'),
|
|
508
|
+
limits: zod_1.z
|
|
509
|
+
.object({
|
|
510
|
+
// service runs one caller has starting or up at a time; one more is 429
|
|
511
|
+
up: zod_1.z.number().int().min(1).default(3),
|
|
512
|
+
})
|
|
513
|
+
.strict()
|
|
514
|
+
.default({}),
|
|
515
|
+
// service runs starting or up on the whole server; one more is 503
|
|
516
|
+
total: zod_1.z.number().int().min(1).default(20),
|
|
517
|
+
// on a Kubernetes backend, the ResourceQuota of the namespace of each service
|
|
518
|
+
// run; backend.quota when left out
|
|
519
|
+
quota: zod_1.z.record(zod_1.z.union([zod_1.z.string().min(1), zod_1.z.number()]).transform(String)).optional(),
|
|
520
|
+
})
|
|
521
|
+
.strict()
|
|
522
|
+
.default({}),
|
|
523
|
+
// how long a passed check stays for `remote verify` to find (see checks.ts)
|
|
524
|
+
checksKeep: duration.default('30d'),
|
|
454
525
|
})
|
|
455
526
|
.strict()
|
|
456
527
|
.default({}),
|
|
@@ -488,6 +559,45 @@ exports.serverConfigSchema = zod_1.z
|
|
|
488
559
|
})
|
|
489
560
|
.strict()
|
|
490
561
|
.default({}),
|
|
562
|
+
// ports of service runs published at hosts of their own below `domain`, behind the
|
|
563
|
+
// server's sign-in. Absent, a run naming endpoints is refused.
|
|
564
|
+
endpoints: zod_1.z
|
|
565
|
+
.object({
|
|
566
|
+
// `run.example.com`: an endpoint is https://<label>.run.example.com, all of them
|
|
567
|
+
// pointing at this server with one wildcard DNS record
|
|
568
|
+
domain: zod_1.z
|
|
569
|
+
.string()
|
|
570
|
+
.regex(/^(?=.{1,200}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, 'expected a domain name in lower case, such as run.example.com'),
|
|
571
|
+
// a wildcard certificate for *.<domain>, served for the hosts of endpoints; left out
|
|
572
|
+
// behind a TLS proxy, which holds it
|
|
573
|
+
tls: zod_1.z
|
|
574
|
+
.object({ cert: zod_1.z.string().min(1), key: zod_1.z.string().min(1) })
|
|
575
|
+
.strict()
|
|
576
|
+
.optional(),
|
|
577
|
+
// how a person signs in with a browser: the authorization code flow of an issuer of
|
|
578
|
+
// `issuers`, with a client of its own; without it only tokens open endpoints
|
|
579
|
+
login: zod_1.z
|
|
580
|
+
.object({
|
|
581
|
+
issuer: zod_1.z.string().min(1),
|
|
582
|
+
clientId: zod_1.z.string().min(1),
|
|
583
|
+
// for providers whose web clients need one (Google): read as an account no
|
|
584
|
+
// entitlement may name, as the secret of a source is
|
|
585
|
+
clientSecret: zod_1.z
|
|
586
|
+
.object({
|
|
587
|
+
from: zod_1.z.string().regex(/^[A-Za-z][A-Za-z0-9_-]*:.+$/, 'expected provider:reference'),
|
|
588
|
+
account: accountNameSchema,
|
|
589
|
+
})
|
|
590
|
+
.strict()
|
|
591
|
+
.optional(),
|
|
592
|
+
scope: zod_1.z.string().min(1).default('openid'),
|
|
593
|
+
})
|
|
594
|
+
.strict()
|
|
595
|
+
.optional(),
|
|
596
|
+
// how long a sign-in at one endpoint lasts, at most until its run ends
|
|
597
|
+
session: duration.default('8h'),
|
|
598
|
+
})
|
|
599
|
+
.strict()
|
|
600
|
+
.optional(),
|
|
491
601
|
backend: zod_1.z.discriminatedUnion('type', [hostBackendSchema, kubernetesBackendSchema]).default({ type: 'host' }),
|
|
492
602
|
})
|
|
493
603
|
.strict()
|
|
@@ -555,7 +665,7 @@ exports.serverConfigSchema = zod_1.z
|
|
|
555
665
|
});
|
|
556
666
|
}
|
|
557
667
|
});
|
|
558
|
-
const readers =
|
|
668
|
+
const readers = getServerAccounts(config);
|
|
559
669
|
config.entitlements.forEach((entitlement, index) => {
|
|
560
670
|
for (const account of Object.keys(entitlement.accounts)) {
|
|
561
671
|
if (readers.includes(account)) {
|
|
@@ -586,7 +696,10 @@ exports.serverConfigSchema = zod_1.z
|
|
|
586
696
|
message: `under runs.network, ${shared}`,
|
|
587
697
|
});
|
|
588
698
|
}
|
|
589
|
-
|
|
699
|
+
// on a cluster a local task runs in a pod of a pool's tool image, which can be limited
|
|
700
|
+
if (config.sandboxes.runtimes?.includes('local') &&
|
|
701
|
+
config.sandboxes.network !== 'open' &&
|
|
702
|
+
config.backend.type === 'host') {
|
|
590
703
|
ctx.addIssue({
|
|
591
704
|
code: zod_1.z.ZodIssueCode.custom,
|
|
592
705
|
path: ['sandboxes', 'network'],
|
|
@@ -635,7 +748,7 @@ exports.serverConfigSchema = zod_1.z
|
|
|
635
748
|
});
|
|
636
749
|
// the secrets of tasks are read in their Job, with the image of the provider; the
|
|
637
750
|
// ones of the sources are read by the server, which checks the code out itself
|
|
638
|
-
const readers =
|
|
751
|
+
const readers = getServerAccounts(config);
|
|
639
752
|
for (const [name, account] of Object.entries(config.accounts)) {
|
|
640
753
|
if (readers.includes(name)) {
|
|
641
754
|
continue;
|
|
@@ -697,6 +810,13 @@ exports.serverConfigSchema = zod_1.z
|
|
|
697
810
|
message: 'runs.network limits every task with a NetworkPolicy, set networkPolicy: enforced once the network plugin of the cluster enforces them',
|
|
698
811
|
});
|
|
699
812
|
}
|
|
813
|
+
if (config.runs.secretNetwork !== 'open' && backend.networkPolicy !== 'enforced') {
|
|
814
|
+
ctx.addIssue({
|
|
815
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
816
|
+
path: ['backend', 'networkPolicy'],
|
|
817
|
+
message: 'runs.secretNetwork limits the tasks that read secrets with a NetworkPolicy, set networkPolicy: enforced once the network plugin of the cluster enforces them',
|
|
818
|
+
});
|
|
819
|
+
}
|
|
700
820
|
if (config.sandboxes.network !== 'open' &&
|
|
701
821
|
backend.networkPolicy !== 'enforced' &&
|
|
702
822
|
config.entitlements.some((entitlement) => entitlement.sandboxes?.length)) {
|
|
@@ -707,13 +827,85 @@ exports.serverConfigSchema = zod_1.z
|
|
|
707
827
|
});
|
|
708
828
|
}
|
|
709
829
|
}
|
|
710
|
-
if (config.runs.network !== 'open' && config.runtimes.includes('local')) {
|
|
830
|
+
if (config.runs.network !== 'open' && config.runtimes.includes('local') && backend.type === 'host') {
|
|
711
831
|
ctx.addIssue({
|
|
712
832
|
code: zod_1.z.ZodIssueCode.custom,
|
|
713
833
|
path: ['runs', 'network'],
|
|
714
834
|
message: 'the network of a local task can not be limited, allow only the docker runtime with runs.network',
|
|
715
835
|
});
|
|
716
836
|
}
|
|
837
|
+
if ((0, units_1.parseDuration)(config.runs.up.ttl) > (0, units_1.parseDuration)(config.runs.up.maxTtl)) {
|
|
838
|
+
ctx.addIssue({
|
|
839
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
840
|
+
path: ['runs', 'up', 'ttl'],
|
|
841
|
+
message: `a ttl of ${config.runs.up.ttl} is more than maxTtl of ${config.runs.up.maxTtl}`,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
const endpoints = config.endpoints;
|
|
845
|
+
if (endpoints) {
|
|
846
|
+
if (endpoints.tls && config.behindTlsProxy) {
|
|
847
|
+
ctx.addIssue({
|
|
848
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
849
|
+
path: ['endpoints', 'tls'],
|
|
850
|
+
message: 'behind a TLS proxy the proxy holds the certificate of the endpoints, leave out endpoints.tls',
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
if (endpoints.tls && !config.tls) {
|
|
854
|
+
ctx.addIssue({
|
|
855
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
856
|
+
path: ['endpoints', 'tls'],
|
|
857
|
+
message: 'the certificate of the endpoints is served next to the one of the server, set tls as well',
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (!endpoints.tls && !config.behindTlsProxy && (config.tls || !LOOPBACK_HOSTS.includes(config.listen.host))) {
|
|
861
|
+
ctx.addIssue({
|
|
862
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
863
|
+
path: ['endpoints', 'tls'],
|
|
864
|
+
message: `name the certificate of *.${endpoints.domain} in endpoints.tls, or set behindTlsProxy: true when a proxy in front holds it`,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
const login = endpoints.login;
|
|
868
|
+
if (login && !config.issuers.some((issuer) => issuer.issuer === login.issuer)) {
|
|
869
|
+
ctx.addIssue({
|
|
870
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
871
|
+
path: ['endpoints', 'login', 'issuer'],
|
|
872
|
+
message: `issuer ${login.issuer} is not one of the accepted issuers`,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
if (login?.clientSecret) {
|
|
876
|
+
const { from, account } = login.clientSecret;
|
|
877
|
+
const kind = from.substring(0, from.indexOf(':'));
|
|
878
|
+
const where = ['endpoints', 'login', 'clientSecret'];
|
|
879
|
+
if (!config.accounts[account]) {
|
|
880
|
+
ctx.addIssue({
|
|
881
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
882
|
+
path: [...where, 'account'],
|
|
883
|
+
message: `unknown account ${account}`,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
else if (!config.accounts[account].providers[kind]) {
|
|
887
|
+
ctx.addIssue({
|
|
888
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
889
|
+
path: [...where, 'from'],
|
|
890
|
+
message: `account ${account} has no credentials for the provider ${kind}`,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
if (!config.secretProviders[kind]) {
|
|
894
|
+
ctx.addIssue({
|
|
895
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
896
|
+
path: [...where, 'from'],
|
|
897
|
+
message: `secret provider ${kind} is not declared in secretProviders`,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (backend.type === 'kubernetes' && backend.kubeconfig) {
|
|
902
|
+
ctx.addIssue({
|
|
903
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
904
|
+
path: ['endpoints'],
|
|
905
|
+
message: 'endpoints reach the services of runs at their Service in the cluster, run the server in the cluster rather than with a kubeconfig',
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
717
909
|
if (!config.tls && !config.behindTlsProxy && !LOOPBACK_HOSTS.includes(config.listen.host)) {
|
|
718
910
|
ctx.addIssue({
|
|
719
911
|
code: zod_1.z.ZodIssueCode.custom,
|
|
@@ -729,11 +921,18 @@ function getSandboxRuntimes(config) {
|
|
|
729
921
|
return config.sandboxes.runtimes ?? config.runtimes.filter((runtime) => runtime === 'docker');
|
|
730
922
|
}
|
|
731
923
|
exports.getSandboxRuntimes = getSandboxRuntimes;
|
|
732
|
-
// The accounts
|
|
733
|
-
|
|
734
|
-
|
|
924
|
+
// The accounts only the server reads secrets as: those that read the repositories of
|
|
925
|
+
// `sources`, and the one of the client secret of `endpoints.login`. No caller uses them and
|
|
926
|
+
// no build file names them.
|
|
927
|
+
function getServerAccounts(config) {
|
|
928
|
+
return [
|
|
929
|
+
...new Set([
|
|
930
|
+
...config.sources.flatMap((source) => (source.secret ? [source.secret.account] : [])),
|
|
931
|
+
...(config.endpoints?.login?.clientSecret ? [config.endpoints.login.clientSecret.account] : []),
|
|
932
|
+
]),
|
|
933
|
+
];
|
|
735
934
|
}
|
|
736
|
-
exports.
|
|
935
|
+
exports.getServerAccounts = getServerAccounts;
|
|
737
936
|
async function readServerConfig(environment, path) {
|
|
738
937
|
if (!(await environment.file.exists(path))) {
|
|
739
938
|
throw new Error(`unable to read ${path}: no such file`);
|