hammerkit 2.0.0-next.4 → 2.0.0-next.6
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/dist/cache/explain.js +7 -5
- package/dist/dashboard/assets/graph-canvas-DLioOiRN.css +1 -0
- package/dist/dashboard/assets/graph-canvas-HmkW_-XJ.js +8 -0
- package/dist/dashboard/assets/index-gIBDC_m8.css +2 -0
- package/dist/dashboard/assets/index-vE22S5E_.js +17 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/logo.png +0 -0
- package/dist/executer/docker-task.js +3 -0
- package/dist/executer/execute-work-task.js +3 -1
- package/dist/executer/local-task.js +3 -0
- package/dist/executer/scheduler/enqueue-next.js +6 -1
- package/dist/executer/scheduler/state-key.js +71 -34
- package/dist/executer/watch-loop.js +18 -5
- package/dist/file/get-file-context.js +5 -1
- package/dist/kubernetes/claim-layout.js +10 -1
- package/dist/kubernetes/ensure-persistent-data.js +45 -5
- package/dist/kubernetes/follow-pod-logs.js +124 -27
- package/dist/kubernetes/network-policy.js +4 -4
- package/dist/kubernetes/restore-kubernetes-data.js +3 -1
- package/dist/kubernetes/store-kubernetes-data.js +3 -1
- package/dist/kubernetes/volumes.js +6 -6
- package/dist/optimizer/calculate-checksum.js +46 -3
- package/dist/optimizer/get-work-cache-stats.js +36 -24
- package/dist/planner/work-item-executions.js +27 -0
- package/dist/planner/work-runtime-kubernetes.js +33 -38
- package/dist/remote/remote-work-commands.js +11 -2
- package/dist/server/capacity.js +63 -0
- package/dist/server/dashboard.js +27 -9
- package/dist/server/endpoints.js +10 -1
- package/dist/server/executor.js +10 -0
- package/dist/server/oauth.js +16 -1
- package/dist/server/overview.js +80 -11
- package/dist/server/plan.js +16 -3
- package/dist/server/policy.js +9 -3
- package/dist/server/run-worker.js +37 -4
- package/dist/server/runs.js +74 -6
- package/dist/server/server-config.js +4 -1
- package/dist/server/server.js +45 -10
- package/dist/server/service-runs.js +8 -1
- package/dist/server/timings.js +23 -3
- package/dist/utils/limit.js +30 -0
- package/dist/utils/web-links.js +34 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-CjnKm5-3.css +0 -2
- package/dist/dashboard/assets/index-zPXa4klZ.js +0 -13
- package/dist/dashboard/favicon.svg +0 -1
|
@@ -519,16 +519,25 @@ function registerWorkCommands(remote, context) {
|
|
|
519
519
|
.command('tasks [server]')
|
|
520
520
|
.description('show what the past runs of each task took, over the runs the server keeps: how often it ran, failed and came from the cache, the median and 90th percentile of its time, and how long its runs waited for a place')
|
|
521
521
|
.addOption(new commander_1.Option('--mine', 'only your runs'))
|
|
522
|
+
.addOption(new commander_1.Option('--since <when>', 'only the runs created since then: back from now, such as 24h or 7d, or a time'))
|
|
523
|
+
.addOption(new commander_1.Option('--until <when>', 'only the runs created until then: back from now, or a time'))
|
|
522
524
|
.addOption(new commander_1.Option('--json', 'print it as the server answers it'))
|
|
523
525
|
.action(guarded('Remote tasks was not successful', async (name, options) => {
|
|
524
526
|
const { name: serverName, server, token } = await connect(name);
|
|
525
|
-
const
|
|
527
|
+
const query = new URLSearchParams({ scope: options.mine ? 'mine' : 'visible' });
|
|
528
|
+
if (options.since) {
|
|
529
|
+
query.set('since', options.since);
|
|
530
|
+
}
|
|
531
|
+
if (options.until) {
|
|
532
|
+
query.set('until', options.until);
|
|
533
|
+
}
|
|
534
|
+
const { tasks } = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/tasks?${query}`)).body;
|
|
526
535
|
if (options.json) {
|
|
527
536
|
environment.stdout.write(`${JSON.stringify(tasks, null, 2)}\n`);
|
|
528
537
|
return;
|
|
529
538
|
}
|
|
530
539
|
if (tasks.length === 0) {
|
|
531
|
-
environment.stdout.write(`${serverName}: no task ran in the runs it keeps\n`);
|
|
540
|
+
environment.stdout.write(`${serverName}: no task ran in the runs it keeps${options.since || options.until ? ' of that period' : ''}\n`);
|
|
532
541
|
return;
|
|
533
542
|
}
|
|
534
543
|
for (const line of (0, remote_status_1.describeTaskStats)(tasks)) {
|
package/dist/server/capacity.js
CHANGED
|
@@ -4,6 +4,9 @@ exports.formatMemory = exports.formatCpu = exports.HostCapacity = exports.Kubern
|
|
|
4
4
|
const os_1 = require("os");
|
|
5
5
|
const log_1 = require("../log");
|
|
6
6
|
const apply_1 = require("../kubernetes/apply");
|
|
7
|
+
const job_placement_1 = require("../kubernetes/job-placement");
|
|
8
|
+
const volumes_1 = require("../kubernetes/volumes");
|
|
9
|
+
const network_policy_1 = require("../kubernetes/network-policy");
|
|
7
10
|
const UNITS = {
|
|
8
11
|
n: 1e-9,
|
|
9
12
|
u: 1e-6,
|
|
@@ -90,6 +93,17 @@ function why(what, e, rule) {
|
|
|
90
93
|
}
|
|
91
94
|
return `${what}: ${(0, log_1.getErrorMessage)(e)}`;
|
|
92
95
|
}
|
|
96
|
+
// `why`, in a few words
|
|
97
|
+
function briefly(e, what) {
|
|
98
|
+
const code = (0, apply_1.statusCodeOf)(e);
|
|
99
|
+
if (code === 403) {
|
|
100
|
+
return what === 'nodes' ? 'may not list nodes' : 'may not read metrics';
|
|
101
|
+
}
|
|
102
|
+
if (code === 404 && what === 'metrics') {
|
|
103
|
+
return 'no metrics API';
|
|
104
|
+
}
|
|
105
|
+
return what === 'nodes' ? 'nodes not readable' : 'metrics not readable';
|
|
106
|
+
}
|
|
93
107
|
const ENDED = ['Succeeded', 'Failed'];
|
|
94
108
|
// On Kubernetes: per namespace of the server, its pods' requests, its quota and its usage.
|
|
95
109
|
class KubernetesCapacity {
|
|
@@ -107,6 +121,37 @@ class KubernetesCapacity {
|
|
|
107
121
|
catch (e) {
|
|
108
122
|
problems.push(why('the namespaces of the server', e));
|
|
109
123
|
}
|
|
124
|
+
const unknown = {};
|
|
125
|
+
// the runs by the value of their label, and what the pods of each run or sandbox take
|
|
126
|
+
const byLabel = new Map(runs.map((run) => [(0, job_placement_1.getRunLabelValue)(run.id), run]));
|
|
127
|
+
const uses = new Map();
|
|
128
|
+
const useOf = (namespace, labels, owner) => {
|
|
129
|
+
const run = byLabel.get(labels?.[job_placement_1.RUN_LABEL] ?? '');
|
|
130
|
+
const sandbox = labels?.[volumes_1.SANDBOX_LABEL] ?? labels?.[network_policy_1.PROXY_OF_SANDBOX_LABEL];
|
|
131
|
+
const key = run ? `run ${run.id}` : sandbox ? `sandbox ${sandbox}` : `other ${namespace}`;
|
|
132
|
+
let use = uses.get(key);
|
|
133
|
+
if (!use) {
|
|
134
|
+
use = run
|
|
135
|
+
? {
|
|
136
|
+
kind: 'run',
|
|
137
|
+
run: run.id,
|
|
138
|
+
tasks: run.tasks,
|
|
139
|
+
...(run.up ? { up: { name: run.up.name, services: run.up.services } } : {}),
|
|
140
|
+
repo: run.source.type === 'upload' ? 'an upload' : run.source.repo,
|
|
141
|
+
owner: { iss: run.owner.iss, sub: run.owner.sub, ...(run.owner.name ? { name: run.owner.name } : {}) },
|
|
142
|
+
pods: 0,
|
|
143
|
+
requested: { cpu: 0, memory: 0 },
|
|
144
|
+
}
|
|
145
|
+
: {
|
|
146
|
+
kind: sandbox ? 'sandbox' : 'other',
|
|
147
|
+
...(owner ? { owner } : {}),
|
|
148
|
+
pods: 0,
|
|
149
|
+
requested: { cpu: 0, memory: 0 },
|
|
150
|
+
};
|
|
151
|
+
uses.set(key, use);
|
|
152
|
+
}
|
|
153
|
+
return use;
|
|
154
|
+
};
|
|
110
155
|
const said = new Set();
|
|
111
156
|
const once = (text) => {
|
|
112
157
|
if (!said.has(text)) {
|
|
@@ -123,6 +168,8 @@ class KubernetesCapacity {
|
|
|
123
168
|
// a namespace whose pods could be listed and that has none asks for nothing
|
|
124
169
|
requested: { cpu: 0, memory: 0 },
|
|
125
170
|
};
|
|
171
|
+
// the use of each pod that runs, by its name
|
|
172
|
+
const usesOfPods = new Map();
|
|
126
173
|
try {
|
|
127
174
|
const pods = await this.instance.coreApi.listNamespacedPod({ namespace });
|
|
128
175
|
for (const pod of pods.items) {
|
|
@@ -130,9 +177,13 @@ class KubernetesCapacity {
|
|
|
130
177
|
continue;
|
|
131
178
|
}
|
|
132
179
|
group.pods++;
|
|
180
|
+
const use = useOf(namespace, pod.metadata?.labels, group.owner);
|
|
181
|
+
usesOfPods.set(pod.metadata?.name ?? '', use);
|
|
182
|
+
use.pods++;
|
|
133
183
|
for (const container of pod.spec?.containers ?? []) {
|
|
134
184
|
const requests = container.resources?.requests ?? {};
|
|
135
185
|
add(group.requested, parseQuantity(requests.cpu) ?? 0, parseQuantity(requests.memory) ?? 0);
|
|
186
|
+
add(use.requested, parseQuantity(requests.cpu) ?? 0, parseQuantity(requests.memory) ?? 0);
|
|
136
187
|
}
|
|
137
188
|
}
|
|
138
189
|
}
|
|
@@ -162,13 +213,19 @@ class KubernetesCapacity {
|
|
|
162
213
|
const metrics = await this.instance.metrics.getPodMetrics(namespace);
|
|
163
214
|
const used = { cpu: 0, memory: 0 };
|
|
164
215
|
for (const pod of metrics.items) {
|
|
216
|
+
const use = usesOfPods.get(pod.metadata?.name ?? '');
|
|
165
217
|
for (const container of pod.containers) {
|
|
166
218
|
add(used, parseQuantity(container.usage.cpu), parseQuantity(container.usage.memory));
|
|
219
|
+
if (use) {
|
|
220
|
+
use.used ?? (use.used = { cpu: 0, memory: 0 });
|
|
221
|
+
add(use.used, parseQuantity(container.usage.cpu), parseQuantity(container.usage.memory));
|
|
222
|
+
}
|
|
167
223
|
}
|
|
168
224
|
}
|
|
169
225
|
group.used = used;
|
|
170
226
|
}
|
|
171
227
|
catch (e) {
|
|
228
|
+
unknown.used ?? (unknown.used = briefly(e, 'metrics'));
|
|
172
229
|
once(why('what the pods use (metrics)', e, 'list on pods.metrics.k8s.io in them (runRole, see the backend docs)'));
|
|
173
230
|
}
|
|
174
231
|
}
|
|
@@ -204,11 +261,13 @@ class KubernetesCapacity {
|
|
|
204
261
|
machines.used = nodesUsed;
|
|
205
262
|
}
|
|
206
263
|
catch (e) {
|
|
264
|
+
unknown.used ?? (unknown.used = briefly(e, 'metrics'));
|
|
207
265
|
once(why('what the nodes use (metrics)', e, 'list on nodes.metrics.k8s.io (see the backend docs)'));
|
|
208
266
|
}
|
|
209
267
|
}
|
|
210
268
|
}
|
|
211
269
|
catch (e) {
|
|
270
|
+
unknown.total = briefly(e, 'nodes');
|
|
212
271
|
// listing nodes is optional, and many clusters do not let a workload do it
|
|
213
272
|
if ((0, apply_1.statusCodeOf)(e) === 403) {
|
|
214
273
|
once('the nodes: not shown, the server may not list nodes (optional, see the backend docs)');
|
|
@@ -223,7 +282,11 @@ class KubernetesCapacity {
|
|
|
223
282
|
...(machines ? { machines } : {}),
|
|
224
283
|
runs: total,
|
|
225
284
|
groups,
|
|
285
|
+
uses: [...uses.values()].sort((a, b) => (b.used?.cpu ?? 0) - (a.used?.cpu ?? 0) ||
|
|
286
|
+
(b.requested.cpu ?? 0) - (a.requested.cpu ?? 0) ||
|
|
287
|
+
(b.requested.memory ?? 0) - (a.requested.memory ?? 0)),
|
|
226
288
|
problems,
|
|
289
|
+
...(Object.keys(unknown).length > 0 ? { unknown } : {}),
|
|
227
290
|
};
|
|
228
291
|
}
|
|
229
292
|
}
|
package/dist/server/dashboard.js
CHANGED
|
@@ -5,12 +5,14 @@ const crypto_1 = require("crypto");
|
|
|
5
5
|
const promises_1 = require("fs/promises");
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const http_1 = require("./http");
|
|
8
|
+
const oauth_1 = require("./oauth");
|
|
8
9
|
const policy_1 = require("./policy");
|
|
9
10
|
const overview_1 = require("./overview");
|
|
10
11
|
// Approving and publishing take a sign-in of the last minutes: the groups of a session are
|
|
11
12
|
// those of its sign-in, and somebody taken out of a group must not decide for hours.
|
|
12
13
|
exports.DECIDE_WITHIN_MS = 15 * 60000;
|
|
13
14
|
const RUN = 'run_[A-Za-z0-9_-]{16}';
|
|
15
|
+
const SANDBOX = 'sbx_[A-Za-z0-9_-]{16}';
|
|
14
16
|
// a hash of a build file version, checked by the API
|
|
15
17
|
const HASH = '[A-Za-z0-9:_-]{1,200}';
|
|
16
18
|
const path = (pattern) => new RegExp(`^/v1/${pattern}$`);
|
|
@@ -28,9 +30,19 @@ const READS = [
|
|
|
28
30
|
'proposals',
|
|
29
31
|
`proposals/${HASH}`,
|
|
30
32
|
`proposals/${HASH}/files`,
|
|
33
|
+
// the build files, and the page of the version a task or run ran
|
|
34
|
+
'definitions',
|
|
35
|
+
`definitions/${HASH}`,
|
|
36
|
+
`definitions/${HASH}/files`,
|
|
37
|
+
// the person's sandboxes and what changed in them; not their files, nothing that writes
|
|
38
|
+
'sandboxes',
|
|
39
|
+
`sandboxes/${SANDBOX}`,
|
|
40
|
+
`sandboxes/${SANDBOX}/changes`,
|
|
31
41
|
].map(path);
|
|
32
42
|
// what the buttons do
|
|
33
|
-
const ACTIONS = [`runs/${RUN}/(cancel|extend)`].map(path);
|
|
43
|
+
const ACTIONS = [`runs/${RUN}/(cancel|extend|wake)`].map(path);
|
|
44
|
+
// what the delete buttons remove: a sandbox of the person's
|
|
45
|
+
const REMOVALS = [`sandboxes/${SANDBOX}`].map(path);
|
|
34
46
|
// what needs a recent sign-in
|
|
35
47
|
const DECISIONS = [`approvals/${RUN}/(approve|reject)`, `proposals/${HASH}/(approve|reject)`].map(path);
|
|
36
48
|
exports.HEADERS = {
|
|
@@ -52,8 +64,8 @@ const TYPES = {
|
|
|
52
64
|
};
|
|
53
65
|
async function handleDashboard(context, principal, req, res, url) {
|
|
54
66
|
const now = context.now ?? Date.now;
|
|
55
|
-
const
|
|
56
|
-
const
|
|
67
|
+
const viewer = (0, overview_1.getViewer)(context.config, principal);
|
|
68
|
+
const { grant } = viewer;
|
|
57
69
|
const pathname = url.pathname;
|
|
58
70
|
for (const [name, value] of Object.entries(exports.HEADERS)) {
|
|
59
71
|
res.setHeader(name, value);
|
|
@@ -78,9 +90,11 @@ async function api(context, principal, viewer, now, req, res, url) {
|
|
|
78
90
|
if (method !== 'GET') {
|
|
79
91
|
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
80
92
|
}
|
|
93
|
+
const name = (0, oauth_1.displayName)(principal);
|
|
81
94
|
const session = {
|
|
82
95
|
iss: principal.iss,
|
|
83
96
|
sub: principal.sub,
|
|
97
|
+
...(name ? { name } : {}),
|
|
84
98
|
observe: grant.observe,
|
|
85
99
|
seesOthers: grant.observe || (0, overview_1.seesActivity)(viewer),
|
|
86
100
|
publishes: (0, policy_1.mayPublish)(grant),
|
|
@@ -103,15 +117,19 @@ async function api(context, principal, viewer, now, req, res, url) {
|
|
|
103
117
|
const read = READS.some((pattern) => pattern.test(target));
|
|
104
118
|
const action = ACTIONS.some((pattern) => pattern.test(target));
|
|
105
119
|
const decision = DECISIONS.some((pattern) => pattern.test(target));
|
|
106
|
-
|
|
120
|
+
const removal = REMOVALS.some((pattern) => pattern.test(target));
|
|
121
|
+
if (!read && !action && !decision && !removal) {
|
|
107
122
|
return (0, http_1.send)(res, 404, { error: 'not found' });
|
|
108
123
|
}
|
|
124
|
+
const allow = [read ? 'GET' : '', action || decision ? 'POST' : '', removal ? 'DELETE' : '']
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join(', ');
|
|
109
127
|
if (method === 'GET' || method === 'HEAD') {
|
|
110
128
|
if (!read) {
|
|
111
|
-
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow
|
|
129
|
+
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow });
|
|
112
130
|
}
|
|
113
131
|
}
|
|
114
|
-
else if (method === 'POST' && (action || decision)) {
|
|
132
|
+
else if ((method === 'POST' && (action || decision)) || (method === 'DELETE' && removal)) {
|
|
115
133
|
// a page of another site can not set the header, nor read the token
|
|
116
134
|
const given = Buffer.from(String(req.headers['x-hammerkit-csrf'] ?? ''));
|
|
117
135
|
const expected = Buffer.from(csrf);
|
|
@@ -128,7 +146,7 @@ async function api(context, principal, viewer, now, req, res, url) {
|
|
|
128
146
|
context.log(`dashboard ${method} ${target} iss=${principal.iss} sub=${principal.sub}`);
|
|
129
147
|
}
|
|
130
148
|
else {
|
|
131
|
-
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow
|
|
149
|
+
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow });
|
|
132
150
|
}
|
|
133
151
|
const forwarded = new URL(`${target}${url.search}`, 'http://localhost');
|
|
134
152
|
return context.api(principal, req, res, forwarded);
|
|
@@ -165,8 +183,8 @@ async function asset(context, req, res, pathname) {
|
|
|
165
183
|
const root = context.assets ?? (0, path_1.join)(__dirname, '..', 'dashboard');
|
|
166
184
|
const file = /^\/assets\/[A-Za-z0-9_-][A-Za-z0-9._-]{0,200}$/.test(pathname)
|
|
167
185
|
? pathname.slice(1)
|
|
168
|
-
: pathname === '/
|
|
169
|
-
? '
|
|
186
|
+
: pathname === '/logo.png'
|
|
187
|
+
? 'logo.png'
|
|
170
188
|
: pathname.startsWith('/assets/') || pathname.startsWith('/.')
|
|
171
189
|
? undefined
|
|
172
190
|
: 'index.html';
|
package/dist/server/endpoints.js
CHANGED
|
@@ -285,7 +285,15 @@ class EndpointGateway {
|
|
|
285
285
|
return null;
|
|
286
286
|
}
|
|
287
287
|
const session = this.readSession(label, req.headers.cookie);
|
|
288
|
-
return session
|
|
288
|
+
return session
|
|
289
|
+
? {
|
|
290
|
+
iss: session.iss,
|
|
291
|
+
sub: session.sub,
|
|
292
|
+
groups: session.groups,
|
|
293
|
+
claims: session.claims,
|
|
294
|
+
...(session.name ? { name: session.name } : {}),
|
|
295
|
+
}
|
|
296
|
+
: null;
|
|
289
297
|
}
|
|
290
298
|
// Whether the browser sends the request for another page than the endpoint's own: an
|
|
291
299
|
// upgrade from another origin, or a request a page of another site makes (a frame too),
|
|
@@ -652,6 +660,7 @@ ${run.state === 'up'
|
|
|
652
660
|
host: label,
|
|
653
661
|
iss: issued.principal.iss,
|
|
654
662
|
sub: issued.principal.sub,
|
|
663
|
+
...((0, oauth_1.displayName)(issued.principal) ? { name: (0, oauth_1.displayName)(issued.principal) } : {}),
|
|
655
664
|
groups: issued.principal.groups,
|
|
656
665
|
claims: this.matchedClaims(issued.principal),
|
|
657
666
|
exp,
|
package/dist/server/executor.js
CHANGED
|
@@ -220,6 +220,7 @@ class HostExecutor {
|
|
|
220
220
|
let claim;
|
|
221
221
|
// the files of the sandbox the tasks work on
|
|
222
222
|
let work;
|
|
223
|
+
const sourceStarted = new Date();
|
|
223
224
|
if (source.type === 'sandbox' && source.check) {
|
|
224
225
|
// a check works on the files a push of the sandbox commits, copied out of it: never
|
|
225
226
|
// on the sandbox, whose ignored files (node_modules, outputs) its owner writes
|
|
@@ -303,12 +304,17 @@ class HostExecutor {
|
|
|
303
304
|
sandboxNetwork: network,
|
|
304
305
|
};
|
|
305
306
|
}
|
|
307
|
+
emitPhase(context, 'source', sourceStarted);
|
|
306
308
|
const home = (0, path_1.join)(root, 'home');
|
|
307
309
|
await (0, promises_1.mkdir)(home, { recursive: true });
|
|
308
310
|
const cache = (0, path_1.join)(this.options.cache, cacheNamespace(run));
|
|
309
311
|
await (0, promises_1.mkdir)(cache, { recursive: true });
|
|
310
312
|
const placement = this.options.placement;
|
|
313
|
+
const namespaceStarted = new Date();
|
|
311
314
|
const placed = placement ? await placement.prepare(run, context, { root: work ?? cwd, claim }) : undefined;
|
|
315
|
+
if (placed) {
|
|
316
|
+
emitPhase(context, 'namespace', namespaceStarted);
|
|
317
|
+
}
|
|
312
318
|
try {
|
|
313
319
|
await this.runInProcess(worker, {
|
|
314
320
|
...input,
|
|
@@ -504,4 +510,8 @@ class HostExecutor {
|
|
|
504
510
|
}
|
|
505
511
|
}
|
|
506
512
|
exports.HostExecutor = HostExecutor;
|
|
513
|
+
// A step of a run before its tasks, for its timeline (RunPhase)
|
|
514
|
+
function emitPhase(context, phase, startedAt) {
|
|
515
|
+
context.emit({ type: 'phase', phase, startedAt: startedAt.toISOString(), finishedAt: new Date().toISOString() });
|
|
516
|
+
}
|
|
507
517
|
//# sourceMappingURL=executor.js.map
|
package/dist/server/oauth.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TokenVerifier = exports.quote = exports.TokenError = void 0;
|
|
3
|
+
exports.TokenVerifier = exports.quote = exports.TokenError = exports.displayName = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* How pages name a person: the `name` of their token, else `preferred_username`, else `email`
|
|
7
|
+
* (with the scopes `profile email`); none with `openid` alone, which carries the subject only.
|
|
8
|
+
*/
|
|
9
|
+
function displayName(principal) {
|
|
10
|
+
const claims = principal.claims ?? {};
|
|
11
|
+
for (const value of [principal.name, claims.name, claims.preferred_username, claims.email]) {
|
|
12
|
+
const trimmed = value?.trim();
|
|
13
|
+
if (trimmed) {
|
|
14
|
+
return trimmed.slice(0, 100);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
exports.displayName = displayName;
|
|
5
20
|
// The reason is for the operator's log, never for the caller.
|
|
6
21
|
class TokenError extends Error {
|
|
7
22
|
}
|
package/dist/server/overview.js
CHANGED
|
@@ -5,6 +5,7 @@ const policy_1 = require("./policy");
|
|
|
5
5
|
const runs_1 = require("./runs");
|
|
6
6
|
const service_runs_1 = require("./service-runs");
|
|
7
7
|
const repo_1 = require("./repo");
|
|
8
|
+
const units_1 = require("../utils/units");
|
|
8
9
|
var wait_reason_1 = require("./wait-reason");
|
|
9
10
|
Object.defineProperty(exports, "describeWait", { enumerable: true, get: function () { return wait_reason_1.describeWait; } });
|
|
10
11
|
function getViewer(config, principal) {
|
|
@@ -12,6 +13,7 @@ function getViewer(config, principal) {
|
|
|
12
13
|
principal,
|
|
13
14
|
grant: (0, policy_1.getGrant)(config.entitlements, principal),
|
|
14
15
|
activity: config.runs.activity === 'everyone',
|
|
16
|
+
nameOf: (owner) => (0, policy_1.subjectName)(config.entitlements, owner),
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
exports.getViewer = getViewer;
|
|
@@ -21,9 +23,14 @@ function runActions(viewer, run) {
|
|
|
21
23
|
}
|
|
22
24
|
const mine = owns(viewer, run);
|
|
23
25
|
const may = {
|
|
24
|
-
...(mine || decides(viewer, run)
|
|
26
|
+
...(mine || decides(viewer, run) || (run.up && (0, service_runs_1.mayPublishEndpoints)(viewer.grant, run))
|
|
27
|
+
? { stop: true }
|
|
28
|
+
: {}),
|
|
25
29
|
...(run.up && run.upAt && (mine || (0, service_runs_1.mayOpenEndpoints)(viewer.grant, run)) ? { extend: true } : {}),
|
|
26
30
|
...(mayDecide(viewer, run) ? { decide: true } : {}),
|
|
31
|
+
...(run.up && run.state === 'asleep' && (mine || (0, service_runs_1.mayOpenEndpoints)(viewer.grant, run))
|
|
32
|
+
? { wake: true }
|
|
33
|
+
: {}),
|
|
27
34
|
};
|
|
28
35
|
return Object.keys(may).length > 0 ? may : undefined;
|
|
29
36
|
}
|
|
@@ -85,7 +92,8 @@ function mayRead(viewer, run) {
|
|
|
85
92
|
return owns(viewer, run) || viewer.grant.observe || decides(viewer, run);
|
|
86
93
|
}
|
|
87
94
|
exports.mayRead = mayRead;
|
|
88
|
-
|
|
95
|
+
// `entryOf` names where in its repository the build file of a version is.
|
|
96
|
+
function toListedRun(viewer, run, queue, entryOf) {
|
|
89
97
|
const { owner, results, plan, options, up, error, ...rest } = run;
|
|
90
98
|
const mine = owns(viewer, run);
|
|
91
99
|
const may = runActions(viewer, run);
|
|
@@ -100,9 +108,11 @@ function toListedRun(viewer, run, queue) {
|
|
|
100
108
|
...(result.cache ? { cached: result.cache.hit } : {}),
|
|
101
109
|
},
|
|
102
110
|
]));
|
|
111
|
+
const entry = entryOf?.(run.definition);
|
|
112
|
+
const name = owner.name ?? viewer.nameOf?.(owner);
|
|
103
113
|
return {
|
|
104
114
|
...rest,
|
|
105
|
-
...(mine ? { plan, options } : { owner: { iss: owner.iss, sub: owner.sub } }),
|
|
115
|
+
...(mine ? { plan, options } : { owner: { iss: owner.iss, sub: owner.sub, ...(name ? { name } : {}) } }),
|
|
106
116
|
...(up
|
|
107
117
|
? {
|
|
108
118
|
up: {
|
|
@@ -116,6 +126,7 @@ function toListedRun(viewer, run, queue) {
|
|
|
116
126
|
...(queue ? { queue } : {}),
|
|
117
127
|
...(may ? { may } : {}),
|
|
118
128
|
...(!mine && mayRead(viewer, run) ? { readable: true } : {}),
|
|
129
|
+
...(entry ? { entry } : {}),
|
|
119
130
|
};
|
|
120
131
|
}
|
|
121
132
|
exports.toListedRun = toListedRun;
|
|
@@ -141,7 +152,8 @@ function queuePlace(registry, viewer, run) {
|
|
|
141
152
|
return { position: queue.position, waitsFor: visibleReason(queue.waitsFor, visible) };
|
|
142
153
|
}
|
|
143
154
|
exports.queuePlace = queuePlace;
|
|
144
|
-
function getOverview(snapshot, usage, viewer) {
|
|
155
|
+
function getOverview(snapshot, usage, viewer, entryOf) {
|
|
156
|
+
const list = (run, place) => toListedRun(viewer, run, place, entryOf);
|
|
145
157
|
const runs = new Map();
|
|
146
158
|
for (const run of [
|
|
147
159
|
...snapshot.running,
|
|
@@ -160,7 +172,7 @@ function getOverview(snapshot, usage, viewer) {
|
|
|
160
172
|
return {
|
|
161
173
|
...place,
|
|
162
174
|
mine: owns(viewer, entry.run),
|
|
163
|
-
...(maySee(viewer, entry.run) ? { run:
|
|
175
|
+
...(maySee(viewer, entry.run) ? { run: list(entry.run, place) } : {}),
|
|
164
176
|
};
|
|
165
177
|
});
|
|
166
178
|
const running = snapshot.running.filter((run) => maySee(viewer, run));
|
|
@@ -170,19 +182,19 @@ function getOverview(snapshot, usage, viewer) {
|
|
|
170
182
|
places: {
|
|
171
183
|
max: snapshot.places.max,
|
|
172
184
|
used: snapshot.places.used,
|
|
173
|
-
runs: running.map((run) =>
|
|
185
|
+
runs: running.map((run) => list(run)),
|
|
174
186
|
others: snapshot.running.length - running.length,
|
|
175
187
|
},
|
|
176
188
|
queue,
|
|
177
189
|
approvals: {
|
|
178
190
|
waiting: snapshot.awaiting.runs.length,
|
|
179
191
|
max: snapshot.awaiting.max,
|
|
180
|
-
runs: snapshot.awaiting.runs.filter((run) => maySee(viewer, run)).map((run) =>
|
|
192
|
+
runs: snapshot.awaiting.runs.filter((run) => maySee(viewer, run)).map((run) => list(run)),
|
|
181
193
|
},
|
|
182
194
|
services: {
|
|
183
195
|
open: snapshot.services.taken,
|
|
184
196
|
...(snapshot.services.max !== undefined ? { max: snapshot.services.max } : {}),
|
|
185
|
-
runs: snapshot.services.runs.filter((run) => maySee(viewer, run)).map((run) =>
|
|
197
|
+
runs: snapshot.services.runs.filter((run) => maySee(viewer, run)).map((run) => list(run)),
|
|
186
198
|
},
|
|
187
199
|
limits: {
|
|
188
200
|
waiting: meter(usage.waiting, limits?.waiting),
|
|
@@ -209,7 +221,7 @@ const STATES = [
|
|
|
209
221
|
'rejected',
|
|
210
222
|
];
|
|
211
223
|
// `scope`, `state` (a comma separated list) and `kind` of GET /v1/runs.
|
|
212
|
-
function parseRunFilter(params, grant) {
|
|
224
|
+
function parseRunFilter(params, grant, now = Date.now()) {
|
|
213
225
|
const scope = params.get('scope') ?? 'mine';
|
|
214
226
|
if (scope !== 'mine' && scope !== 'visible' && scope !== 'all') {
|
|
215
227
|
throw new Error('scope: mine, visible or all');
|
|
@@ -234,9 +246,35 @@ function parseRunFilter(params, grant) {
|
|
|
234
246
|
if (id !== undefined && !/^run_[A-Za-z0-9_-]{16}$/.test(id)) {
|
|
235
247
|
throw new Error('id: the id of a run, run_...');
|
|
236
248
|
}
|
|
237
|
-
|
|
249
|
+
const since = parseMoment(params.get('since'), now, 'since');
|
|
250
|
+
const until = parseMoment(params.get('until'), now, 'until');
|
|
251
|
+
if (since !== undefined && until !== undefined && until < since) {
|
|
252
|
+
throw new Error('until: after since');
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
scope,
|
|
256
|
+
...(states ? { states } : {}),
|
|
257
|
+
...(kind ? { kind } : {}),
|
|
258
|
+
...(id ? { id } : {}),
|
|
259
|
+
...(since !== undefined ? { since } : {}),
|
|
260
|
+
...(until !== undefined ? { until } : {}),
|
|
261
|
+
};
|
|
238
262
|
}
|
|
239
263
|
exports.parseRunFilter = parseRunFilter;
|
|
264
|
+
// `7d` back from now, or a time such as `2026-11-01T08:00:00Z`, in ms
|
|
265
|
+
function parseMoment(value, now, name) {
|
|
266
|
+
if (value === null) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
if (/^(\d+(\.\d+)?(ms|s|m|h|d))+$/.test(value)) {
|
|
270
|
+
return now - (0, units_1.parseDuration)(value);
|
|
271
|
+
}
|
|
272
|
+
const at = /^\d{4}-\d{2}-\d{2}/.test(value) ? Date.parse(value) : NaN;
|
|
273
|
+
if (!Number.isFinite(at)) {
|
|
274
|
+
throw new Error(`${name}: a duration back from now, such as 1h, 24h or 7d, or a time, such as 2026-11-01T08:00:00Z`);
|
|
275
|
+
}
|
|
276
|
+
return at;
|
|
277
|
+
}
|
|
240
278
|
class FilterRefused extends Error {
|
|
241
279
|
}
|
|
242
280
|
exports.FilterRefused = FilterRefused;
|
|
@@ -244,7 +282,9 @@ function filterRuns(runs, viewer, filter) {
|
|
|
244
282
|
return runs.filter((run) => (filter.scope === 'mine' ? owns(viewer, run) : maySee(viewer, run)) &&
|
|
245
283
|
(!filter.id || run.id === filter.id) &&
|
|
246
284
|
(!filter.states || filter.states.includes(run.state)) &&
|
|
247
|
-
(!filter.kind || (filter.kind === 'services') === !!run.up)
|
|
285
|
+
(!filter.kind || (filter.kind === 'services') === !!run.up) &&
|
|
286
|
+
(filter.since === undefined || Date.parse(run.createdAt) >= filter.since) &&
|
|
287
|
+
(filter.until === undefined || Date.parse(run.createdAt) <= filter.until));
|
|
248
288
|
}
|
|
249
289
|
exports.filterRuns = filterRuns;
|
|
250
290
|
// The machines and what the runs take, with whose the namespaces are where the caller may
|
|
@@ -255,6 +295,35 @@ function visibleCapacity(capacity, viewer) {
|
|
|
255
295
|
...capacity,
|
|
256
296
|
// what the cluster answered is for operators; others learn that something is missing
|
|
257
297
|
problems: viewer.grant.observe || capacity.problems.length === 0 ? capacity.problems : ['some of it could not be read'],
|
|
298
|
+
...(capacity.unknown
|
|
299
|
+
? {
|
|
300
|
+
unknown: everyone
|
|
301
|
+
? capacity.unknown
|
|
302
|
+
: {
|
|
303
|
+
...(capacity.unknown.total ? { total: 'not readable' } : {}),
|
|
304
|
+
...(capacity.unknown.used ? { used: 'not readable' } : {}),
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
: {}),
|
|
308
|
+
// what others' runs and sandboxes take shows, not what they are
|
|
309
|
+
...(capacity.uses
|
|
310
|
+
? {
|
|
311
|
+
uses: capacity.uses.map((use) => {
|
|
312
|
+
const mine = use.owner?.iss === viewer.principal.iss && use.owner?.sub === viewer.principal.sub;
|
|
313
|
+
const name = use.owner && !use.owner.name ? viewer.nameOf?.(use.owner) : undefined;
|
|
314
|
+
return everyone || mine || !use.owner
|
|
315
|
+
? name && use.owner
|
|
316
|
+
? { ...use, owner: { ...use.owner, name } }
|
|
317
|
+
: use
|
|
318
|
+
: {
|
|
319
|
+
kind: use.kind,
|
|
320
|
+
pods: use.pods,
|
|
321
|
+
requested: use.requested,
|
|
322
|
+
...(use.used ? { used: use.used } : {}),
|
|
323
|
+
};
|
|
324
|
+
}),
|
|
325
|
+
}
|
|
326
|
+
: {}),
|
|
258
327
|
groups: capacity.groups.map((group) => {
|
|
259
328
|
const mine = group.owner?.iss === viewer.principal.iss && group.owner?.sub === viewer.principal.sub;
|
|
260
329
|
if (everyone || mine || !group.owner) {
|
package/dist/server/plan.js
CHANGED
|
@@ -294,6 +294,13 @@ function describeMountSource(mount) {
|
|
|
294
294
|
const home = (0, normalize_path_1.getHomeReference)(mount.mount.split(':')[0]);
|
|
295
295
|
return home ? `${mount.localPath} (${home} is the home directory)` : mount.localPath;
|
|
296
296
|
}
|
|
297
|
+
// where the build file of an item is: in the bundle, or in the repository of a git include
|
|
298
|
+
function declaredIn(root, scope) {
|
|
299
|
+
if (scope.remote) {
|
|
300
|
+
return `${scope.remote.git} ${(0, path_1.relative)(scope.remote.root, scope.fileName).split(path_1.sep).join('/')}`;
|
|
301
|
+
}
|
|
302
|
+
return (0, path_1.relative)(root, scope.fileName).split(path_1.sep).join('/');
|
|
303
|
+
}
|
|
297
304
|
function toPlan(root, item) {
|
|
298
305
|
const data = item.data;
|
|
299
306
|
const secrets = secretsOf(item).map(describeSecret);
|
|
@@ -322,6 +329,7 @@ function toPlan(root, item) {
|
|
|
322
329
|
const isTask = data.type === 'local-task' || data.type === 'container-task';
|
|
323
330
|
return {
|
|
324
331
|
kind: isTask ? 'task' : 'service',
|
|
332
|
+
file: declaredIn(root, data.scope),
|
|
325
333
|
...(data.description ? { description: data.description } : {}),
|
|
326
334
|
runtime: (0, work_task_1.isAggregateTask)(data) ? 'none' : data.type === 'local-task' ? 'local' : 'docker',
|
|
327
335
|
...((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data) ? { image: data.image } : {}),
|
|
@@ -330,13 +338,18 @@ function toPlan(root, item) {
|
|
|
330
338
|
network: (0, work_network_1.toNetworkSchema)('network' in data ? data.network : { type: 'open' }),
|
|
331
339
|
...('args' in data && data.args !== null ? { args: data.args } : {}),
|
|
332
340
|
deps: item.deps.map((dep) => dep.name).sort(),
|
|
333
|
-
|
|
341
|
+
// by their names in the plan, not the names the task reaches them at (`postgres` of an include)
|
|
342
|
+
needs: item.needs.map((need) => need.service.name).sort(),
|
|
334
343
|
accounts,
|
|
335
344
|
secrets,
|
|
336
345
|
serverEnvs: 'envs' in data ? data.envs.processNames ?? [] : [],
|
|
337
346
|
mounts,
|
|
338
347
|
ports,
|
|
339
|
-
|
|
348
|
+
// where in the bundle, as the build file names them: the directory the server read it in is
|
|
349
|
+
// its own
|
|
350
|
+
generates: 'generates' in data
|
|
351
|
+
? data.generates.map((generate) => insideBundle(root, data.cwd, generate.path)[0] ?? generate.path)
|
|
352
|
+
: [],
|
|
340
353
|
...(capAdd.length > 0 ? { capAdd: [...capAdd] } : {}),
|
|
341
354
|
flags,
|
|
342
355
|
};
|
|
@@ -400,7 +413,7 @@ async function planBundle(bundle, environment, options = {}) {
|
|
|
400
413
|
? item.data.mounts.flatMap((mount) => insideBundle(directory, item.data.cwd, mount.localPath))
|
|
401
414
|
: [],
|
|
402
415
|
deps: item.deps.map((dep) => dep.name).sort(),
|
|
403
|
-
needs: item.needs.map((need) => need.name).sort(),
|
|
416
|
+
needs: item.needs.map((need) => need.service.name).sort(),
|
|
404
417
|
};
|
|
405
418
|
}
|
|
406
419
|
const plan = {};
|
package/dist/server/policy.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.decideRun = exports.sandboxRepos = exports.decideSandbox = exports.sandboxPushBranches = exports.sandboxBranchType = exports.endpointLevel = exports.getGrant = exports.usedAccounts = exports.mayYank = exports.canUse = exports.maySeeDefinitions = exports.mayPublish = exports.mayPropose = exports.mayReadChecks = exports.mayRun = exports.matches = void 0;
|
|
3
|
+
exports.decideRun = exports.sandboxRepos = exports.decideSandbox = exports.sandboxPushBranches = exports.sandboxBranchType = exports.endpointLevel = exports.getGrant = exports.usedAccounts = exports.mayYank = exports.canUse = exports.maySeeDefinitions = exports.mayPublish = exports.mayPropose = exports.mayReadChecks = exports.mayRun = exports.matches = exports.subjectName = void 0;
|
|
4
4
|
const repo_1 = require("./repo");
|
|
5
5
|
const BUILDS_ORDER = ['run', 'propose', 'publish'];
|
|
6
|
+
// The name an entitlement gives a subject, for the pages to name its runs by when its tokens
|
|
7
|
+
// name nobody.
|
|
8
|
+
function subjectName(entitlements, who) {
|
|
9
|
+
return entitlements.find((entitlement) => entitlement.subject?.name && entitlement.subject.iss === who.iss && entitlement.subject.sub === who.sub)?.subject?.name;
|
|
10
|
+
}
|
|
11
|
+
exports.subjectName = subjectName;
|
|
6
12
|
function matches(entitlement, principal) {
|
|
7
13
|
if (entitlement.subject) {
|
|
8
14
|
return entitlement.subject.iss === principal.iss && entitlement.subject.sub === principal.sub;
|
|
@@ -34,9 +40,9 @@ const mayPublish = (grant) => atLeast(grant, 'publish');
|
|
|
34
40
|
exports.mayPublish = mayPublish;
|
|
35
41
|
// A caller who can neither run nor publish anything sees nothing of the server's
|
|
36
42
|
// build files either. Someone who uses an account sees them: they have to know what
|
|
37
|
-
// they approve.
|
|
43
|
+
// they approve. Who observes sees them too, as they see what every run ran.
|
|
38
44
|
function maySeeDefinitions(grant) {
|
|
39
|
-
return grant.builds !== null || Object.keys(grant.accounts).length > 0;
|
|
45
|
+
return grant.builds !== null || Object.keys(grant.accounts).length > 0 || grant.observe;
|
|
40
46
|
}
|
|
41
47
|
exports.maySeeDefinitions = maySeeDefinitions;
|
|
42
48
|
// `use` includes deciding about the runs others request on the account.
|