hammerkit 2.0.0-next.5 → 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 +2 -2
- 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/server/capacity.js +63 -0
- package/dist/server/dashboard.js +2 -2
- package/dist/server/executor.js +10 -0
- package/dist/server/overview.js +42 -9
- 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 +49 -4
- package/dist/server/server-config.js +4 -1
- package/dist/server/server.js +10 -7
- package/dist/utils/limit.js +30 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-D51NA6-h.js +0 -14
- package/dist/dashboard/assets/index-mmvC6u9O.css +0 -2
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
|
@@ -64,8 +64,8 @@ const TYPES = {
|
|
|
64
64
|
};
|
|
65
65
|
async function handleDashboard(context, principal, req, res, url) {
|
|
66
66
|
const now = context.now ?? Date.now;
|
|
67
|
-
const
|
|
68
|
-
const
|
|
67
|
+
const viewer = (0, overview_1.getViewer)(context.config, principal);
|
|
68
|
+
const { grant } = viewer;
|
|
69
69
|
const pathname = url.pathname;
|
|
70
70
|
for (const [name, value] of Object.entries(exports.HEADERS)) {
|
|
71
71
|
res.setHeader(name, value);
|
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/overview.js
CHANGED
|
@@ -13,6 +13,7 @@ function getViewer(config, principal) {
|
|
|
13
13
|
principal,
|
|
14
14
|
grant: (0, policy_1.getGrant)(config.entitlements, principal),
|
|
15
15
|
activity: config.runs.activity === 'everyone',
|
|
16
|
+
nameOf: (owner) => (0, policy_1.subjectName)(config.entitlements, owner),
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
exports.getViewer = getViewer;
|
|
@@ -91,7 +92,8 @@ function mayRead(viewer, run) {
|
|
|
91
92
|
return owns(viewer, run) || viewer.grant.observe || decides(viewer, run);
|
|
92
93
|
}
|
|
93
94
|
exports.mayRead = mayRead;
|
|
94
|
-
|
|
95
|
+
// `entryOf` names where in its repository the build file of a version is.
|
|
96
|
+
function toListedRun(viewer, run, queue, entryOf) {
|
|
95
97
|
const { owner, results, plan, options, up, error, ...rest } = run;
|
|
96
98
|
const mine = owns(viewer, run);
|
|
97
99
|
const may = runActions(viewer, run);
|
|
@@ -106,11 +108,11 @@ function toListedRun(viewer, run, queue) {
|
|
|
106
108
|
...(result.cache ? { cached: result.cache.hit } : {}),
|
|
107
109
|
},
|
|
108
110
|
]));
|
|
111
|
+
const entry = entryOf?.(run.definition);
|
|
112
|
+
const name = owner.name ?? viewer.nameOf?.(owner);
|
|
109
113
|
return {
|
|
110
114
|
...rest,
|
|
111
|
-
...(mine
|
|
112
|
-
? { plan, options }
|
|
113
|
-
: { owner: { iss: owner.iss, sub: owner.sub, ...(owner.name ? { name: owner.name } : {}) } }),
|
|
115
|
+
...(mine ? { plan, options } : { owner: { iss: owner.iss, sub: owner.sub, ...(name ? { name } : {}) } }),
|
|
114
116
|
...(up
|
|
115
117
|
? {
|
|
116
118
|
up: {
|
|
@@ -124,6 +126,7 @@ function toListedRun(viewer, run, queue) {
|
|
|
124
126
|
...(queue ? { queue } : {}),
|
|
125
127
|
...(may ? { may } : {}),
|
|
126
128
|
...(!mine && mayRead(viewer, run) ? { readable: true } : {}),
|
|
129
|
+
...(entry ? { entry } : {}),
|
|
127
130
|
};
|
|
128
131
|
}
|
|
129
132
|
exports.toListedRun = toListedRun;
|
|
@@ -149,7 +152,8 @@ function queuePlace(registry, viewer, run) {
|
|
|
149
152
|
return { position: queue.position, waitsFor: visibleReason(queue.waitsFor, visible) };
|
|
150
153
|
}
|
|
151
154
|
exports.queuePlace = queuePlace;
|
|
152
|
-
function getOverview(snapshot, usage, viewer) {
|
|
155
|
+
function getOverview(snapshot, usage, viewer, entryOf) {
|
|
156
|
+
const list = (run, place) => toListedRun(viewer, run, place, entryOf);
|
|
153
157
|
const runs = new Map();
|
|
154
158
|
for (const run of [
|
|
155
159
|
...snapshot.running,
|
|
@@ -168,7 +172,7 @@ function getOverview(snapshot, usage, viewer) {
|
|
|
168
172
|
return {
|
|
169
173
|
...place,
|
|
170
174
|
mine: owns(viewer, entry.run),
|
|
171
|
-
...(maySee(viewer, entry.run) ? { run:
|
|
175
|
+
...(maySee(viewer, entry.run) ? { run: list(entry.run, place) } : {}),
|
|
172
176
|
};
|
|
173
177
|
});
|
|
174
178
|
const running = snapshot.running.filter((run) => maySee(viewer, run));
|
|
@@ -178,19 +182,19 @@ function getOverview(snapshot, usage, viewer) {
|
|
|
178
182
|
places: {
|
|
179
183
|
max: snapshot.places.max,
|
|
180
184
|
used: snapshot.places.used,
|
|
181
|
-
runs: running.map((run) =>
|
|
185
|
+
runs: running.map((run) => list(run)),
|
|
182
186
|
others: snapshot.running.length - running.length,
|
|
183
187
|
},
|
|
184
188
|
queue,
|
|
185
189
|
approvals: {
|
|
186
190
|
waiting: snapshot.awaiting.runs.length,
|
|
187
191
|
max: snapshot.awaiting.max,
|
|
188
|
-
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)),
|
|
189
193
|
},
|
|
190
194
|
services: {
|
|
191
195
|
open: snapshot.services.taken,
|
|
192
196
|
...(snapshot.services.max !== undefined ? { max: snapshot.services.max } : {}),
|
|
193
|
-
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)),
|
|
194
198
|
},
|
|
195
199
|
limits: {
|
|
196
200
|
waiting: meter(usage.waiting, limits?.waiting),
|
|
@@ -291,6 +295,35 @@ function visibleCapacity(capacity, viewer) {
|
|
|
291
295
|
...capacity,
|
|
292
296
|
// what the cluster answered is for operators; others learn that something is missing
|
|
293
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
|
+
: {}),
|
|
294
327
|
groups: capacity.groups.map((group) => {
|
|
295
328
|
const mine = group.owner?.iss === viewer.principal.iss && group.owner?.sub === viewer.principal.sub;
|
|
296
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.workerMain = exports.readInput = exports.taskCauses = exports.describeFailures = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
|
|
3
|
+
exports.workerMain = exports.readInput = exports.taskCommands = exports.taskCauses = exports.describeFailures = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
|
|
4
4
|
const readline_1 = require("readline");
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const cli_1 = require("../cli");
|
|
@@ -10,6 +10,7 @@ const schema_parser_1 = require("../schema/schema-parser");
|
|
|
10
10
|
const reference_parser_1 = require("../schema/reference-parser");
|
|
11
11
|
const work_scope_parser_1 = require("../schema/work-scope-parser");
|
|
12
12
|
const work_item_status_1 = require("../planner/work-item-status");
|
|
13
|
+
const work_item_executions_1 = require("../planner/work-item-executions");
|
|
13
14
|
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
14
15
|
const assert_item_paths_1 = require("../planner/utils/assert-item-paths");
|
|
15
16
|
const hosted_volumes_1 = require("../planner/utils/hosted-volumes");
|
|
@@ -218,6 +219,7 @@ up) {
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
}
|
|
222
|
+
let planned = false;
|
|
221
223
|
for (const { name, scope } of scopes) {
|
|
222
224
|
const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(input.entry, environment, {
|
|
223
225
|
directory: input.directory,
|
|
@@ -234,6 +236,16 @@ up) {
|
|
|
234
236
|
if (cli.tasks().length === 0 && (!input.up || cli.services().length === 0)) {
|
|
235
237
|
throw new Error(`no tasks found for ${name}`);
|
|
236
238
|
}
|
|
239
|
+
if (!planned) {
|
|
240
|
+
// from the start of this process, which the executor started for the run
|
|
241
|
+
planned = true;
|
|
242
|
+
emit({
|
|
243
|
+
type: 'phase',
|
|
244
|
+
phase: 'plan',
|
|
245
|
+
startedAt: new Date(performance.timeOrigin).toISOString(),
|
|
246
|
+
finishedAt: new Date().toISOString(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
237
249
|
const execution = input.up
|
|
238
250
|
? cli.up({ ...execOptions, workers: cli.defaultWorkers(), daemon: false, wait: 'ready' })
|
|
239
251
|
: cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
|
|
@@ -392,6 +404,19 @@ function taskCauses(causes, redact) {
|
|
|
392
404
|
: {};
|
|
393
405
|
}
|
|
394
406
|
exports.taskCauses = taskCauses;
|
|
407
|
+
// When the commands of a task that is over ran, as its runtime saw them.
|
|
408
|
+
function taskCommands(task) {
|
|
409
|
+
const executions = (0, work_item_executions_1.readExecutions)(task);
|
|
410
|
+
return executions.length
|
|
411
|
+
? {
|
|
412
|
+
commands: executions.slice(0, runs_1.MAX_COMMANDS).map((e) => ({
|
|
413
|
+
startedAt: e.startedAt.toISOString(),
|
|
414
|
+
finishedAt: e.finishedAt.toISOString(),
|
|
415
|
+
})),
|
|
416
|
+
}
|
|
417
|
+
: {};
|
|
418
|
+
}
|
|
419
|
+
exports.taskCommands = taskCommands;
|
|
395
420
|
// A task event when a task has moved on since the last look, with what its result needs
|
|
396
421
|
// once it is over.
|
|
397
422
|
function reportTasks(emit, workTree, seen,
|
|
@@ -414,17 +439,25 @@ redact) {
|
|
|
414
439
|
state: status,
|
|
415
440
|
...(status === 'skipped' ? {} : { durationMs: state.duration }),
|
|
416
441
|
...taskCauses(state.missCauses, redact),
|
|
442
|
+
...(status === 'succeeded' ? taskCommands(task.data) : {}),
|
|
417
443
|
};
|
|
418
444
|
}
|
|
419
445
|
else if (state.type === 'crash') {
|
|
420
|
-
event = {
|
|
446
|
+
event = {
|
|
447
|
+
type: 'task',
|
|
448
|
+
task: task.name,
|
|
449
|
+
state: 'failed',
|
|
450
|
+
exitCode: state.exitCode,
|
|
451
|
+
...since(),
|
|
452
|
+
...taskCommands(task.data),
|
|
453
|
+
};
|
|
421
454
|
}
|
|
422
455
|
else if (state.type === 'error') {
|
|
423
456
|
const error = redact(state.errorMessage).slice(0, runs_1.MAX_TASK_ERROR);
|
|
424
|
-
event = { type: 'task', task: task.name, state: 'failed', error, ...since() };
|
|
457
|
+
event = { type: 'task', task: task.name, state: 'failed', error, ...since(), ...taskCommands(task.data) };
|
|
425
458
|
}
|
|
426
459
|
else if (state.type === 'canceled') {
|
|
427
|
-
event = { type: 'task', task: task.name, state: 'cancelled' };
|
|
460
|
+
event = { type: 'task', task: task.name, state: 'cancelled', ...taskCommands(task.data) };
|
|
428
461
|
}
|
|
429
462
|
if (event && before?.status !== event.state) {
|
|
430
463
|
seen.set(task.name, { status: event.state, started });
|
package/dist/server/runs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.unavailableExecutor = exports.RunRegistry = exports.MAX_TASK_ERROR = exports.MAX_CAUSES = exports.SANDBOX_ID_PATTERN = exports.RUN_ID_PATTERN = exports.QuotaError = exports.RunError = exports.isFinal = void 0;
|
|
3
|
+
exports.unavailableExecutor = exports.RunRegistry = exports.MAX_COMMANDS = exports.MAX_TASK_ERROR = exports.MAX_CAUSES = exports.SANDBOX_ID_PATTERN = exports.RUN_ID_PATTERN = exports.QuotaError = exports.RunError = exports.RUN_PHASES = exports.isFinal = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const oauth_1 = require("./oauth");
|
|
6
6
|
const run_logs_1 = require("./run-logs");
|
|
@@ -10,6 +10,11 @@ function isFinal(state) {
|
|
|
10
10
|
return FINAL_STATES.includes(state);
|
|
11
11
|
}
|
|
12
12
|
exports.isFinal = isFinal;
|
|
13
|
+
// What an executor reports while it works. `seq` and `time` are added by the registry.
|
|
14
|
+
// The steps of a run before its tasks: `source` gets its files (a checkout of the commit, or
|
|
15
|
+
// those of the sandbox), `namespace` prepares its place on the cluster, `plan` starts the
|
|
16
|
+
// process of the run and reads its build files.
|
|
17
|
+
exports.RUN_PHASES = ['source', 'namespace', 'plan'];
|
|
13
18
|
// The server can not take the run now; `status` is what the caller is told
|
|
14
19
|
class RunError extends Error {
|
|
15
20
|
constructor() {
|
|
@@ -61,6 +66,11 @@ const TAIL_LINE_LENGTH = 1000;
|
|
|
61
66
|
// does not can still not make a result of any size.
|
|
62
67
|
exports.MAX_CAUSES = 20;
|
|
63
68
|
exports.MAX_TASK_ERROR = 2 * 1024;
|
|
69
|
+
// the command times a task result keeps
|
|
70
|
+
exports.MAX_COMMANDS = 50;
|
|
71
|
+
function isTimestamp(value) {
|
|
72
|
+
return typeof value === 'string' && value.length <= 40 && !Number.isNaN(Date.parse(value));
|
|
73
|
+
}
|
|
64
74
|
function bounded(event) {
|
|
65
75
|
const causes = event.causes;
|
|
66
76
|
return {
|
|
@@ -74,12 +84,20 @@ function bounded(event) {
|
|
|
74
84
|
}
|
|
75
85
|
: {}),
|
|
76
86
|
...(event.error !== undefined ? { error: event.error.slice(0, exports.MAX_TASK_ERROR) } : {}),
|
|
87
|
+
...(event.commands
|
|
88
|
+
? {
|
|
89
|
+
commands: event.commands
|
|
90
|
+
.filter((c) => isTimestamp(c?.startedAt) && isTimestamp(c?.finishedAt))
|
|
91
|
+
.slice(0, exports.MAX_COMMANDS)
|
|
92
|
+
.map(({ startedAt, finishedAt }) => ({ startedAt, finishedAt })),
|
|
93
|
+
}
|
|
94
|
+
: {}),
|
|
77
95
|
};
|
|
78
96
|
}
|
|
79
97
|
// what a task event weighs against the log limits of a run
|
|
80
98
|
function taskEventSize(event) {
|
|
81
99
|
const causes = (event.causes ?? []).reduce((sum, cause) => sum + Buffer.byteLength(cause) + 4, 0);
|
|
82
|
-
return 64 + causes + Buffer.byteLength(event.error ?? '');
|
|
100
|
+
return 64 + causes + Buffer.byteLength(event.error ?? '') + (event.commands?.length ?? 0) * 64;
|
|
83
101
|
}
|
|
84
102
|
// Where runs are kept while the server is up. Everything is scoped to the
|
|
85
103
|
// caller that submitted the run: the same id asked for by someone else is
|
|
@@ -95,6 +113,8 @@ class RunRegistry {
|
|
|
95
113
|
this.waiters = new Set();
|
|
96
114
|
// the moves of service runs that execute beside their services
|
|
97
115
|
this.moving = new Set();
|
|
116
|
+
// the records of evicted runs still being removed from the store, waited for by close
|
|
117
|
+
this.removing = new Set();
|
|
98
118
|
this.maxConcurrent = options.maxConcurrent ?? 4;
|
|
99
119
|
this.keep = options.keep ?? 200;
|
|
100
120
|
this.maxEvents = options.maxEvents ?? 5000;
|
|
@@ -598,8 +618,10 @@ class RunRegistry {
|
|
|
598
618
|
[...this.entries.values()].some((entry) => entry.run.state === 'running' || entry.run.state === 'up')) {
|
|
599
619
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
600
620
|
}
|
|
601
|
-
// the runs that were stopped are written down before the server is gone
|
|
621
|
+
// the runs that were stopped are written down before the server is gone, and the records
|
|
622
|
+
// of evicted runs are gone from the store
|
|
602
623
|
await Promise.all([...this.entries.values()].map((entry) => entry.saving));
|
|
624
|
+
await Promise.all([...this.removing]);
|
|
603
625
|
}
|
|
604
626
|
find(owner, id, canUse) {
|
|
605
627
|
const entry = this.entries.get(id);
|
|
@@ -635,6 +657,7 @@ class RunRegistry {
|
|
|
635
657
|
accounts: [...run.accounts],
|
|
636
658
|
...(run.approvals ? { approvals: JSON.parse(JSON.stringify(run.approvals)) } : {}),
|
|
637
659
|
...(run.results ? { results: JSON.parse(JSON.stringify(run.results)) } : {}),
|
|
660
|
+
...(run.phases ? { phases: run.phases.map((phase) => ({ ...phase })) } : {}),
|
|
638
661
|
...(run.moves ? { moves: run.moves.map((move) => ({ ...move })) } : {}),
|
|
639
662
|
...(run.moving ? { moving: { ...run.moving } } : {}),
|
|
640
663
|
};
|
|
@@ -1447,6 +1470,10 @@ class RunRegistry {
|
|
|
1447
1470
|
}
|
|
1448
1471
|
return;
|
|
1449
1472
|
}
|
|
1473
|
+
if (recorded.type === 'phase') {
|
|
1474
|
+
this.keepPhase(entry, recorded);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1450
1477
|
const event = recorded.type === 'task' ? bounded(recorded) : recorded;
|
|
1451
1478
|
if (event.type === 'log') {
|
|
1452
1479
|
this.keepTail(entry, event.task, event.text);
|
|
@@ -1469,6 +1496,19 @@ class RunRegistry {
|
|
|
1469
1496
|
}
|
|
1470
1497
|
this.push(entry, event);
|
|
1471
1498
|
}
|
|
1499
|
+
// once each, from the executor: a phase it reports again (a service run that wakes) is the
|
|
1500
|
+
// newest
|
|
1501
|
+
keepPhase(entry, event) {
|
|
1502
|
+
if (!exports.RUN_PHASES.includes(event.phase) ||
|
|
1503
|
+
!isTimestamp(event.startedAt) ||
|
|
1504
|
+
!isTimestamp(event.finishedAt) ||
|
|
1505
|
+
Date.parse(event.finishedAt) < Date.parse(event.startedAt)) {
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const phases = (entry.run.phases ?? []).filter((phase) => phase.phase !== event.phase);
|
|
1509
|
+
phases.push({ phase: event.phase, startedAt: event.startedAt, finishedAt: event.finishedAt });
|
|
1510
|
+
entry.run.phases = phases.sort((a, b) => exports.RUN_PHASES.indexOf(a.phase) - exports.RUN_PHASES.indexOf(b.phase));
|
|
1511
|
+
}
|
|
1472
1512
|
keepTail(entry, task, text) {
|
|
1473
1513
|
let tail = entry.tails.get(task);
|
|
1474
1514
|
if (!tail) {
|
|
@@ -1510,6 +1550,7 @@ class RunRegistry {
|
|
|
1510
1550
|
...(event.durationMs !== undefined ? { durationMs: event.durationMs } : {}),
|
|
1511
1551
|
...(event.exitCode !== undefined ? { exitCode: event.exitCode } : {}),
|
|
1512
1552
|
...(event.error ? { error: event.error } : {}),
|
|
1553
|
+
...(event.commands?.length ? { commands: event.commands } : {}),
|
|
1513
1554
|
...(event.state === 'cached'
|
|
1514
1555
|
? { cache: { hit: true } }
|
|
1515
1556
|
: event.state === 'succeeded' || event.state === 'failed'
|
|
@@ -1547,7 +1588,11 @@ class RunRegistry {
|
|
|
1547
1588
|
const finished = [...this.entries.values()].filter((entry) => FINAL_STATES.includes(entry.run.state));
|
|
1548
1589
|
for (const entry of finished.slice(0, Math.max(0, finished.length - this.keep))) {
|
|
1549
1590
|
this.entries.delete(entry.run.id);
|
|
1550
|
-
|
|
1591
|
+
const removal = entry.saving
|
|
1592
|
+
.then(() => this.options.store?.remove(entry.run.id))
|
|
1593
|
+
.catch(() => undefined)
|
|
1594
|
+
.finally(() => this.removing.delete(removal));
|
|
1595
|
+
this.removing.add(removal);
|
|
1551
1596
|
}
|
|
1552
1597
|
}
|
|
1553
1598
|
}
|
|
@@ -45,7 +45,10 @@ exports.issuerSchema = zod_1.z
|
|
|
45
45
|
scope: zod_1.z.string().min(1).default('openid'),
|
|
46
46
|
})
|
|
47
47
|
.strict();
|
|
48
|
-
|
|
48
|
+
// `name`: how pages name a subject whose tokens name nobody, such as a service account's
|
|
49
|
+
const subjectSchema = zod_1.z
|
|
50
|
+
.object({ iss: zod_1.z.string().min(1), sub: zod_1.z.string().min(1), name: zod_1.z.string().trim().min(1).max(100).optional() })
|
|
51
|
+
.strict();
|
|
49
52
|
const groupSchema = zod_1.z.object({ iss: zod_1.z.string().min(1), name: zod_1.z.string().min(1) }).strict();
|
|
50
53
|
const claimsSchema = zod_1.z.object({ iss: zod_1.z.string().min(1), match: zod_1.z.record(zod_1.z.string()) }).strict();
|
|
51
54
|
// Who: the person or client (`subject`), everyone in a group of an issuer
|