hammerkit 2.0.0-next.5 → 2.0.0-next.7
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 +84 -10
- package/dist/kubernetes/follow-pod-logs.js +124 -27
- package/dist/kubernetes/network-policy.js +4 -4
- package/dist/kubernetes/restore-kubernetes-data.js +21 -3
- package/dist/kubernetes/store-kubernetes-data.js +13 -2
- package/dist/kubernetes/volumes.js +34 -7
- 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
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.kubernetesForwardRuntime = exports.isRolledOut = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = void 0;
|
|
6
|
+
exports.kubernetesForwardRuntime = exports.isRolledOut = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = exports.recordJobExecution = void 0;
|
|
7
7
|
const abort_1 = require("../executer/abort");
|
|
8
8
|
const kubernetes_service_1 = require("../executer/kubernetes-service");
|
|
9
9
|
const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
|
|
@@ -12,6 +12,7 @@ const apply_1 = require("../kubernetes/apply");
|
|
|
12
12
|
const ensure_kubernetes_service_exists_1 = require("../kubernetes/ensure-kubernetes-service-exists");
|
|
13
13
|
const ensure_kubernetes_deployment_exists_1 = require("../kubernetes/ensure-kubernetes-deployment-exists");
|
|
14
14
|
const follow_pod_logs_1 = require("../kubernetes/follow-pod-logs");
|
|
15
|
+
const work_item_executions_1 = require("./work-item-executions");
|
|
15
16
|
const service_definition_1 = require("./service-definition");
|
|
16
17
|
const ensure_namespace_1 = require("../kubernetes/ensure-namespace");
|
|
17
18
|
const ensure_persistent_data_1 = require("../kubernetes/ensure-persistent-data");
|
|
@@ -62,32 +63,17 @@ async function getJobPod(instance, kubernetes, job) {
|
|
|
62
63
|
.sort((a, b) => (a.metadata?.creationTimestamp?.toISOString() ?? '').localeCompare(b.metadata?.creationTimestamp?.toISOString() ?? ''))
|
|
63
64
|
.pop();
|
|
64
65
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const log = await instance.coreApi.readNamespacedPodLog({
|
|
76
|
-
name: pod.metadata.name,
|
|
77
|
-
namespace: kubernetes.namespace,
|
|
78
|
-
container,
|
|
79
|
-
limitBytes: MAX_JOB_LOG_BYTES,
|
|
80
|
-
});
|
|
81
|
-
for (const line of String(log ?? '').split(/\r?\n/)) {
|
|
82
|
-
if (line.length > 0) {
|
|
83
|
-
task.status.console('stdout', line);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch (e) {
|
|
88
|
-
task.status.write('debug', `the log of ${job} could not be read: ${(0, log_1.getErrorMessage)(e)}`);
|
|
66
|
+
// When the command of a Job ran, as its container says: what the task spent before it (the
|
|
67
|
+
// pod scheduled, the image pulled, the init containers) and after it is not the command's.
|
|
68
|
+
async function recordJobExecution(instance, kubernetes, task, job, container) {
|
|
69
|
+
const pod = await getJobPod(instance, kubernetes, job).catch(() => undefined);
|
|
70
|
+
const status = pod?.status?.containerStatuses?.find((s) => s.name === container);
|
|
71
|
+
const terminated = status?.state?.terminated ?? status?.lastState?.terminated;
|
|
72
|
+
if (terminated?.startedAt && terminated.finishedAt) {
|
|
73
|
+
(0, work_item_executions_1.recordExecution)(task, new Date(terminated.startedAt), new Date(terminated.finishedAt));
|
|
89
74
|
}
|
|
90
75
|
}
|
|
76
|
+
exports.recordJobExecution = recordJobExecution;
|
|
91
77
|
// the command of a Job exited with this code, as a crash on docker or a host
|
|
92
78
|
class JobCrash extends Error {
|
|
93
79
|
constructor(exitCode) {
|
|
@@ -394,24 +380,33 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
394
380
|
};
|
|
395
381
|
try {
|
|
396
382
|
const pod = await (0, apply_1.apply)(instance, spec);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
|
|
383
|
+
// what the command prints, while it prints it; the last lines once the pod ended
|
|
384
|
+
const logs = (0, follow_pod_logs_1.followPodLogs)(instance, kubernetes.namespace, `job-name=${podName}`, container, (line) => task.status.console('stdout', line), { onError: (text) => task.status.write('debug', text) });
|
|
385
|
+
try {
|
|
386
|
+
if (!pod.status?.succeeded) {
|
|
387
|
+
try {
|
|
388
|
+
await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort);
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
if (!(e instanceof abort_1.AbortError)) {
|
|
392
|
+
await logs.settle();
|
|
393
|
+
await recordJobExecution(instance, kubernetes, task.data, podName, container);
|
|
394
|
+
const failure = await describeFailedJob(instance, kubernetes, task.name, podName, container, inJob);
|
|
395
|
+
if (failure?.type === 'crash') {
|
|
396
|
+
throw new JobCrash(failure.exitCode);
|
|
397
|
+
}
|
|
398
|
+
throw new Error(failure?.message ?? (0, log_1.getErrorMessage)(e));
|
|
407
399
|
}
|
|
408
|
-
throw
|
|
400
|
+
throw e;
|
|
409
401
|
}
|
|
410
|
-
throw e;
|
|
411
402
|
}
|
|
403
|
+
await logs.settle();
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
logs.stop();
|
|
412
407
|
}
|
|
413
|
-
await writeJobLogs(instance, kubernetes, task, podName, container);
|
|
414
408
|
task.status.write('debug', 'pod completed');
|
|
409
|
+
await recordJobExecution(instance, kubernetes, task.data, podName, container);
|
|
415
410
|
// its init containers are done with the credentials
|
|
416
411
|
await (0, job_secrets_1.removeJobCredentials)(instance, kubernetes, task);
|
|
417
412
|
if (i < task.data.cmds.length) {
|
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 });
|