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.
Files changed (38) hide show
  1. package/dist/cache/explain.js +7 -5
  2. package/dist/dashboard/assets/graph-canvas-DLioOiRN.css +1 -0
  3. package/dist/dashboard/assets/graph-canvas-HmkW_-XJ.js +8 -0
  4. package/dist/dashboard/assets/index-gIBDC_m8.css +2 -0
  5. package/dist/dashboard/assets/index-vE22S5E_.js +17 -0
  6. package/dist/dashboard/index.html +2 -2
  7. package/dist/executer/docker-task.js +3 -0
  8. package/dist/executer/execute-work-task.js +3 -1
  9. package/dist/executer/local-task.js +3 -0
  10. package/dist/executer/scheduler/enqueue-next.js +6 -1
  11. package/dist/executer/scheduler/state-key.js +71 -34
  12. package/dist/executer/watch-loop.js +18 -5
  13. package/dist/file/get-file-context.js +5 -1
  14. package/dist/kubernetes/claim-layout.js +10 -1
  15. package/dist/kubernetes/ensure-persistent-data.js +84 -10
  16. package/dist/kubernetes/follow-pod-logs.js +124 -27
  17. package/dist/kubernetes/network-policy.js +4 -4
  18. package/dist/kubernetes/restore-kubernetes-data.js +21 -3
  19. package/dist/kubernetes/store-kubernetes-data.js +13 -2
  20. package/dist/kubernetes/volumes.js +34 -7
  21. package/dist/optimizer/calculate-checksum.js +46 -3
  22. package/dist/optimizer/get-work-cache-stats.js +36 -24
  23. package/dist/planner/work-item-executions.js +27 -0
  24. package/dist/planner/work-runtime-kubernetes.js +33 -38
  25. package/dist/server/capacity.js +63 -0
  26. package/dist/server/dashboard.js +2 -2
  27. package/dist/server/executor.js +10 -0
  28. package/dist/server/overview.js +42 -9
  29. package/dist/server/plan.js +16 -3
  30. package/dist/server/policy.js +9 -3
  31. package/dist/server/run-worker.js +37 -4
  32. package/dist/server/runs.js +49 -4
  33. package/dist/server/server-config.js +4 -1
  34. package/dist/server/server.js +10 -7
  35. package/dist/utils/limit.js +30 -0
  36. package/package.json +1 -1
  37. package/dist/dashboard/assets/index-D51NA6-h.js +0 -14
  38. package/dist/dashboard/assets/index-mmvC6u9O.css +0 -2
@@ -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
- void entry.saving.then(() => this.options.store?.remove(entry.run.id)).catch(() => undefined);
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
- const subjectSchema = zod_1.z.object({ iss: zod_1.z.string().min(1), sub: zod_1.z.string().min(1) }).strict();
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
@@ -129,7 +129,7 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
129
129
  }
130
130
  const runs = filter.scope === 'mine' ? registry.list(principal) : registry.all();
131
131
  (0, http_2.send)(res, 200, {
132
- runs: (0, overview_1.filterRuns)(runs, viewer, filter).map((run) => listRun(registry, viewer, run)),
132
+ runs: (0, overview_1.filterRuns)(runs, viewer, filter).map((run) => listRun(registry, definitions, viewer, run)),
133
133
  });
134
134
  }
135
135
  else if (method === 'POST') {
@@ -228,7 +228,7 @@ async function handleRuns(config, registry, definitions, sources, sandboxes, pla
228
228
  const may = run && (0, overview_1.runActions)(viewer, run);
229
229
  run
230
230
  ? (0, http_2.send)(res, 200, {
231
- ...toRunResponse(run, (0, overview_1.queuePlace)(registry, viewer, run), !(0, overview_1.owns)(viewer, run)),
231
+ ...toRunResponse(run, (0, overview_1.queuePlace)(registry, viewer, run), !(0, overview_1.owns)(viewer, run), viewer.nameOf),
232
232
  ...(may ? { may } : {}),
233
233
  })
234
234
  : (0, http_2.send)(res, 404, { error: 'not found' });
@@ -322,18 +322,21 @@ function runsTheSame(run, next) {
322
322
  }
323
323
  exports.runsTheSame = runsTheSame;
324
324
  // A run whole, for its owner and who may read it; who is not its owner is told whose it is.
325
- function toRunResponse(run, queue, named = false) {
325
+ function toRunResponse(run, queue, named = false, nameOf) {
326
326
  const { owner, ...visible } = run;
327
+ const name = owner.name ?? nameOf?.(owner);
327
328
  return {
328
329
  ...visible,
329
- ...(named ? { owner: { iss: owner.iss, sub: owner.sub, ...(owner.name ? { name: owner.name } : {}) } } : {}),
330
+ ...(named ? { owner: { iss: owner.iss, sub: owner.sub, ...(name ? { name } : {}) } } : {}),
330
331
  results: run.results ?? {},
331
332
  ...(queue ? { queue } : {}),
332
333
  };
333
334
  }
334
- function listRun(registry, viewer, run) {
335
- return (0, overview_1.toListedRun)(viewer, run, (0, overview_1.queuePlace)(registry, viewer, run));
335
+ function listRun(registry, definitions, viewer, run) {
336
+ return (0, overview_1.toListedRun)(viewer, run, (0, overview_1.queuePlace)(registry, viewer, run), entryOf(definitions));
336
337
  }
338
+ // where in its repository the build file of a version is
339
+ const entryOf = (definitions) => (definition) => definitions.find(definition)?.entry;
337
340
  async function submitRun(config, registry, definitions, sources, sandboxes, planning, repos, endpointUrl, log, principal, req, res) {
338
341
  const parsed = run_request_1.runRequestSchema.safeParse(await (0, http_2.readJson)(req));
339
342
  if (!parsed.success) {
@@ -697,7 +700,7 @@ async function handleApi(api, principal, req, res, url) {
697
700
  (0, http_2.send)(res, 403, { error: 'you are not entitled to anything on this server' });
698
701
  return;
699
702
  }
700
- (0, http_2.send)(res, 200, (0, overview_1.getOverview)(registry.snapshot(), registry.usage(principal), viewer));
703
+ (0, http_2.send)(res, 200, (0, overview_1.getOverview)(registry.snapshot(), registry.usage(principal), viewer, entryOf(definitions)));
701
704
  return;
702
705
  }
703
706
  if (path === '/v1/capacity' || path === '/v1/tasks') {
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLimit = void 0;
4
+ function createLimit(concurrency) {
5
+ let running = 0;
6
+ const waiting = [];
7
+ return async (fn) => {
8
+ if (running >= concurrency) {
9
+ await new Promise((resolve) => waiting.push(resolve));
10
+ }
11
+ else {
12
+ running++;
13
+ }
14
+ try {
15
+ return await fn();
16
+ }
17
+ finally {
18
+ const next = waiting.shift();
19
+ if (next) {
20
+ // the slot goes to the next one as it is
21
+ next();
22
+ }
23
+ else {
24
+ running--;
25
+ }
26
+ }
27
+ };
28
+ }
29
+ exports.createLimit = createLimit;
30
+ //# sourceMappingURL=limit.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "2.0.0-next.5",
4
+ "version": "2.0.0-next.7",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",