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,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sandboxPodMain = exports.decodePodError = exports.encodeEditOps = exports.INCOMING_REF = exports.INCOMING = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
4
5
|
const promises_1 = require("fs/promises");
|
|
5
6
|
const path_1 = require("path");
|
|
6
7
|
const url_1 = require("url");
|
|
7
8
|
const git_checkout_1 = require("./git-checkout");
|
|
8
9
|
const sandbox_files_1 = require("./sandbox-files");
|
|
9
10
|
const sandbox_storage_1 = require("./sandbox-storage");
|
|
11
|
+
const tar_1 = require("../utils/tar");
|
|
10
12
|
// On a cluster a sandbox is a claim, and what the server does with its files runs in a
|
|
11
13
|
// short pod that mounts it (see KubernetesSandboxStorage): `hammerkit server-sandbox
|
|
12
14
|
// <directory>` reads one request on its standard input, does it with the code of a host
|
|
@@ -91,6 +93,32 @@ async function answer(storage, directory, request, out) {
|
|
|
91
93
|
}
|
|
92
94
|
return send({ files: plan.files, bytes: plan.bytes });
|
|
93
95
|
}
|
|
96
|
+
case 'archive': {
|
|
97
|
+
// written beside the sandbox on the claim, sent, and removed
|
|
98
|
+
const into = (0, path_1.join)(directory, `${request.id}-archive-${(0, crypto_1.randomBytes)(6).toString('hex')}`);
|
|
99
|
+
try {
|
|
100
|
+
await storage.exportTree(key, request.tree, into, AbortSignal.timeout(10 * 60000));
|
|
101
|
+
await write(out, JSON.stringify({ ok: true, result: null, stream: true }) + '\n');
|
|
102
|
+
const pack = (0, tar_1.packArchive)({ cwd: into, portable: true }, ['.']);
|
|
103
|
+
try {
|
|
104
|
+
for await (const chunk of pack) {
|
|
105
|
+
await write(out, chunk);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
throw new AnswerStarted(e instanceof Error ? e.message : String(e));
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
// a reader that went away (over its size limit) leaves the pack reading files
|
|
113
|
+
// that are removed next: what fails then is no answer of anybody's
|
|
114
|
+
pack.on('error', () => undefined);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await (0, promises_1.rm)(into, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
94
122
|
case 'raw': {
|
|
95
123
|
const raw = await files().raw(request.path);
|
|
96
124
|
const header = {
|
|
@@ -276,6 +276,29 @@ class HostSandboxStorage {
|
|
|
276
276
|
// files change, and what it wrote to the git data is pruned again. Files a task made
|
|
277
277
|
// unreadable are made readable first: git would leave them out and call the sandbox
|
|
278
278
|
// clean.
|
|
279
|
+
async exportTree({ id }, tree, into, signal) {
|
|
280
|
+
if (!/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(tree)) {
|
|
281
|
+
throw new Error(`${JSON.stringify(tree.slice(0, 80))} is not a git tree`);
|
|
282
|
+
}
|
|
283
|
+
return this.withIndex(id, signal, async (run) => {
|
|
284
|
+
const now = (await run(['write-tree'], 'read')).trim();
|
|
285
|
+
if (now !== tree) {
|
|
286
|
+
throw new Error(`the files of the sandbox ${id} are ${now} now, not ${tree}`);
|
|
287
|
+
}
|
|
288
|
+
// an index of its own holds the tree alone, whatever the sandbox's index says of its
|
|
289
|
+
// entries; the files are written as git has them, through no filter of the server's
|
|
290
|
+
const index = (0, path_1.join)(this.root(id), `export-${(0, crypto_1.randomBytes)(6).toString('hex')}`);
|
|
291
|
+
await (0, promises_1.mkdir)(into, { recursive: true });
|
|
292
|
+
try {
|
|
293
|
+
const env = this.readEnv({ GIT_INDEX_FILE: index });
|
|
294
|
+
await this.git(id, ['read-tree', tree], env, signal, 'read');
|
|
295
|
+
await this.git(id, ['checkout-index', '--all', '--force', `--prefix=${(0, path_1.join)(into, '/')}`], env, signal, 'read');
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
await (0, promises_1.rm)(index, { force: true });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
279
302
|
async state({ id }, signal = AbortSignal.timeout(5 * 60 * 1000)) {
|
|
280
303
|
return this.withIndex(id, signal, async (run) => {
|
|
281
304
|
const tree = (await run(['write-tree'], 'read')).trim();
|
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) {
|
|
@@ -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");
|
|
@@ -8,6 +8,7 @@ const log_1 = require("../log");
|
|
|
8
8
|
const units_1 = require("../utils/units");
|
|
9
9
|
const version_1 = require("../version");
|
|
10
10
|
const network_schema_1 = require("../schema/network-schema");
|
|
11
|
+
const capabilities_schema_1 = require("../schema/capabilities-schema");
|
|
11
12
|
const build_file_secret_provider_schema_1 = require("../schema/build-file-secret-provider-schema");
|
|
12
13
|
const repo_1 = require("./repo");
|
|
13
14
|
const work_network_1 = require("../planner/work-network");
|
|
@@ -117,12 +118,24 @@ exports.accountSchema = zod_1.z
|
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
});
|
|
121
|
+
// The capabilities (`capAdd`) a task of a run may add, and the sandboxed runtime its
|
|
122
|
+
// container then runs in, so they hold in the sandbox's own kernel and never in the host's
|
|
123
|
+
const capabilitiesAllowSchema = zod_1.z
|
|
124
|
+
.array(zod_1.z.enum(capabilities_schema_1.LINUX_CAPABILITIES))
|
|
125
|
+
.min(1)
|
|
126
|
+
.refine((names) => new Set(names).size === names.length, 'a capability is listed twice');
|
|
120
127
|
// The tasks of runs execute on this machine, with its docker daemon and local tools.
|
|
121
128
|
const hostBackendSchema = zod_1.z
|
|
122
129
|
.object({
|
|
123
130
|
type: zod_1.z.literal('host'),
|
|
124
131
|
// what this machine offers; detected when left out
|
|
125
132
|
platform: exports.serverPlatformSchema.optional(),
|
|
133
|
+
// `runtime` is a runtime of the docker daemon (`runtimes` in its daemon.json), such as
|
|
134
|
+
// runsc of gVisor
|
|
135
|
+
capabilities: zod_1.z
|
|
136
|
+
.object({ allow: capabilitiesAllowSchema, runtime: zod_1.z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/) })
|
|
137
|
+
.strict()
|
|
138
|
+
.optional(),
|
|
126
139
|
})
|
|
127
140
|
.strict();
|
|
128
141
|
const tolerationSchema = zod_1.z
|
|
@@ -188,8 +201,12 @@ const kubernetesSandboxesSchema = zod_1.z
|
|
|
188
201
|
// when left out. With a class of their own, a ResourceQuota on that class in each
|
|
189
202
|
// caller's namespace holds `sandboxes.limits`.
|
|
190
203
|
storageClass: zod_1.z.string().min(1).optional(),
|
|
191
|
-
// how long the pod of a sandbox's files stays after the last request
|
|
204
|
+
// how long the pod of a sandbox's files stays after the last request; the runner of the
|
|
205
|
+
// sandbox goes with it
|
|
192
206
|
idle: duration.default('1m'),
|
|
207
|
+
// exec the commands of a task shaped like a shell (it mounts the sandbox only, has no
|
|
208
|
+
// needs, outputs or secrets) into one pod per sandbox, in place of a Job each
|
|
209
|
+
runner: zod_1.z.boolean().default(true),
|
|
193
210
|
// of the pod of a sandbox's files, which runs git and ripgrep over all of them
|
|
194
211
|
resources: zod_1.z
|
|
195
212
|
.object({ requests: quantitiesSchema.optional(), limits: quantitiesSchema.optional() })
|
|
@@ -278,6 +295,9 @@ const kubernetesBackendSchema = zod_1.z
|
|
|
278
295
|
// build file's environment does: hammerkit can't tell from the API, and without it a
|
|
279
296
|
// task with a limited network fails rather than runs open
|
|
280
297
|
networkPolicy: zod_1.z.literal('enforced').optional(),
|
|
298
|
+
// `runtimeClass` is a RuntimeClass of a sandbox, such as gvisor; the pods of tasks that
|
|
299
|
+
// add capabilities run in it
|
|
300
|
+
capabilities: zod_1.z.object({ allow: capabilitiesAllowSchema, runtimeClass: kubernetesNameSchema }).strict().optional(),
|
|
281
301
|
})
|
|
282
302
|
.strict();
|
|
283
303
|
// the most a task's claim may request on a server whose operator states no maxVolumeSize
|
|
@@ -323,6 +343,11 @@ exports.sandboxGrantSchema = zod_1.z
|
|
|
323
343
|
.refine((branches) => Object.keys(branches).length > 0, 'name the branches a sandbox may start from'),
|
|
324
344
|
})
|
|
325
345
|
.strict();
|
|
346
|
+
// `open`: open the endpoints of the service runs of a repository, whoever started them, and
|
|
347
|
+
// extend those runs; `publish`: also name endpoints in one's own service runs.
|
|
348
|
+
exports.endpointLevelSchema = zod_1.z.enum(['open', 'publish']);
|
|
349
|
+
// One repository (`host/path` pattern) whose endpoints an entitlement opens or publishes.
|
|
350
|
+
exports.endpointGrantSchema = zod_1.z.object({ repo: repoPatternSchema, level: exports.endpointLevelSchema }).strict();
|
|
326
351
|
exports.entitlementSchema = zod_1.z
|
|
327
352
|
.object({
|
|
328
353
|
...whoShape,
|
|
@@ -332,19 +357,38 @@ exports.entitlementSchema = zod_1.z
|
|
|
332
357
|
// the repositories (`host/path` patterns) the caller may run from a git ref
|
|
333
358
|
repos: zod_1.z.array(repoPatternSchema).default([]),
|
|
334
359
|
builds: exports.buildsLevelSchema.optional(),
|
|
360
|
+
// `read`: may look up which tasks runs passed for files of the `repos` (remote verify),
|
|
361
|
+
// and nothing more; a builds level includes it
|
|
362
|
+
checks: zod_1.z.literal('read').optional(),
|
|
335
363
|
// the repositories the caller may keep sandboxes of, checkouts on the server that runs
|
|
336
364
|
// work on: a list of its own, the `repos` above are for runs from a ref
|
|
337
365
|
sandboxes: zod_1.z.array(exports.sandboxGrantSchema).optional(),
|
|
366
|
+
// the repositories whose service runs' endpoints the caller may open, or publish
|
|
367
|
+
endpoints: zod_1.z.array(exports.endpointGrantSchema).optional(),
|
|
368
|
+
// sees every run of the server in full, who started it, its tasks and its log, in
|
|
369
|
+
// `remote status`, `remote runs --all` and the dashboard: for operators, it stops,
|
|
370
|
+
// extends and approves nothing
|
|
371
|
+
observe: zod_1.z.literal(true).optional(),
|
|
338
372
|
})
|
|
339
373
|
.strict()
|
|
340
374
|
.superRefine((entitlement, ctx) => {
|
|
341
375
|
checkWho(entitlement, ctx);
|
|
342
376
|
if (Object.keys(entitlement.accounts).length === 0 &&
|
|
343
377
|
entitlement.builds === undefined &&
|
|
344
|
-
|
|
378
|
+
entitlement.checks === undefined &&
|
|
379
|
+
!entitlement.sandboxes?.length &&
|
|
380
|
+
!entitlement.endpoints?.length &&
|
|
381
|
+
!entitlement.observe) {
|
|
345
382
|
ctx.addIssue({
|
|
346
383
|
code: zod_1.z.ZodIssueCode.custom,
|
|
347
|
-
message: 'name accounts to use or request, a builds level or
|
|
384
|
+
message: 'name accounts to use or request, a builds level, checks, sandboxes, endpoints or observe',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (entitlement.checks && entitlement.repos.length === 0) {
|
|
388
|
+
ctx.addIssue({
|
|
389
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
390
|
+
path: ['repos'],
|
|
391
|
+
message: 'checks are read for repositories: name them in repos',
|
|
348
392
|
});
|
|
349
393
|
}
|
|
350
394
|
});
|
|
@@ -429,6 +473,10 @@ exports.serverConfigSchema = zod_1.z
|
|
|
429
473
|
maxConcurrent: zod_1.z.number().int().min(1).default(4),
|
|
430
474
|
// finished runs that are kept for their state and logs
|
|
431
475
|
keep: zod_1.z.number().int().min(1).default(200),
|
|
476
|
+
// who sees the runs of others: `everyone` with an entitlement sees who runs what,
|
|
477
|
+
// where it stands and how long it took, never its arguments, error or log;
|
|
478
|
+
// `involved` only sees the runs they decide about or may open the endpoints of
|
|
479
|
+
activity: zod_1.z.enum(['everyone', 'involved']).default('everyone'),
|
|
432
480
|
// what one caller (issuer and subject) may have at a time, so a single
|
|
433
481
|
// caller can not take the whole machine; over it a request gets 429
|
|
434
482
|
limits: zod_1.z
|
|
@@ -472,6 +520,40 @@ exports.serverConfigSchema = zod_1.z
|
|
|
472
520
|
// which tasks take arguments: `any` but those that say `args: false`, or
|
|
473
521
|
// `declared`, only those that say `args: true`
|
|
474
522
|
args: zod_1.z.enum(['any', 'declared']).default('any'),
|
|
523
|
+
// runs of services (`services` in a run), kept up until their ttl ends
|
|
524
|
+
up: zod_1.z
|
|
525
|
+
.object({
|
|
526
|
+
// how long the services stay once ready, when the run names no ttl; then they are removed
|
|
527
|
+
ttl: duration.default('8h'),
|
|
528
|
+
// how long the services stay up without a request to an endpoint before they sleep,
|
|
529
|
+
// when the run names none: the next request wakes them. Unset, they never sleep.
|
|
530
|
+
sleepTtl: duration.optional(),
|
|
531
|
+
// the furthest a run, or an extend, sets the end from now
|
|
532
|
+
maxTtl: duration.default('72h'),
|
|
533
|
+
limits: zod_1.z
|
|
534
|
+
.object({
|
|
535
|
+
// service runs one caller has starting or up at a time; one more is 429
|
|
536
|
+
up: zod_1.z.number().int().min(1).default(3),
|
|
537
|
+
// service runs one caller has asleep, which take no place of `up`; a new run
|
|
538
|
+
// when there are this many is 429
|
|
539
|
+
asleep: zod_1.z.number().int().min(1).default(10),
|
|
540
|
+
})
|
|
541
|
+
.strict()
|
|
542
|
+
.default({}),
|
|
543
|
+
// service runs starting or up on the whole server; one more is 503
|
|
544
|
+
total: zod_1.z.number().int().min(1).default(20),
|
|
545
|
+
// on a Kubernetes backend, the ResourceQuota of the namespace of each service
|
|
546
|
+
// run; backend.quota when left out
|
|
547
|
+
quota: zod_1.z.record(zod_1.z.union([zod_1.z.string().min(1), zod_1.z.number()]).transform(String)).optional(),
|
|
548
|
+
// the volumes of a run's services stay while it sleeps, and are mounted again when
|
|
549
|
+
// it wakes; they go when the run ends. false for a server short on disk: a woken
|
|
550
|
+
// run starts from nothing.
|
|
551
|
+
keepVolumes: zod_1.z.boolean().default(true),
|
|
552
|
+
})
|
|
553
|
+
.strict()
|
|
554
|
+
.default({}),
|
|
555
|
+
// how long a passed check stays for `remote verify` to find (see checks.ts)
|
|
556
|
+
checksKeep: duration.default('30d'),
|
|
475
557
|
})
|
|
476
558
|
.strict()
|
|
477
559
|
.default({}),
|
|
@@ -496,6 +578,9 @@ exports.serverConfigSchema = zod_1.z
|
|
|
496
578
|
.object({ allow: zod_1.z.array(zod_1.z.string().regex(/^[A-Za-z_][A-Za-z0-9_()]*$/)).default([]) })
|
|
497
579
|
.strict()
|
|
498
580
|
.default({}),
|
|
581
|
+
// the capabilities of `backend.capabilities.allow` a task of a run on a sandbox may
|
|
582
|
+
// add: none by default, a sandbox holds code nobody reviewed
|
|
583
|
+
capabilities: zod_1.z.array(zod_1.z.enum(capabilities_schema_1.LINUX_CAPABILITIES)).default([]),
|
|
499
584
|
// what one caller may keep; a sandbox over it can't be created
|
|
500
585
|
limits: zod_1.z
|
|
501
586
|
.object({
|
|
@@ -509,6 +594,59 @@ exports.serverConfigSchema = zod_1.z
|
|
|
509
594
|
})
|
|
510
595
|
.strict()
|
|
511
596
|
.default({}),
|
|
597
|
+
// ports of service runs published at hosts of their own below `domain`, behind the
|
|
598
|
+
// server's sign-in. Absent, a run naming endpoints is refused.
|
|
599
|
+
endpoints: zod_1.z
|
|
600
|
+
.object({
|
|
601
|
+
// `run.example.com`: an endpoint is https://<label>.run.example.com, all of them
|
|
602
|
+
// pointing at this server with one wildcard DNS record
|
|
603
|
+
domain: zod_1.z
|
|
604
|
+
.string()
|
|
605
|
+
.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'),
|
|
606
|
+
// a wildcard certificate for *.<domain>, served for the hosts of endpoints; left out
|
|
607
|
+
// behind a TLS proxy, which holds it
|
|
608
|
+
tls: zod_1.z
|
|
609
|
+
.object({ cert: zod_1.z.string().min(1), key: zod_1.z.string().min(1) })
|
|
610
|
+
.strict()
|
|
611
|
+
.optional(),
|
|
612
|
+
// how a person signs in with a browser: the authorization code flow of an issuer of
|
|
613
|
+
// `issuers`, with a client of its own; without it only tokens open endpoints
|
|
614
|
+
login: zod_1.z
|
|
615
|
+
.object({
|
|
616
|
+
issuer: zod_1.z.string().min(1),
|
|
617
|
+
clientId: zod_1.z.string().min(1),
|
|
618
|
+
// for providers whose web clients need one (Google): read as an account no
|
|
619
|
+
// entitlement may name, as the secret of a source is
|
|
620
|
+
clientSecret: zod_1.z
|
|
621
|
+
.object({
|
|
622
|
+
from: zod_1.z.string().regex(/^[A-Za-z][A-Za-z0-9_-]*:.+$/, 'expected provider:reference'),
|
|
623
|
+
account: accountNameSchema,
|
|
624
|
+
})
|
|
625
|
+
.strict()
|
|
626
|
+
.optional(),
|
|
627
|
+
scope: zod_1.z.string().min(1).default('openid'),
|
|
628
|
+
})
|
|
629
|
+
.strict()
|
|
630
|
+
.optional(),
|
|
631
|
+
// how long a sign-in at one endpoint lasts, at most until its run ends: the groups and
|
|
632
|
+
// claims of the sign-in hold that long, a change at the issuer counts at the next one
|
|
633
|
+
session: duration.default('1h'),
|
|
634
|
+
// the host of the dashboard below the domain, `hammerkit.<domain>`, signed in at like an
|
|
635
|
+
// endpoint (it needs `login`); false for none
|
|
636
|
+
dashboard: zod_1.z
|
|
637
|
+
.union([
|
|
638
|
+
zod_1.z.literal(false),
|
|
639
|
+
zod_1.z
|
|
640
|
+
.string()
|
|
641
|
+
.regex(/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, 'expected a host label, such as hammerkit')
|
|
642
|
+
.refine((label) => label !== 'login', 'login is the host of the sign-in')
|
|
643
|
+
// the hosts of endpoints end in -<port>-<12 hex>
|
|
644
|
+
.refine((label) => !/-\d+-[0-9a-f]{12}$/.test(label), 'that is how the hosts of endpoints are named'),
|
|
645
|
+
])
|
|
646
|
+
.default('hammerkit'),
|
|
647
|
+
})
|
|
648
|
+
.strict()
|
|
649
|
+
.optional(),
|
|
512
650
|
backend: zod_1.z.discriminatedUnion('type', [hostBackendSchema, kubernetesBackendSchema]).default({ type: 'host' }),
|
|
513
651
|
})
|
|
514
652
|
.strict()
|
|
@@ -576,7 +714,7 @@ exports.serverConfigSchema = zod_1.z
|
|
|
576
714
|
});
|
|
577
715
|
}
|
|
578
716
|
});
|
|
579
|
-
const readers =
|
|
717
|
+
const readers = getServerAccounts(config);
|
|
580
718
|
config.entitlements.forEach((entitlement, index) => {
|
|
581
719
|
for (const account of Object.keys(entitlement.accounts)) {
|
|
582
720
|
if (readers.includes(account)) {
|
|
@@ -588,6 +726,16 @@ exports.serverConfigSchema = zod_1.z
|
|
|
588
726
|
}
|
|
589
727
|
}
|
|
590
728
|
});
|
|
729
|
+
const allowed = config.backend.capabilities?.allow ?? [];
|
|
730
|
+
config.sandboxes.capabilities.forEach((capability, index) => {
|
|
731
|
+
if (!allowed.includes(capability)) {
|
|
732
|
+
ctx.addIssue({
|
|
733
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
734
|
+
path: ['sandboxes', 'capabilities', index],
|
|
735
|
+
message: `the capability ${capability} is not in backend.capabilities.allow, no run may add it`,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
});
|
|
591
739
|
const sandboxNetwork = (0, work_network_1.parseWorkNetwork)(config.sandboxes.network);
|
|
592
740
|
const runsNetwork = (0, work_network_1.parseWorkNetwork)(config.runs.network);
|
|
593
741
|
if ((0, work_network_1.exceedsNetwork)(sandboxNetwork, runsNetwork)) {
|
|
@@ -659,7 +807,7 @@ exports.serverConfigSchema = zod_1.z
|
|
|
659
807
|
});
|
|
660
808
|
// the secrets of tasks are read in their Job, with the image of the provider; the
|
|
661
809
|
// ones of the sources are read by the server, which checks the code out itself
|
|
662
|
-
const readers =
|
|
810
|
+
const readers = getServerAccounts(config);
|
|
663
811
|
for (const [name, account] of Object.entries(config.accounts)) {
|
|
664
812
|
if (readers.includes(name)) {
|
|
665
813
|
continue;
|
|
@@ -745,6 +893,87 @@ exports.serverConfigSchema = zod_1.z
|
|
|
745
893
|
message: 'the network of a local task can not be limited, allow only the docker runtime with runs.network',
|
|
746
894
|
});
|
|
747
895
|
}
|
|
896
|
+
if ((0, units_1.parseDuration)(config.runs.up.ttl) > (0, units_1.parseDuration)(config.runs.up.maxTtl)) {
|
|
897
|
+
ctx.addIssue({
|
|
898
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
899
|
+
path: ['runs', 'up', 'ttl'],
|
|
900
|
+
message: `a ttl of ${config.runs.up.ttl} is more than maxTtl of ${config.runs.up.maxTtl}`,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
const endpoints = config.endpoints;
|
|
904
|
+
if (endpoints) {
|
|
905
|
+
if (endpoints.tls && config.behindTlsProxy) {
|
|
906
|
+
ctx.addIssue({
|
|
907
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
908
|
+
path: ['endpoints', 'tls'],
|
|
909
|
+
message: 'behind a TLS proxy the proxy holds the certificate of the endpoints, leave out endpoints.tls',
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
if (endpoints.tls && !config.tls) {
|
|
913
|
+
ctx.addIssue({
|
|
914
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
915
|
+
path: ['endpoints', 'tls'],
|
|
916
|
+
message: 'the certificate of the endpoints is served next to the one of the server, set tls as well',
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
if (!endpoints.tls && !config.behindTlsProxy && (config.tls || !LOOPBACK_HOSTS.includes(config.listen.host))) {
|
|
920
|
+
ctx.addIssue({
|
|
921
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
922
|
+
path: ['endpoints', 'tls'],
|
|
923
|
+
message: `name the certificate of *.${endpoints.domain} in endpoints.tls, or set behindTlsProxy: true when a proxy in front holds it`,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
const login = endpoints.login;
|
|
927
|
+
if (login && !config.issuers.some((issuer) => issuer.issuer === login.issuer)) {
|
|
928
|
+
ctx.addIssue({
|
|
929
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
930
|
+
path: ['endpoints', 'login', 'issuer'],
|
|
931
|
+
message: `issuer ${login.issuer} is not one of the accepted issuers`,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
if (login?.clientSecret) {
|
|
935
|
+
const { from, account } = login.clientSecret;
|
|
936
|
+
const kind = from.substring(0, from.indexOf(':'));
|
|
937
|
+
const where = ['endpoints', 'login', 'clientSecret'];
|
|
938
|
+
if (!config.accounts[account]) {
|
|
939
|
+
ctx.addIssue({
|
|
940
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
941
|
+
path: [...where, 'account'],
|
|
942
|
+
message: `unknown account ${account}`,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
else if (!config.accounts[account].providers[kind]) {
|
|
946
|
+
ctx.addIssue({
|
|
947
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
948
|
+
path: [...where, 'from'],
|
|
949
|
+
message: `account ${account} has no credentials for the provider ${kind}`,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if (!config.secretProviders[kind]) {
|
|
953
|
+
ctx.addIssue({
|
|
954
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
955
|
+
path: [...where, 'from'],
|
|
956
|
+
message: `secret provider ${kind} is not declared in secretProviders`,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// only the server is to reach the services behind endpoints, which the NetworkPolicy of
|
|
961
|
+
// a service run says: without it, any pod of the cluster would get past the sign-in
|
|
962
|
+
if (backend.type === 'kubernetes' && backend.networkPolicy !== 'enforced') {
|
|
963
|
+
ctx.addIssue({
|
|
964
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
965
|
+
path: ['backend', 'networkPolicy'],
|
|
966
|
+
message: 'endpoints keep the services of a run to the server with a NetworkPolicy, set networkPolicy: enforced once the network plugin of the cluster enforces them',
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
if (backend.type === 'kubernetes' && backend.kubeconfig) {
|
|
970
|
+
ctx.addIssue({
|
|
971
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
972
|
+
path: ['endpoints'],
|
|
973
|
+
message: 'endpoints reach the services of runs at their Service in the cluster, run the server in the cluster rather than with a kubeconfig',
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
748
977
|
if (!config.tls && !config.behindTlsProxy && !LOOPBACK_HOSTS.includes(config.listen.host)) {
|
|
749
978
|
ctx.addIssue({
|
|
750
979
|
code: zod_1.z.ZodIssueCode.custom,
|
|
@@ -760,11 +989,18 @@ function getSandboxRuntimes(config) {
|
|
|
760
989
|
return config.sandboxes.runtimes ?? config.runtimes.filter((runtime) => runtime === 'docker');
|
|
761
990
|
}
|
|
762
991
|
exports.getSandboxRuntimes = getSandboxRuntimes;
|
|
763
|
-
// The accounts
|
|
764
|
-
|
|
765
|
-
|
|
992
|
+
// The accounts only the server reads secrets as: those that read the repositories of
|
|
993
|
+
// `sources`, and the one of the client secret of `endpoints.login`. No caller uses them and
|
|
994
|
+
// no build file names them.
|
|
995
|
+
function getServerAccounts(config) {
|
|
996
|
+
return [
|
|
997
|
+
...new Set([
|
|
998
|
+
...config.sources.flatMap((source) => (source.secret ? [source.secret.account] : [])),
|
|
999
|
+
...(config.endpoints?.login?.clientSecret ? [config.endpoints.login.clientSecret.account] : []),
|
|
1000
|
+
]),
|
|
1001
|
+
];
|
|
766
1002
|
}
|
|
767
|
-
exports.
|
|
1003
|
+
exports.getServerAccounts = getServerAccounts;
|
|
768
1004
|
async function readServerConfig(environment, path) {
|
|
769
1005
|
if (!(await environment.file.exists(path))) {
|
|
770
1006
|
throw new Error(`unable to read ${path}: no such file`);
|