hammerkit 1.27.0-next.9 → 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 +49 -1
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/kubernetes/ensure-persistent-data.js +211 -6
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +2 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +94 -9
- package/dist/main.js +2 -0
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-kubernetes.js +136 -13
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +415 -48
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +962 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
package/dist/server/server.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.startServer = exports.getDiscoveryDocument = exports.SERVER_PROTOCOL = exports.DISCOVERY_PATH = void 0;
|
|
3
|
+
exports.startServer = exports.runsTheSame = exports.refuseWake = exports.getDiscoveryDocument = exports.SERVER_PROTOCOL = exports.DISCOVERY_PATH = void 0;
|
|
4
4
|
const http_1 = require("http");
|
|
5
5
|
const https_1 = require("https");
|
|
6
|
+
const tls_1 = require("tls");
|
|
6
7
|
const version_1 = require("../version");
|
|
7
8
|
const server_config_1 = require("./server-config");
|
|
8
9
|
const discovery_1 = require("./discovery");
|
|
9
10
|
const oauth_1 = require("./oauth");
|
|
10
11
|
const policy_1 = require("./policy");
|
|
12
|
+
const repo_1 = require("./repo");
|
|
11
13
|
const run_store_1 = require("./run-store");
|
|
12
14
|
const runs_1 = require("./runs");
|
|
13
15
|
const git_checkout_1 = require("./git-checkout");
|
|
14
16
|
const repo_access_1 = require("./repo-access");
|
|
15
17
|
const executor_1 = require("./executor");
|
|
18
|
+
const capacity_1 = require("./capacity");
|
|
19
|
+
const execute_docker_1 = require("../executer/execute-docker");
|
|
20
|
+
const timings_1 = require("./timings");
|
|
16
21
|
const kubernetes_backend_1 = require("./kubernetes-backend");
|
|
17
22
|
const run_plan_1 = require("./run-plan");
|
|
18
23
|
const work_network_1 = require("../planner/work-network");
|
|
@@ -32,10 +37,15 @@ const sandbox_storage_1 = require("./sandbox-storage");
|
|
|
32
37
|
const kubernetes_sandbox_storage_1 = require("./kubernetes-sandbox-storage");
|
|
33
38
|
const sandbox_docker_1 = require("./sandbox-docker");
|
|
34
39
|
const sandboxes_api_1 = require("./sandboxes-api");
|
|
40
|
+
const endpoints_1 = require("./endpoints");
|
|
41
|
+
const service_runs_1 = require("./service-runs");
|
|
35
42
|
const units_1 = require("../utils/units");
|
|
36
43
|
const run_logs_1 = require("./run-logs");
|
|
37
44
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
38
45
|
const path_1 = require("path");
|
|
46
|
+
const checks_1 = require("./checks");
|
|
47
|
+
const checks_api_1 = require("./checks-api");
|
|
48
|
+
const overview_1 = require("./overview");
|
|
39
49
|
var discovery_2 = require("./discovery");
|
|
40
50
|
Object.defineProperty(exports, "DISCOVERY_PATH", { enumerable: true, get: function () { return discovery_2.DISCOVERY_PATH; } });
|
|
41
51
|
Object.defineProperty(exports, "SERVER_PROTOCOL", { enumerable: true, get: function () { return discovery_2.SERVER_PROTOCOL; } });
|
|
@@ -64,6 +74,8 @@ const HEARTBEAT_MS = 15000;
|
|
|
64
74
|
// the longest `?wait=` of a run: below the 60 seconds after which a proxy in front of the
|
|
65
75
|
// server (nginx by default) closes a connection that says nothing
|
|
66
76
|
const MAX_WAIT_MS = 50000;
|
|
77
|
+
// how often checks older than `runs.checksKeep` are removed
|
|
78
|
+
const CHECK_SWEEP_MS = 6 * 60 * 60 * 1000;
|
|
67
79
|
// Server-sent events: `id` is the sequence number, so a client that lost the
|
|
68
80
|
// connection continues with Last-Event-ID (or ?after=) and misses nothing.
|
|
69
81
|
function streamEvents(registry, config, principal, id, req, res, url) {
|
|
@@ -96,8 +108,8 @@ function streamEvents(registry, config, principal, id, req, res, url) {
|
|
|
96
108
|
watch.stop();
|
|
97
109
|
});
|
|
98
110
|
}
|
|
99
|
-
async function handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res, url) {
|
|
100
|
-
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events|logs))?)?$/.exec(url.pathname);
|
|
111
|
+
async function handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res, url) {
|
|
112
|
+
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events|logs|extend))?)?$/.exec(url.pathname);
|
|
101
113
|
if (!match) {
|
|
102
114
|
return false;
|
|
103
115
|
}
|
|
@@ -106,16 +118,22 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
106
118
|
if (!id) {
|
|
107
119
|
if (method === 'GET') {
|
|
108
120
|
// the results of each task are read one run at a time
|
|
121
|
+
const viewer = (0, overview_1.getViewer)(config, principal);
|
|
122
|
+
let filter;
|
|
123
|
+
try {
|
|
124
|
+
filter = (0, overview_1.parseRunFilter)(url.searchParams, viewer.grant);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
(0, http_2.send)(res, e instanceof overview_1.FilterRefused ? 403 : 400, { error: e instanceof Error ? e.message : String(e) });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const runs = filter.scope === 'mine' ? registry.list(principal) : registry.all();
|
|
109
131
|
(0, http_2.send)(res, 200, {
|
|
110
|
-
runs:
|
|
111
|
-
const { results, ...listed } = toRunResponse(run);
|
|
112
|
-
void results;
|
|
113
|
-
return listed;
|
|
114
|
-
}),
|
|
132
|
+
runs: (0, overview_1.filterRuns)(runs, viewer, filter).map((run) => listRun(registry, viewer, run)),
|
|
115
133
|
});
|
|
116
134
|
}
|
|
117
135
|
else if (method === 'POST') {
|
|
118
|
-
await submitRun(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res);
|
|
136
|
+
await submitRun(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res);
|
|
119
137
|
}
|
|
120
138
|
else {
|
|
121
139
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET, POST' });
|
|
@@ -134,6 +152,13 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
134
152
|
log(`cancel iss=${principal.iss} sub=${principal.sub} run=${id} found=${Boolean(run)}`);
|
|
135
153
|
run ? (0, http_2.send)(res, 200, toRunResponse(run)) : (0, http_2.send)(res, 404, { error: 'not found' });
|
|
136
154
|
}
|
|
155
|
+
else if (action === 'extend') {
|
|
156
|
+
if (method !== 'POST') {
|
|
157
|
+
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
await extendRun(config, registry, log, principal, id, req, res);
|
|
161
|
+
}
|
|
137
162
|
else if (method !== 'GET') {
|
|
138
163
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
139
164
|
}
|
|
@@ -149,9 +174,12 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
149
174
|
(0, http_2.send)(res, 400, { error: e instanceof Error ? e.message : String(e) });
|
|
150
175
|
return true;
|
|
151
176
|
}
|
|
152
|
-
|
|
177
|
+
// an entitlement with `observe` reads the log of every run
|
|
178
|
+
const logs = registry.logs(principal, id, accountsOf(config, principal)) ??
|
|
179
|
+
((0, policy_1.getGrant)(config.entitlements, principal).observe ? registry.logsOf(id) : undefined);
|
|
180
|
+
// with the tasks, services and the server's own lines (`hammerkit`) the log has
|
|
153
181
|
logs
|
|
154
|
-
? (0, http_2.send)(res, 200, (0, run_logs_1.readLogs)(logs.events, logs.index, query, logs.truncated))
|
|
182
|
+
? (0, http_2.send)(res, 200, { ...(0, run_logs_1.readLogs)(logs.events, logs.index, query, logs.truncated), names: logs.index.names() })
|
|
155
183
|
: (0, http_2.send)(res, 404, { error: 'not found' });
|
|
156
184
|
}
|
|
157
185
|
else {
|
|
@@ -169,9 +197,18 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
|
|
|
169
197
|
// a caller that hangs up stops waiting
|
|
170
198
|
const gone = new AbortController();
|
|
171
199
|
res.on('close', () => gone.abort());
|
|
172
|
-
const
|
|
200
|
+
const viewer = (0, overview_1.getViewer)(config, principal);
|
|
201
|
+
// an entitlement with `observe` reads every run as it is, without waiting
|
|
202
|
+
const run = (await registry.wait(principal, id, wait, accountsOf(config, principal), gone.signal)) ??
|
|
203
|
+
(viewer.grant.observe ? registry.peek(id) : undefined);
|
|
173
204
|
if (!gone.signal.aborted) {
|
|
174
|
-
|
|
205
|
+
const may = run && (0, overview_1.runActions)(viewer, run);
|
|
206
|
+
run
|
|
207
|
+
? (0, http_2.send)(res, 200, {
|
|
208
|
+
...toRunResponse(run, (0, overview_1.queuePlace)(registry, viewer, run), !(0, overview_1.owns)(viewer, run)),
|
|
209
|
+
...(may ? { may } : {}),
|
|
210
|
+
})
|
|
211
|
+
: (0, http_2.send)(res, 404, { error: 'not found' });
|
|
175
212
|
}
|
|
176
213
|
}
|
|
177
214
|
return true;
|
|
@@ -181,12 +218,100 @@ function accountsOf(config, principal) {
|
|
|
181
218
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
182
219
|
return (account) => (0, policy_1.canUse)(grant, account);
|
|
183
220
|
}
|
|
184
|
-
function
|
|
221
|
+
async function extendRun(config, registry, log, principal, id, req, res) {
|
|
222
|
+
const body = (await (0, http_2.readJson)(req));
|
|
223
|
+
let ttl;
|
|
224
|
+
try {
|
|
225
|
+
if (typeof body !== 'object' || body === null || typeof body.ttl !== 'string') {
|
|
226
|
+
throw new Error();
|
|
227
|
+
}
|
|
228
|
+
ttl = (0, units_1.parseDuration)(body.ttl);
|
|
229
|
+
if (!(ttl > 0)) {
|
|
230
|
+
throw new Error();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
(0, http_2.send)(res, 400, { error: 'ttl: expected a duration like 30m, 8h or 1d' });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
238
|
+
const result = registry.extend(principal, id, ttl, (run) => (0, service_runs_1.mayOpenEndpoints)(grant, run));
|
|
239
|
+
log(`extend iss=${principal.iss} sub=${principal.sub} run=${id} result=${result.status}`);
|
|
240
|
+
if (result.status === 'not-found') {
|
|
241
|
+
(0, http_2.send)(res, 404, { error: 'not found' });
|
|
242
|
+
}
|
|
243
|
+
else if (result.status === 'not-up') {
|
|
244
|
+
(0, http_2.send)(res, 409, { error: `the run is ${result.state}, only a service run that is up can be extended` });
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
(0, http_2.send)(res, 200, toRunResponse(result.run));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Why the owner of a service run that sleeps may no longer run it, with the entitlements of
|
|
251
|
+
// the moment: a wake is started by who opens an endpoint, but runs as the owner, as a new
|
|
252
|
+
// run of theirs would.
|
|
253
|
+
function refuseWake(config, repos, run) {
|
|
254
|
+
if (run.source.type !== 'ref') {
|
|
255
|
+
return 'a run from a sandbox does not wake';
|
|
256
|
+
}
|
|
257
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, {
|
|
258
|
+
iss: run.owner.iss,
|
|
259
|
+
sub: run.owner.sub,
|
|
260
|
+
groups: run.owner.groups ?? [],
|
|
261
|
+
claims: run.owner.claims ?? {},
|
|
262
|
+
});
|
|
263
|
+
const decision = (0, policy_1.decideRun)({
|
|
264
|
+
grant,
|
|
265
|
+
accounts: run.accounts,
|
|
266
|
+
catalog: config.accounts,
|
|
267
|
+
source: { type: 'ref', repo: run.source.repo, ref: run.source.ref },
|
|
268
|
+
// an account it may only request is refused, the wake has nobody to approve it
|
|
269
|
+
approvable: () => true,
|
|
270
|
+
readable: (repo) => repos.readable(repo),
|
|
271
|
+
});
|
|
272
|
+
if (decision.type === 'refuse') {
|
|
273
|
+
return `its owner ${run.owner.sub} may no longer run it: ${decision.error.replace(/^you /, 'they ')}`;
|
|
274
|
+
}
|
|
275
|
+
if (decision.type === 'approval') {
|
|
276
|
+
return `its owner ${run.owner.sub} may only request ${decision.accounts.join(', ')} now, start it again`;
|
|
277
|
+
}
|
|
278
|
+
if (run.up?.endpoints.length && (0, policy_1.endpointLevel)(grant, run.source.repo) !== 'publish') {
|
|
279
|
+
return `its owner ${run.owner.sub} may no longer publish endpoints of ${run.source.repo}`;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
exports.refuseWake = refuseWake;
|
|
284
|
+
// Whether a service run that is not over runs what a new one would, but for its commit: then
|
|
285
|
+
// the new one moves it there (RunRegistry.move)
|
|
286
|
+
function runsTheSame(run, next) {
|
|
287
|
+
const sorted = (values) => JSON.stringify([...values].sort());
|
|
288
|
+
const options = (value) => JSON.stringify(value ?? {}, Object.keys(value ?? {}).sort());
|
|
289
|
+
const endpoints = (spec) => JSON.stringify(spec.endpoints.map((endpoint) => [endpoint.service, endpoint.port, endpoint.host]));
|
|
290
|
+
return (!!run.up &&
|
|
291
|
+
run.source.type === 'ref' &&
|
|
292
|
+
run.source.repo === next.source.repo &&
|
|
293
|
+
run.definition === next.definition &&
|
|
294
|
+
sorted(run.accounts) === sorted(next.accounts) &&
|
|
295
|
+
sorted(run.up.services) === sorted(next.up.services) &&
|
|
296
|
+
endpoints(run.up) === endpoints(next.up) &&
|
|
297
|
+
(run.up.sleepTtl ?? null) === (next.up.sleepTtl ?? null) &&
|
|
298
|
+
options(run.options) === options(next.request.options));
|
|
299
|
+
}
|
|
300
|
+
exports.runsTheSame = runsTheSame;
|
|
301
|
+
// A run whole, for its owner and who may read it; who is not its owner is told whose it is.
|
|
302
|
+
function toRunResponse(run, queue, named = false) {
|
|
185
303
|
const { owner, ...visible } = run;
|
|
186
|
-
|
|
187
|
-
|
|
304
|
+
return {
|
|
305
|
+
...visible,
|
|
306
|
+
...(named ? { owner: { iss: owner.iss, sub: owner.sub } } : {}),
|
|
307
|
+
results: run.results ?? {},
|
|
308
|
+
...(queue ? { queue } : {}),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function listRun(registry, viewer, run) {
|
|
312
|
+
return (0, overview_1.toListedRun)(viewer, run, (0, overview_1.queuePlace)(registry, viewer, run));
|
|
188
313
|
}
|
|
189
|
-
async function submitRun(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res) {
|
|
314
|
+
async function submitRun(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res) {
|
|
190
315
|
const parsed = run_request_1.runRequestSchema.safeParse(await (0, http_2.readJson)(req));
|
|
191
316
|
if (!parsed.success) {
|
|
192
317
|
const issue = parsed.error.issues[0];
|
|
@@ -218,6 +343,27 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
218
343
|
(0, http_2.send)(res, 400, { error: `${version.hash} has no task ${unknown.map(oauth_1.quote).join(', ')}` });
|
|
219
344
|
return;
|
|
220
345
|
}
|
|
346
|
+
const notServices = request.services.filter((service) => !version.services.includes(service));
|
|
347
|
+
if (notServices.length > 0) {
|
|
348
|
+
(0, http_2.send)(res, 400, { error: `${version.hash} has no service ${notServices.map(oauth_1.quote).join(', ')}` });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (request.endpoints.length > 0 && !config.endpoints) {
|
|
352
|
+
(0, http_2.send)(res, 400, { error: 'this server publishes no endpoints (endpoints is not configured)' });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const ttl = request.services.length > 0 ? (0, units_1.parseDuration)(request.ttl ?? config.runs.up.ttl) : 0;
|
|
356
|
+
if (ttl > (0, units_1.parseDuration)(config.runs.up.maxTtl)) {
|
|
357
|
+
(0, http_2.send)(res, 400, { error: `ttl: at most ${config.runs.up.maxTtl} on this server (runs.up.maxTtl)` });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// the files of a sandbox change under a run that sleeps, it would wake to others
|
|
361
|
+
if (request.sleepTtl !== undefined && request.source.type === 'sandbox') {
|
|
362
|
+
(0, http_2.send)(res, 400, { error: 'sleepTtl: a service run from a sandbox does not sleep, start it from a branch' });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const sleepTtlText = request.source.type === 'ref' ? request.sleepTtl ?? config.runs.up.sleepTtl : undefined;
|
|
366
|
+
const sleepTtl = request.services.length > 0 && sleepTtlText ? (0, units_1.parseDuration)(sleepTtlText) : undefined;
|
|
221
367
|
const argued = (request.options.args ?? []).length > 0 ? request.tasks[0] : undefined;
|
|
222
368
|
if (argued !== undefined && version.services.includes(argued)) {
|
|
223
369
|
(0, http_2.send)(res, 400, { error: `${argued} is a service, arguments go to a task` });
|
|
@@ -239,6 +385,12 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
239
385
|
return;
|
|
240
386
|
}
|
|
241
387
|
}
|
|
388
|
+
if (request.services.length > 0 && request.source.type === 'sandbox' && config.backend.type === 'kubernetes') {
|
|
389
|
+
(0, http_2.send)(res, 400, {
|
|
390
|
+
error: 'on a cluster a service run starts from a branch, push the sandbox and start it from there',
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
242
394
|
const sandbox = request.source.type === 'sandbox' ? sandboxes.registry.get(principal, request.source.id) : undefined;
|
|
243
395
|
if (request.source.type === 'sandbox') {
|
|
244
396
|
if (!sandbox) {
|
|
@@ -257,6 +409,24 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
257
409
|
(0, http_2.send)(res, 413, { error: over });
|
|
258
410
|
return;
|
|
259
411
|
}
|
|
412
|
+
// a check counts like a run from a commit of the repository, and leaves what it made in
|
|
413
|
+
// the repository's cache, which runs from main restore from: the caller must be allowed
|
|
414
|
+
// to start runs from it, and to push a branch of it, which with a run from that branch
|
|
415
|
+
// leaves the same
|
|
416
|
+
if (request.source.check && !(0, repo_1.matchesAnyRepo)(grant.repos, sandbox.repo)) {
|
|
417
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: check on ${sandbox.repo} without the repository`);
|
|
418
|
+
(0, http_2.send)(res, 403, {
|
|
419
|
+
error: `a check counts like a run from ${sandbox.repo}, which no entitlement of yours lists in repos`,
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (request.source.check && (0, policy_1.sandboxPushBranches)(grant, sandbox.repo).length === 0) {
|
|
424
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: check on ${sandbox.repo} without a branch to push`);
|
|
425
|
+
(0, http_2.send)(res, 403, {
|
|
426
|
+
error: `a check counts like a run from a branch you pushed, and you may push no branch of ${sandbox.repo}`,
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
260
430
|
}
|
|
261
431
|
// the accounts of a run are read from the files it is pinned to, with the engine
|
|
262
432
|
const bundle = version.plan ? await definitions.files(version) : undefined;
|
|
@@ -275,12 +445,20 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
275
445
|
(0, http_2.send)(res, 400, { error: `the run does not load: ${e instanceof Error ? e.message : String(e)}` });
|
|
276
446
|
return;
|
|
277
447
|
}
|
|
278
|
-
const refused = (0, run_plan_1.refuseArguments)(request, planned.plan, sandbox ? sandboxes.policy : planning.policy)
|
|
448
|
+
const refused = (0, run_plan_1.refuseArguments)(request, planned.plan, sandbox ? sandboxes.policy : planning.policy) ??
|
|
449
|
+
(0, service_runs_1.refuseEndpoints)(request, planned.plan, config.runs.secretNetwork === 'open');
|
|
279
450
|
if (refused) {
|
|
280
451
|
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${refused}`);
|
|
281
452
|
(0, http_2.send)(res, 400, { error: refused });
|
|
282
453
|
return;
|
|
283
454
|
}
|
|
455
|
+
const runRepo = request.source.type === 'ref' ? request.source.repo : sandbox?.repo;
|
|
456
|
+
if (request.endpoints.length > 0 && (!runRepo || (0, policy_1.endpointLevel)(grant, runRepo) !== 'publish')) {
|
|
457
|
+
const error = `you may not publish endpoints of ${runRepo ?? 'this source'}, an entitlement needs endpoints with level publish for it`;
|
|
458
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${error}`);
|
|
459
|
+
(0, http_2.send)(res, 403, { error });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
284
462
|
const decision = (0, policy_1.decideRun)({
|
|
285
463
|
grant,
|
|
286
464
|
accounts: planned.accounts,
|
|
@@ -324,13 +502,54 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
324
502
|
else if (request.source.type === 'upload') {
|
|
325
503
|
source = request.source;
|
|
326
504
|
}
|
|
327
|
-
|
|
505
|
+
// a service run is named after its branch unless it says, and its endpoints after that
|
|
506
|
+
const serviceName = request.services.length > 0
|
|
507
|
+
? request.name ??
|
|
508
|
+
(0, service_runs_1.getServiceRunName)(request.source.type === 'ref' ? request.source.ref : sandbox?.ref)
|
|
509
|
+
: '';
|
|
510
|
+
const up = request.services.length > 0
|
|
511
|
+
? {
|
|
512
|
+
services: request.services,
|
|
513
|
+
ttl,
|
|
514
|
+
...(sleepTtl ? { sleepTtl } : {}),
|
|
515
|
+
name: serviceName,
|
|
516
|
+
endpoints: (0, service_runs_1.toRunEndpoints)(request.endpoints, (0, service_runs_1.getEndpointHosts)(serviceName, principal, request.endpoints), endpointUrl),
|
|
517
|
+
}
|
|
518
|
+
: undefined;
|
|
519
|
+
// a run that may sleep keeps what its owner is entitled by, for the check of a wake
|
|
520
|
+
const claimNames = new Set(config.entitlements.flatMap((entitlement) => Object.keys(entitlement.claims?.match ?? {})));
|
|
521
|
+
const owner = {
|
|
522
|
+
...principal,
|
|
523
|
+
claims: Object.fromEntries(Object.entries(principal.claims).filter(([name]) => claimNames.has(name))),
|
|
524
|
+
};
|
|
525
|
+
// A service run of the caller of this name that runs the same build file, services and
|
|
526
|
+
// endpoints moves to the commit, in place of a new run that would replace it: it keeps its
|
|
527
|
+
// id, its addresses and its volumes. A run that needs an approval is new as before.
|
|
528
|
+
if (up && source?.type === 'ref' && approvals.length === 0) {
|
|
529
|
+
const named = registry.named(principal, up.name);
|
|
530
|
+
if (named && runsTheSame(named, { definition: version.hash, accounts: planned.accounts, request, source, up })) {
|
|
531
|
+
const from = (registry.logs(principal, named.id)?.index.size ?? 0) + 1;
|
|
532
|
+
const moved = registry.move(principal, named.id, {
|
|
533
|
+
source,
|
|
534
|
+
plan: planned.plan,
|
|
535
|
+
owner,
|
|
536
|
+
...(request.ttl ? { ttl } : {}),
|
|
537
|
+
});
|
|
538
|
+
if (moved) {
|
|
539
|
+
log(`move run ${moved.id} iss=${principal.iss} sub=${principal.sub} name=${up.name} to ${source.commit} (${moved.state})`);
|
|
540
|
+
(0, http_2.send)(res, 202, { ...toRunResponse(moved), moved: { logFrom: from } });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const submit = (runSource) => registry.submit(owner, {
|
|
328
546
|
definition: version.hash,
|
|
329
547
|
tasks: request.tasks,
|
|
330
548
|
accounts: planned.accounts,
|
|
331
549
|
plan: planned.plan,
|
|
332
550
|
source: runSource,
|
|
333
551
|
options: request.options,
|
|
552
|
+
...(up ? { up } : {}),
|
|
334
553
|
}, approvals);
|
|
335
554
|
try {
|
|
336
555
|
// a sandbox is read and the run submitted while nothing else changes it: the run
|
|
@@ -346,10 +565,13 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
346
565
|
commit: state.commit,
|
|
347
566
|
tree: state.tree,
|
|
348
567
|
clean: state.clean,
|
|
568
|
+
...(request.source.type === 'sandbox' && request.source.check ? { check: true } : {}),
|
|
349
569
|
});
|
|
350
570
|
});
|
|
351
|
-
log(`run ${run.id} iss=${principal.iss} sub=${principal.sub} accounts=[${planned.accounts.join(',')}] source=${run.source.type}${run.source.type === 'sandbox' ? ` sandbox=${run.source.id}` : ''} tasks=${run.tasks.length}${
|
|
352
|
-
|
|
571
|
+
log(`run ${run.id} iss=${principal.iss} sub=${principal.sub} accounts=[${planned.accounts.join(',')}] source=${run.source.type}${run.source.type === 'sandbox' ? ` sandbox=${run.source.id}${run.source.check ? ' check' : ''}` : ''} tasks=${run.tasks.length}${run.up
|
|
572
|
+
? ` services=[${run.up.services.join(',')}] name=${run.up.name} endpoints=${run.up.endpoints.length}`
|
|
573
|
+
: ''}${approvals.length > 0 ? ` approval=[${approvals.join(',')}]` : ''}`);
|
|
574
|
+
(0, http_2.send)(res, 202, toRunResponse(run, (0, overview_1.queuePlace)(registry, (0, overview_1.getViewer)(config, principal), run)));
|
|
353
575
|
}
|
|
354
576
|
catch (e) {
|
|
355
577
|
if (e instanceof runs_1.RunError) {
|
|
@@ -385,7 +607,14 @@ async function authenticate(verifier, log, req, res) {
|
|
|
385
607
|
return null;
|
|
386
608
|
}
|
|
387
609
|
}
|
|
388
|
-
async function handle(
|
|
610
|
+
async function handle(api, verifier, req, res) {
|
|
611
|
+
const { config, gateway, log } = api;
|
|
612
|
+
// the hosts of endpoints are the apps of service runs, not the API
|
|
613
|
+
const label = gateway?.hostOf(req);
|
|
614
|
+
if (gateway && label) {
|
|
615
|
+
await gateway.handle(label, req, res);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
389
618
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
390
619
|
const path = url.pathname;
|
|
391
620
|
if (path === discovery_1.DISCOVERY_PATH || path === '/healthz') {
|
|
@@ -393,13 +622,30 @@ async function handle(config, registry, definitions, proposals, sources, sandbox
|
|
|
393
622
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET, HEAD' });
|
|
394
623
|
return;
|
|
395
624
|
}
|
|
396
|
-
|
|
625
|
+
const dashboard = gateway?.dashboardUrl();
|
|
626
|
+
(0, http_2.send)(res, 200, path === '/healthz' ? { status: 'ok' } : { ...getDiscoveryDocument(config), ...(dashboard ? { dashboard } : {}) });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
// a person who opens the address of the server in a browser goes to the dashboard
|
|
630
|
+
const dashboard = gateway?.dashboardUrl();
|
|
631
|
+
if (path === '/' && req.method === 'GET' && dashboard && (req.headers.accept ?? '').includes('text/html')) {
|
|
632
|
+
res.writeHead(302, { location: dashboard, 'cache-control': 'no-store' });
|
|
633
|
+
res.end();
|
|
397
634
|
return;
|
|
398
635
|
}
|
|
399
636
|
const principal = await authenticate(verifier, log, req, res);
|
|
400
637
|
if (!principal) {
|
|
401
638
|
return;
|
|
402
639
|
}
|
|
640
|
+
await handleApi(api, principal, req, res, url);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The API for a caller who is known: by their token here, or by the sign-in of the
|
|
644
|
+
* dashboard, which asks the same API for what its pages show (dashboard.ts).
|
|
645
|
+
*/
|
|
646
|
+
async function handleApi(api, principal, req, res, url) {
|
|
647
|
+
const { config, registry, gateway, definitions, proposals, sources, sandboxes, planning, repos, checks, capacity, log, } = api;
|
|
648
|
+
const path = url.pathname;
|
|
403
649
|
if (await (0, accounts_api_1.handleAccounts)(config, log, principal, req, res, url)) {
|
|
404
650
|
return;
|
|
405
651
|
}
|
|
@@ -412,7 +658,51 @@ async function handle(config, registry, definitions, proposals, sources, sandbox
|
|
|
412
658
|
if (await (0, sandboxes_api_1.handleSandboxes)(config, sandboxes.registry, repos, log, principal, req, res, url)) {
|
|
413
659
|
return;
|
|
414
660
|
}
|
|
415
|
-
if (await handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, log, principal, req, res, url)) {
|
|
661
|
+
if (await handleRuns(config, registry, definitions, sources, sandboxes, planning, repos, (host) => gateway?.url(host) ?? host, log, principal, req, res, url)) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (path === '/v1/overview') {
|
|
665
|
+
if (req.method !== 'GET') {
|
|
666
|
+
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const viewer = (0, overview_1.getViewer)(config, principal);
|
|
670
|
+
if (viewer.grant.matched.length === 0) {
|
|
671
|
+
(0, http_2.send)(res, 403, { error: 'you are not entitled to anything on this server' });
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
(0, http_2.send)(res, 200, (0, overview_1.getOverview)(registry.snapshot(), registry.usage(principal), viewer));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (path === '/v1/capacity' || path === '/v1/tasks') {
|
|
678
|
+
if (req.method !== 'GET') {
|
|
679
|
+
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const viewer = (0, overview_1.getViewer)(config, principal);
|
|
683
|
+
if (viewer.grant.matched.length === 0) {
|
|
684
|
+
(0, http_2.send)(res, 403, { error: 'you are not entitled to anything on this server' });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (path === '/v1/capacity') {
|
|
688
|
+
(0, http_2.send)(res, 200, (0, overview_1.visibleCapacity)(await capacity.read(registry.all()), viewer));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// of every run the caller may see, unless they ask for their own
|
|
692
|
+
const params = new URLSearchParams(url.searchParams);
|
|
693
|
+
if (!params.has('scope')) {
|
|
694
|
+
params.set('scope', 'visible');
|
|
695
|
+
}
|
|
696
|
+
let filter;
|
|
697
|
+
try {
|
|
698
|
+
filter = (0, overview_1.parseRunFilter)(params, viewer.grant);
|
|
699
|
+
}
|
|
700
|
+
catch (e) {
|
|
701
|
+
(0, http_2.send)(res, e instanceof overview_1.FilterRefused ? 403 : 400, { error: e instanceof Error ? e.message : String(e) });
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const runs = (0, overview_1.filterRuns)(registry.all(), viewer, filter).map((run) => (0, overview_1.toListedRun)(viewer, run));
|
|
705
|
+
(0, http_2.send)(res, 200, { tasks: (0, timings_1.getTaskStats)(runs, Date.now()) });
|
|
416
706
|
return;
|
|
417
707
|
}
|
|
418
708
|
if (await (0, approvals_api_1.handleApprovals)(config, registry, log, principal, req, res, url)) {
|
|
@@ -421,6 +711,9 @@ async function handle(config, registry, definitions, proposals, sources, sandbox
|
|
|
421
711
|
if (await (0, proposals_api_1.handleProposals)(config, definitions, proposals, log, principal, req, res, url)) {
|
|
422
712
|
return;
|
|
423
713
|
}
|
|
714
|
+
if (await (0, checks_api_1.handleChecks)(config, checks, definitions, log, principal, req, res, url)) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
424
717
|
(0, http_2.send)(res, 404, { error: 'not found' });
|
|
425
718
|
}
|
|
426
719
|
async function startServer(config, environment, options = {}) {
|
|
@@ -447,12 +740,13 @@ async function startServer(config, environment, options = {}) {
|
|
|
447
740
|
},
|
|
448
741
|
policy: {
|
|
449
742
|
envs: config.runs.env.allow,
|
|
450
|
-
reserved: (0, server_config_1.
|
|
743
|
+
reserved: (0, server_config_1.getServerAccounts)(config),
|
|
451
744
|
runtimes: config.runtimes,
|
|
452
745
|
backend: config.backend.type,
|
|
453
746
|
network: (0, work_network_1.parseWorkNetwork)(config.runs.network),
|
|
454
747
|
secretNetwork: (0, work_network_1.parseWorkNetwork)(config.runs.secretNetwork),
|
|
455
748
|
args: config.runs.args,
|
|
749
|
+
capabilities: config.backend.capabilities?.allow ?? [],
|
|
456
750
|
proxiedProviders: config.backend.type === 'kubernetes'
|
|
457
751
|
? Object.entries(config.backend.providers)
|
|
458
752
|
.filter(([, provider]) => provider.allow)
|
|
@@ -493,6 +787,8 @@ async function startServer(config, environment, options = {}) {
|
|
|
493
787
|
envs: config.sandboxes.env.allow,
|
|
494
788
|
envsFrom: 'sandboxes.env.allow',
|
|
495
789
|
runtimes: sandboxRuntimes,
|
|
790
|
+
capabilities: config.sandboxes.capabilities,
|
|
791
|
+
capabilitiesFrom: 'sandboxes.capabilities',
|
|
496
792
|
},
|
|
497
793
|
};
|
|
498
794
|
if (sandboxRuntimes.length === 0 && config.entitlements.some((entitlement) => entitlement.sandboxes?.length)) {
|
|
@@ -512,6 +808,9 @@ async function startServer(config, environment, options = {}) {
|
|
|
512
808
|
cache: (0, path_1.join)(storage, 'cache'),
|
|
513
809
|
worker: options.worker,
|
|
514
810
|
sandboxes: { ...sandboxes, network: config.sandboxes.network },
|
|
811
|
+
...(config.backend.type === 'host' && config.backend.capabilities
|
|
812
|
+
? { capabilityRuntime: config.backend.capabilities.runtime }
|
|
813
|
+
: {}),
|
|
515
814
|
log,
|
|
516
815
|
};
|
|
517
816
|
const backend = config.backend;
|
|
@@ -519,7 +818,16 @@ async function startServer(config, environment, options = {}) {
|
|
|
519
818
|
(backend.type === 'kubernetes'
|
|
520
819
|
? new kubernetes_backend_1.KubernetesExecutor({ ...config, backend }, environment.processEnvs, executorOptions)
|
|
521
820
|
: new executor_1.HostExecutor(executorOptions));
|
|
821
|
+
const capacity = (0, capacity_1.cachedCapacity)(options.capacity ??
|
|
822
|
+
(executor instanceof kubernetes_backend_1.KubernetesExecutor
|
|
823
|
+
? executor.capacity()
|
|
824
|
+
: new capacity_1.HostCapacity((0, execute_docker_1.createDockerClient)(undefined), !environment.processEnvs.DOCKER_HOST)));
|
|
522
825
|
const sources = new sources_1.SourceStore((0, path_1.join)(storage, 'sources'));
|
|
826
|
+
const checks = new checks_1.CheckStore((0, path_1.join)(storage, 'checks'), { keep: (0, units_1.parseDuration)(config.runs.checksKeep) });
|
|
827
|
+
const sweepChecks = () => checks.sweep().catch((e) => log(`the old checks could not be removed: ${e instanceof Error ? e.message : e}`));
|
|
828
|
+
void sweepChecks();
|
|
829
|
+
const sweeping = setInterval(sweepChecks, CHECK_SWEEP_MS);
|
|
830
|
+
sweeping.unref();
|
|
523
831
|
const registry = new runs_1.RunRegistry({
|
|
524
832
|
executor,
|
|
525
833
|
maxConcurrent: config.runs.maxConcurrent,
|
|
@@ -529,21 +837,74 @@ async function startServer(config, environment, options = {}) {
|
|
|
529
837
|
limits: config.runs.limits,
|
|
530
838
|
maxDuration: (0, units_1.parseDuration)(config.runs.maxDuration),
|
|
531
839
|
maxLogBytes: config.runs.maxLogBytes,
|
|
840
|
+
up: {
|
|
841
|
+
maxTtl: (0, units_1.parseDuration)(config.runs.up.maxTtl),
|
|
842
|
+
perCaller: config.runs.up.limits.up,
|
|
843
|
+
asleep: config.runs.up.limits.asleep,
|
|
844
|
+
total: config.runs.up.total,
|
|
845
|
+
keepVolumes: config.runs.up.keepVolumes,
|
|
846
|
+
},
|
|
532
847
|
store: new run_store_1.FileRunStore((0, path_1.join)(storage, 'records')),
|
|
848
|
+
// what a check run passed, for `remote verify` to find
|
|
849
|
+
onFinished: (run, keys) => {
|
|
850
|
+
const passed = (0, checks_1.getCheckRecords)(run, keys);
|
|
851
|
+
if (passed.length > 0) {
|
|
852
|
+
checks
|
|
853
|
+
.save(passed)
|
|
854
|
+
.then(() => log(`checks run=${run.id} passed=[${passed.map((record) => record.task).join(',')}]`))
|
|
855
|
+
.catch((e) => log(`the checks of run ${run.id} were not kept: ${e instanceof Error ? e.message : e}`));
|
|
856
|
+
}
|
|
857
|
+
},
|
|
533
858
|
log,
|
|
534
859
|
});
|
|
860
|
+
// the executor hears which service runs sleep with their volumes kept, and removes what it
|
|
861
|
+
// kept for any other (RunExecutor.restored)
|
|
535
862
|
await registry.restore();
|
|
536
|
-
sandboxes.registry.useRuns({
|
|
537
|
-
holder: (id) => registry.holder(id),
|
|
538
|
-
stop: (id) => registry.stopSandbox(id, 'the sandbox was deleted'),
|
|
539
|
-
});
|
|
540
863
|
const proposals = new proposals_1.ProposalRegistry({
|
|
541
864
|
maxWaiting: config.approvals.maxWaiting,
|
|
542
865
|
timeout: (0, units_1.parseDuration)(config.approvals.timeout),
|
|
543
866
|
cooldown: (0, units_1.parseDuration)(config.approvals.cooldown),
|
|
544
867
|
});
|
|
868
|
+
const login = config.endpoints?.login;
|
|
869
|
+
const gateway = config.endpoints
|
|
870
|
+
? new endpoints_1.EndpointGateway(config, registry, {
|
|
871
|
+
keyFile: (0, path_1.join)(storage, 'endpoints', 'session.key'),
|
|
872
|
+
verifier,
|
|
873
|
+
...(login?.clientSecret
|
|
874
|
+
? { clientSecret: () => repos.serverSecret(login.clientSecret) }
|
|
875
|
+
: {}),
|
|
876
|
+
log,
|
|
877
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
878
|
+
...(options.now ? { now: options.now } : {}),
|
|
879
|
+
mayWake: (run) => refuseWake(config, repos, run),
|
|
880
|
+
// the pages of the dashboard read and do what they show through the API
|
|
881
|
+
dashboard: {
|
|
882
|
+
api: (principal, req, res, url) => handleApi(api, principal, req, res, url),
|
|
883
|
+
...(options.dashboardAssets ? { assets: options.dashboardAssets } : {}),
|
|
884
|
+
},
|
|
885
|
+
})
|
|
886
|
+
: undefined;
|
|
887
|
+
const api = {
|
|
888
|
+
config,
|
|
889
|
+
registry,
|
|
890
|
+
gateway,
|
|
891
|
+
definitions,
|
|
892
|
+
proposals,
|
|
893
|
+
sources,
|
|
894
|
+
sandboxes,
|
|
895
|
+
planning,
|
|
896
|
+
repos,
|
|
897
|
+
checks,
|
|
898
|
+
capacity,
|
|
899
|
+
log,
|
|
900
|
+
};
|
|
901
|
+
await gateway?.load();
|
|
902
|
+
sandboxes.registry.useRuns({
|
|
903
|
+
holder: (id) => registry.holder(id),
|
|
904
|
+
stop: (id) => registry.stopSandbox(id, 'the sandbox was deleted'),
|
|
905
|
+
});
|
|
545
906
|
const listener = (req, res) => {
|
|
546
|
-
handle(
|
|
907
|
+
handle(api, verifier, req, res).catch((e) => {
|
|
547
908
|
if (e instanceof http_2.HttpError) {
|
|
548
909
|
(0, http_2.send)(res, e.status, { error: e.message });
|
|
549
910
|
return;
|
|
@@ -563,11 +924,44 @@ async function startServer(config, environment, options = {}) {
|
|
|
563
924
|
environment.file.read(config.tls.cert),
|
|
564
925
|
environment.file.read(config.tls.key),
|
|
565
926
|
]);
|
|
566
|
-
|
|
927
|
+
// the hosts of endpoints have a wildcard certificate of their own
|
|
928
|
+
const endpointsTls = config.endpoints?.tls;
|
|
929
|
+
const endpointContext = endpointsTls
|
|
930
|
+
? (0, tls_1.createSecureContext)({
|
|
931
|
+
cert: await environment.file.read(endpointsTls.cert),
|
|
932
|
+
key: await environment.file.read(endpointsTls.key),
|
|
933
|
+
})
|
|
934
|
+
: undefined;
|
|
935
|
+
const domain = config.endpoints?.domain;
|
|
936
|
+
server = (0, https_1.createServer)({
|
|
937
|
+
cert,
|
|
938
|
+
key,
|
|
939
|
+
...(endpointContext && domain
|
|
940
|
+
? {
|
|
941
|
+
SNICallback: (servername, done) => done(null, servername.toLowerCase().endsWith(`.${domain}`) ? endpointContext : undefined),
|
|
942
|
+
}
|
|
943
|
+
: {}),
|
|
944
|
+
}, listener);
|
|
567
945
|
}
|
|
568
946
|
else {
|
|
569
947
|
server = (0, http_1.createServer)(listener);
|
|
570
948
|
}
|
|
949
|
+
// WebSockets of the apps behind endpoints; the API has none. An upgraded connection is
|
|
950
|
+
// no longer the HTTP server's, it goes when the server closes.
|
|
951
|
+
const upgraded = new Set();
|
|
952
|
+
server.on('upgrade', (req, socket, head) => {
|
|
953
|
+
upgraded.add(socket);
|
|
954
|
+
socket.once('close', () => upgraded.delete(socket));
|
|
955
|
+
const label = gateway?.hostOf(req);
|
|
956
|
+
if (!gateway || !label) {
|
|
957
|
+
socket.end('HTTP/1.1 404 Not Found\r\nconnection: close\r\ncontent-length: 0\r\n\r\n');
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
gateway.upgrade(label, req, socket, head).catch((e) => {
|
|
961
|
+
log(`error upgrade ${label}: ${e instanceof Error ? e.message : String(e)}`);
|
|
962
|
+
socket.destroy();
|
|
963
|
+
});
|
|
964
|
+
});
|
|
571
965
|
await new Promise((resolve, reject) => {
|
|
572
966
|
server.once('error', reject);
|
|
573
967
|
server.listen(config.listen.port, config.listen.host, () => {
|
|
@@ -576,11 +970,12 @@ async function startServer(config, environment, options = {}) {
|
|
|
576
970
|
});
|
|
577
971
|
});
|
|
578
972
|
const address = server.address();
|
|
973
|
+
gateway?.listening(address.port, Boolean(config.tls));
|
|
579
974
|
const host = address.family === 'IPv6' ? `[${address.address}]` : address.address;
|
|
580
975
|
return {
|
|
581
976
|
url: `${config.tls ? 'https' : 'http'}://${host}:${address.port}`,
|
|
582
977
|
reload: (next) => {
|
|
583
|
-
const readers = (0, server_config_1.
|
|
978
|
+
const readers = (0, server_config_1.getServerAccounts)(config);
|
|
584
979
|
next.entitlements.forEach((entitlement, index) => {
|
|
585
980
|
const iss = (entitlement.subject ?? entitlement.group ?? entitlement.claims)?.iss;
|
|
586
981
|
if (iss && !config.issuers.some((issuer) => issuer.issuer === iss)) {
|
|
@@ -600,11 +995,13 @@ async function startServer(config, environment, options = {}) {
|
|
|
600
995
|
return { restart };
|
|
601
996
|
},
|
|
602
997
|
close: async () => {
|
|
998
|
+
clearInterval(sweeping);
|
|
603
999
|
proposals.close();
|
|
604
1000
|
await registry.close();
|
|
605
1001
|
await new Promise((resolve, reject) => {
|
|
606
1002
|
server.close((err) => (err ? reject(err) : resolve()));
|
|
607
1003
|
server.closeAllConnections();
|
|
1004
|
+
upgraded.forEach((socket) => socket.destroy());
|
|
608
1005
|
});
|
|
609
1006
|
},
|
|
610
1007
|
};
|