hammerkit 1.27.0-next.8 → 2.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/build.schema.json +176 -6
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry/registry-client.js +1 -0
  4. package/dist/cache/cache-sync.js +1 -1
  5. package/dist/cache/explain.js +17 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
  8. package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
  9. package/dist/dashboard/favicon.svg +1 -0
  10. package/dist/dashboard/index.html +15 -0
  11. package/dist/docker/item-network.js +13 -5
  12. package/dist/executer/archive-task-entry.js +4 -2
  13. package/dist/executer/deadline.js +17 -3
  14. package/dist/executer/deferred-restore.js +36 -0
  15. package/dist/executer/docker-service.js +30 -8
  16. package/dist/executer/docker-task.js +21 -1
  17. package/dist/executer/execute-work-service.js +2 -0
  18. package/dist/executer/execute-work-task.js +11 -4
  19. package/dist/executer/execution-steps.js +25 -4
  20. package/dist/executer/reset-work-tree.js +4 -0
  21. package/dist/executer/scheduler/enqueue-next.js +57 -6
  22. package/dist/executer/scheduler/state-key.js +42 -5
  23. package/dist/executer/watch-loop.js +6 -3
  24. package/dist/harness/connect.js +20 -6
  25. package/dist/harness/mcp-command.js +22 -6
  26. package/dist/harness/mcp.js +63 -114
  27. package/dist/harness/opencode.js +158 -50
  28. package/dist/harness/pi.js +29 -0
  29. package/dist/harness/sandbox-client.js +194 -23
  30. package/dist/harness/service-tools.js +141 -0
  31. package/dist/harness/task-tools.js +221 -0
  32. package/dist/kubernetes/apply.js +76 -17
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-http-route.js +1 -1
  36. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +138 -18
  37. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  38. package/dist/kubernetes/ensure-persistent-data.js +212 -10
  39. package/dist/kubernetes/follow-pod-logs.js +182 -0
  40. package/dist/kubernetes/get-service-ip.js +2 -2
  41. package/dist/kubernetes/job-placement.js +36 -12
  42. package/dist/kubernetes/job-secrets.js +9 -6
  43. package/dist/kubernetes/kept-claims.js +22 -10
  44. package/dist/kubernetes/kubernetes-instance.js +1 -0
  45. package/dist/kubernetes/network-policy.js +16 -8
  46. package/dist/kubernetes/remove-persistent-data.js +3 -1
  47. package/dist/kubernetes/remove-stale-items.js +46 -0
  48. package/dist/kubernetes/resources.js +25 -3
  49. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  50. package/dist/kubernetes/sandbox-runner.js +628 -0
  51. package/dist/kubernetes/secrets.js +7 -11
  52. package/dist/kubernetes/service-rollouts.js +74 -0
  53. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  54. package/dist/kubernetes/volumes.js +96 -11
  55. package/dist/main.js +2 -0
  56. package/dist/optimizer/calculate-checksum.js +3 -2
  57. package/dist/optimizer/check-key.js +94 -0
  58. package/dist/optimizer/check-keys.js +64 -0
  59. package/dist/optimizer/get-work-cache-stats.js +17 -3
  60. package/dist/optimizer/work-task-cache-description.js +49 -1
  61. package/dist/planner/service-definition.js +9 -4
  62. package/dist/planner/utils/append-work-dependencies.js +5 -3
  63. package/dist/planner/utils/append-work-service.js +18 -1
  64. package/dist/planner/utils/append-work-task.js +13 -3
  65. package/dist/planner/utils/get-work-runtime.js +6 -1
  66. package/dist/planner/utils/normalize-path.js +19 -1
  67. package/dist/planner/utils/parse-work-mount.js +7 -1
  68. package/dist/planner/utils/parse-work-port.js +4 -1
  69. package/dist/planner/validate.js +22 -0
  70. package/dist/planner/work-network.js +3 -1
  71. package/dist/planner/work-runtime-aggregate.js +32 -0
  72. package/dist/planner/work-runtime-kubernetes.js +153 -26
  73. package/dist/planner/work-task.js +6 -1
  74. package/dist/program.js +27 -9
  75. package/dist/remote/device-login.js +7 -2
  76. package/dist/remote/host-config.js +69 -3
  77. package/dist/remote/remote-commands.js +33 -7
  78. package/dist/remote/remote-status.js +112 -0
  79. package/dist/remote/remote-work-commands.js +562 -45
  80. package/dist/remote/server-client.js +15 -2
  81. package/dist/remote/token-source.js +64 -26
  82. package/dist/run-program.js +9 -0
  83. package/dist/schema/build-file-base-task-schema.js +3 -3
  84. package/dist/schema/build-file-container-service-schema.js +10 -0
  85. package/dist/schema/build-file-container-task-schema.js +16 -1
  86. package/dist/schema/build-file-include-schema.js +18 -4
  87. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  88. package/dist/schema/build-file-task-schema.js +5 -1
  89. package/dist/schema/capabilities-schema.js +56 -0
  90. package/dist/schema/reference-parser.js +119 -11
  91. package/dist/schema/removed-properties.js +20 -0
  92. package/dist/schema/schema-parser.js +94 -41
  93. package/dist/schema/work-scope-parser.js +33 -10
  94. package/dist/secrets/command-secret-provider.js +3 -1
  95. package/dist/secrets/github-app-token-command.js +3 -1
  96. package/dist/secrets/secret-catalog.js +43 -19
  97. package/dist/server/accounts-api.js +4 -1
  98. package/dist/server/approvals-api.js +1 -1
  99. package/dist/server/capacity-format.js +36 -0
  100. package/dist/server/capacity.js +300 -0
  101. package/dist/server/checks-api.js +83 -0
  102. package/dist/server/checks.js +116 -0
  103. package/dist/server/dashboard.js +196 -0
  104. package/dist/server/definitions-api.js +66 -3
  105. package/dist/server/endpoints.js +794 -0
  106. package/dist/server/executor.js +164 -9
  107. package/dist/server/kubernetes-backend.js +478 -70
  108. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  109. package/dist/server/overview.js +271 -0
  110. package/dist/server/plan.js +170 -121
  111. package/dist/server/policy.js +34 -2
  112. package/dist/server/proposals-api.js +17 -8
  113. package/dist/server/repo-access.js +5 -0
  114. package/dist/server/run-logs.js +4 -0
  115. package/dist/server/run-plan.js +38 -4
  116. package/dist/server/run-request.js +58 -3
  117. package/dist/server/run-worker.js +262 -13
  118. package/dist/server/runs.js +969 -36
  119. package/dist/server/sandbox-pod.js +28 -0
  120. package/dist/server/sandbox-storage.js +23 -0
  121. package/dist/server/sandboxes.js +8 -0
  122. package/dist/server/server-config.js +246 -10
  123. package/dist/server/server.js +429 -32
  124. package/dist/server/service-runs.js +82 -0
  125. package/dist/server/timings.js +132 -0
  126. package/dist/server/wait-reason.js +21 -0
  127. package/dist/utils/tar.js +28 -1
  128. package/dist/utils/terminal-prompt.js +53 -0
  129. package/package.json +8 -2
@@ -118,6 +118,7 @@ function parseService(workTree, service, environment, context) {
118
118
  secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment, context.secrets, service.schema.account),
119
119
  resources: (0, work_resources_1.parseWorkResources)(service.schema.resources),
120
120
  volumeSize: service.schema.volumeSize ?? null,
121
+ restarts: service.schema.restarts ?? null,
121
122
  network: (0, work_network_1.parseWorkNetwork)(service.schema.network),
122
123
  networkCeiling: null,
123
124
  args: service.schema.args ?? null,
@@ -143,7 +144,11 @@ function appendServiceInit(workTree, service, item, environment, context) {
143
144
  task.needs[selfNeed] = { name: task.needs[selfNeed].name, service: view };
144
145
  }
145
146
  else {
146
- task.needs.push({ name: item.name, service: view });
147
+ const name = getInitNeedName(service);
148
+ if (task.needs.some((need) => need.name === name)) {
149
+ throw new Error(`${task.name} needs two services as ${name}, give one another name with name:`);
150
+ }
151
+ task.needs.push({ name, service: view });
147
152
  }
148
153
  removeRequirer(item, task.name);
149
154
  // explicit, so neither the default method nor `--cache` turns it back on
@@ -153,6 +158,18 @@ function appendServiceInit(workTree, service, item, environment, context) {
153
158
  }
154
159
  item.data.init = { task, state };
155
160
  }
161
+ // The host name the init reaches its service by: the name its task declares the
162
+ // need by, else the service's name in its own build file. Planning the task
163
+ // skips that need (it is the init's), and the full name of a service of an
164
+ // include, `services:db`, is no host name: Docker and DNS take no colon.
165
+ function getInitNeedName(service) {
166
+ const declared = service.init?.task.needs.find((need) => need.service.relativeName === service.relativeName);
167
+ if (declared) {
168
+ return declared.relativeName;
169
+ }
170
+ const prefix = service.scope.namePrefix;
171
+ return prefix.length > 0 ? service.relativeName.slice(prefix.length + 1) : service.relativeName;
172
+ }
156
173
  const initViews = new WeakMap();
157
174
  // The service itself for the view its init task needs, else the item.
158
175
  function getPlannedService(item) {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.appendWorkTask = void 0;
4
4
  const work_secret_1 = require("../work-secret");
5
+ const work_task_1 = require("../work-task");
5
6
  const parse_work_generate_1 = require("./parse-work-generate");
6
7
  const template_value_1 = require("./template-value");
7
8
  const parse_work_mounts_1 = require("./parse-work-mounts");
@@ -25,7 +26,8 @@ function appendWorkTask(workTree, cwd, referenceTask, environment, context) {
25
26
  const parsed = parseTask(cwd, referenceTask, environment, context);
26
27
  // on a server's cluster a local task runs in a tool image like a container task
27
28
  const server = workTree.environment.type === 'kubernetes' ? workTree.environment.server : undefined;
28
- const task = !server
29
+ // an aggregate runs nothing, so it needs no pod either
30
+ const task = !server || (0, work_task_1.isAggregateTask)(parsed)
29
31
  ? parsed
30
32
  : parsed.type === 'local-task'
31
33
  ? (0, job_placement_1.runInToolImage)(parsed, server, referenceTask.schema.platform)
@@ -77,7 +79,6 @@ function parseTask(cwd, task, environment, context) {
77
79
  labels: task.labels,
78
80
  caching: (0, resolve_cache_1.resolveCache)(task.schema.cache ?? null, context.caches, task.relativeName),
79
81
  shell: task.schema.shell ? (0, template_value_1.templateValue)(task.schema.shell, envs) : '/bin/sh',
80
- continuous: task.schema.continuous ?? false,
81
82
  timeout: task.schema.timeout ? (0, units_1.parseDuration)(task.schema.timeout) : null,
82
83
  secrets: (0, work_secret_1.parseWorkSecrets)(cwd, context.projectRoot, task.schema.secrets, envs, environment, context.secrets, task.schema.account),
83
84
  resources: (0, work_resources_1.parseWorkResources)(task.schema.resources),
@@ -90,10 +91,12 @@ function parseTask(cwd, task, environment, context) {
90
91
  return {
91
92
  ...baseWorkNode,
92
93
  type: 'container-task',
93
- user: (0, get_container_user_1.getContainerUser)(),
94
+ // a capability holds for root only: a task that adds some runs as its image's user
95
+ user: task.schema.capAdd?.length ? null : (0, get_container_user_1.getContainerUser)(),
94
96
  image: (0, template_value_1.templateValue)(task.schema.image, envs),
95
97
  mounts: (0, parse_work_mounts_1.parseWorkMounts)(cwd, task.schema, envs),
96
98
  volumeSize: task.schema.volumeSize ?? null,
99
+ capAdd: task.schema.capAdd ?? [],
97
100
  };
98
101
  }
99
102
  else {
@@ -107,9 +110,16 @@ function parseTask(cwd, task, environment, context) {
107
110
  throw new Error(`task ${task.relativeName}: network ${(0, work_network_1.formatNetwork)(baseWorkNode.network)} needs a container, ` +
108
111
  `hammerkit can't limit the network of a local task`);
109
112
  }
113
+ const aggregate = task.deps.length > 0 &&
114
+ task.needs.length === 0 &&
115
+ baseWorkNode.src.length === 0 &&
116
+ baseWorkNode.cmds.length === 0 &&
117
+ baseWorkNode.generates.length === 0 &&
118
+ baseWorkNode.secrets.length === 0;
110
119
  return {
111
120
  ...baseWorkNode,
112
121
  type: 'local-task',
122
+ ...(aggregate ? { aggregate } : {}),
113
123
  };
114
124
  }
115
125
  }
@@ -4,6 +4,8 @@ exports.getWorkTaskRuntime = exports.getWorkServiceRuntime = void 0;
4
4
  const work_runtime_docker_1 = require("../work-runtime-docker");
5
5
  const work_runtime_kubernetes_1 = require("../work-runtime-kubernetes");
6
6
  const work_runtime_local_1 = require("../work-runtime-local");
7
+ const work_runtime_aggregate_1 = require("../work-runtime-aggregate");
8
+ const work_task_1 = require("../work-task");
7
9
  const work_item_1 = require("../work-item");
8
10
  function getWorkServiceRuntime(workTree, service) {
9
11
  if ((0, work_item_1.isContainerWorkServiceItem)(service)) {
@@ -24,7 +26,10 @@ function getWorkServiceRuntime(workTree, service) {
24
26
  }
25
27
  exports.getWorkServiceRuntime = getWorkServiceRuntime;
26
28
  function getWorkTaskRuntime(workTree, task) {
27
- if ((0, work_item_1.isLocalWorkTaskItem)(task)) {
29
+ if ((0, work_task_1.isAggregateTask)(task.data)) {
30
+ return (0, work_runtime_aggregate_1.aggregateWorkRuntime)();
31
+ }
32
+ else if ((0, work_item_1.isLocalWorkTaskItem)(task)) {
28
33
  return (0, work_runtime_local_1.getLocalWorkRuntime)(task);
29
34
  }
30
35
  else if ((0, work_item_1.isContainerWorkTaskItem)(task)) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizePath = void 0;
3
+ exports.normalizeHostPath = exports.getHomeReference = exports.normalizePath = void 0;
4
4
  const path_1 = require("path");
5
5
  function normalizePath(cwd, pwd, path) {
6
6
  if (path.startsWith('/')) {
@@ -14,4 +14,22 @@ function normalizePath(cwd, pwd, path) {
14
14
  }
15
15
  }
16
16
  exports.normalizePath = normalizePath;
17
+ // What a path of the host starts with to name the home directory: `~` and `$HOME`, and
18
+ // `$PWD`, which in a mount has always meant the home directory, not the project it reads like
19
+ function getHomeReference(path) {
20
+ for (const reference of ['~', '$HOME']) {
21
+ if (path === reference || path.startsWith(`${reference}/`) || path.startsWith(`${reference}\\`)) {
22
+ return reference;
23
+ }
24
+ }
25
+ return path.startsWith('$PWD') ? '$PWD' : null;
26
+ }
27
+ exports.getHomeReference = getHomeReference;
28
+ // A path of the host in a mount: in the home directory when it starts with one of the
29
+ // references above, else in the directory of the build file unless it is absolute
30
+ function normalizeHostPath(cwd, home, path) {
31
+ const reference = getHomeReference(path);
32
+ return reference ? (0, path_1.join)(home, path.substring(reference.length)) : normalizePath(cwd, home, path);
33
+ }
34
+ exports.normalizeHostPath = normalizeHostPath;
17
35
  //# sourceMappingURL=normalize-path.js.map
@@ -7,6 +7,12 @@ const path_1 = require("path");
7
7
  function parseWorkMount(cwd, dir) {
8
8
  const parts = dir.split(':');
9
9
  if (parts.length === 1) {
10
+ const home = (0, normalize_path_1.getHomeReference)(dir);
11
+ if (home === '~' || home === '$HOME') {
12
+ // the container has no such directory: the same path on both sides would be one of the
13
+ // project there (`$PWD` has always been, so it stays)
14
+ throw new Error(`mount ${dir} is in the home directory, name where it goes in the container (${dir}:/path)`);
15
+ }
10
16
  return parseLocalMount(cwd, dir, dir, dir, false);
11
17
  }
12
18
  else if (parts.length === 2) {
@@ -22,7 +28,7 @@ function parseWorkMount(cwd, dir) {
22
28
  exports.parseWorkMount = parseWorkMount;
23
29
  function parseLocalMount(cwd, localPath, containerPath, mount, readOnly) {
24
30
  return {
25
- localPath: (0, normalize_path_1.normalizePath)(cwd, (0, os_1.homedir)(), localPath),
31
+ localPath: (0, normalize_path_1.normalizeHostPath)(cwd, (0, os_1.homedir)(), localPath),
26
32
  containerPath: (0, normalize_path_1.normalizePath)(cwd, cwd, containerPath),
27
33
  isFile: (0, path_1.extname)(localPath).length > 1,
28
34
  mount,
@@ -8,7 +8,10 @@ function parseWorkPort(port) {
8
8
  return { containerPort: port, hostPort: port };
9
9
  }
10
10
  else if (parts.length === 2) {
11
- return { containerPort: parsePort(parts[1], false), hostPort: parsePort(parts[0], true) };
11
+ // `0:80` is not published, as an empty host port: 0 (a free port) is for the endpoints
12
+ // of a server only
13
+ const hostPort = parsePort(parts[0], true);
14
+ return { containerPort: parsePort(parts[1], false), hostPort: hostPort === 0 ? null : hostPort };
12
15
  }
13
16
  else {
14
17
  throw new Error(`invalid port ${port}`);
@@ -4,6 +4,7 @@ exports.checkIfContextExists = exports.hasDependencyCycle = exports.hasNeedCycle
4
4
  const read_build_file_1 = require("../parser/read-build-file");
5
5
  const plan_work_tasks_1 = require("./utils/plan-work-tasks");
6
6
  const unreferenced_build_file_envs_1 = require("./unreferenced-build-file-envs");
7
+ const normalize_path_1 = require("./utils/normalize-path");
7
8
  async function* validate(workTree, context) {
8
9
  const cycleItems = [];
9
10
  for (const item of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
@@ -16,6 +17,10 @@ async function* validate(workTree, context) {
16
17
  yield { type: 'warn', message: 'missing healthcheck', item: service };
17
18
  }
18
19
  for (const mount of service.mounts) {
20
+ const pwd = getPwdMountWarning(mount);
21
+ if (pwd) {
22
+ yield { type: 'warn', message: pwd, item: service };
23
+ }
19
24
  if (!(await context.file.exists(mount.localPath))) {
20
25
  yield {
21
26
  type: 'warn',
@@ -62,6 +67,12 @@ async function* validate(workTree, context) {
62
67
  item: task,
63
68
  };
64
69
  }
70
+ for (const mount of task.type === 'container-task' ? task.mounts : []) {
71
+ const pwd = getPwdMountWarning(mount);
72
+ if (pwd) {
73
+ yield { type: 'warn', message: pwd, item: task };
74
+ }
75
+ }
65
76
  for (const src of task.src) {
66
77
  if (!(await context.file.exists(src.absolutePath))) {
67
78
  yield {
@@ -146,4 +157,15 @@ async function checkIfContextExists(service, context) {
146
157
  return false;
147
158
  }
148
159
  exports.checkIfContextExists = checkIfContextExists;
160
+ // `$PWD` in a mount is the home directory, which reads like a path of the project
161
+ function getPwdMountWarning(mount) {
162
+ const source = mount.mount.split(':')[0];
163
+ if ((0, normalize_path_1.getHomeReference)(source) !== '$PWD') {
164
+ return null;
165
+ }
166
+ const rest = source.substring('$PWD'.length);
167
+ const home = rest === '' || rest.startsWith('/') || rest.startsWith('\\') ? `~${rest}` : '~/…';
168
+ return (`mount ${source} is ${mount.localPath}: $PWD in a mount is the home directory, not the project. ` +
169
+ `Write ${home} for the home directory, or a relative path for the project`);
170
+ }
149
171
  //# sourceMappingURL=validate.js.map
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyNetworkCeiling = exports.formatNetwork = exports.getTcpForwards = exports.matchesHost = exports.isLimitedNetwork = exports.toNetworkSchema = exports.exceedsNetwork = exports.getEffectiveNetwork = exports.limitNetwork = exports.parseNetworkOption = exports.parseWorkNetwork = exports.OPEN_NETWORK = void 0;
4
4
  const network_schema_1 = require("../schema/network-schema");
5
+ const work_task_1 = require("./work-task");
5
6
  exports.OPEN_NETWORK = { type: 'open' };
6
7
  function parseWorkNetwork(schema) {
7
8
  if (!schema || schema === 'open') {
@@ -153,7 +154,8 @@ exports.formatNetwork = formatNetwork;
153
154
  function applyNetworkCeiling(workTree, ceiling) {
154
155
  const limit = ceiling && isLimitedNetwork(ceiling) ? ceiling : null;
155
156
  if (limit) {
156
- const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task');
157
+ // an aggregate runs nothing, so there is nothing of it to limit
158
+ const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task' && !(0, work_task_1.isAggregateTask)(task.data));
157
159
  if (local) {
158
160
  throw new Error(`--network ${formatNetwork(limit)} needs every task in a container, ` +
159
161
  `hammerkit can't limit the network of the local task ${local.name}`);
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aggregateWorkRuntime = void 0;
4
+ // An aggregate runs nothing, so it touches nothing: no pid or state file in its
5
+ // directory (on a server that is a checkout it doesn't trust), no outputs.
6
+ function aggregateWorkRuntime() {
7
+ return {
8
+ async initialize() {
9
+ // nothing to prepare
10
+ },
11
+ async remove() {
12
+ // it has no outputs of its own
13
+ },
14
+ async execute() {
15
+ // its state stays running, and executeWorkTask completes it
16
+ },
17
+ async stop() {
18
+ // nothing is running
19
+ },
20
+ async archive() {
21
+ // never pushed, see pushToBackend
22
+ },
23
+ async restore() {
24
+ // never a hit, see checkCacheState
25
+ },
26
+ async currentStateKey() {
27
+ return null;
28
+ },
29
+ };
30
+ }
31
+ exports.aggregateWorkRuntime = aggregateWorkRuntime;
32
+ //# sourceMappingURL=work-runtime-aggregate.js.map
@@ -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.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = void 0;
6
+ exports.kubernetesForwardRuntime = exports.isRolledOut = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = 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");
@@ -11,12 +11,14 @@ const replace_env_variables_1 = require("../environment/replace-env-variables");
11
11
  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
+ const follow_pod_logs_1 = require("../kubernetes/follow-pod-logs");
14
15
  const service_definition_1 = require("./service-definition");
15
16
  const ensure_namespace_1 = require("../kubernetes/ensure-namespace");
16
17
  const ensure_persistent_data_1 = require("../kubernetes/ensure-persistent-data");
17
18
  const network_gate_1 = require("../kubernetes/network-gate");
18
19
  const await_running_state_1 = require("../kubernetes/await-running-state");
19
20
  const volumes_1 = require("../kubernetes/volumes");
21
+ const service_rollouts_1 = require("../kubernetes/service-rollouts");
20
22
  const resources_1 = require("../kubernetes/resources");
21
23
  const ensure_http_route_1 = require("../kubernetes/ensure-http-route");
22
24
  const find_process_1 = __importDefault(require("find-process"));
@@ -34,11 +36,12 @@ const job_placement_1 = require("../kubernetes/job-placement");
34
36
  const network_policy_1 = require("../kubernetes/network-policy");
35
37
  const run_id_1 = require("../utils/run-id");
36
38
  const work_network_1 = require("./work-network");
39
+ const sandbox_runner_1 = require("../kubernetes/sandbox-runner");
37
40
  const work_secret_1 = require("./work-secret");
38
41
  async function listJobNames(instance, kubernetes, task) {
39
42
  const jobs = await instance.batchApi.listNamespacedJob({
40
43
  namespace: kubernetes.namespace,
41
- labelSelector: `hammerkit.dev/id=${task.id()}`,
44
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
42
45
  });
43
46
  return jobs.items.flatMap((job) => (job.metadata?.name ? [job.metadata.name] : []));
44
47
  }
@@ -129,7 +132,8 @@ exports.describeFailedJob = describeFailedJob;
129
132
  * The containers of the Job of a command: the network gate first, nothing runs before the
130
133
  * item's policy holds, then the init containers that read its secrets, then the command. The
131
134
  * proxy's token (`network.secretEnv`) reaches the init containers through `secretSpec` only:
132
- * the command's container gets the proxy variables of a limited network without it.
135
+ * the command's container gets the proxy variables of a limited network without it. A task that
136
+ * adds capabilities gets them in the sandbox's RuntimeClass only.
133
137
  */
134
138
  function getTaskJobContainers(options) {
135
139
  const { task, cmd, envs, secrets, network, secretSpec, kubernetes } = options;
@@ -154,9 +158,10 @@ function getTaskJobContainers(options) {
154
158
  name: options.container,
155
159
  volumeMounts: [...options.mounts, ...secrets.mounts, ...secretSpec.mounts],
156
160
  ...(0, container_resources_1.getKubernetesResources)(task.data.resources),
157
- ...(0, job_placement_1.getContainerSecurity)(kubernetes),
161
+ ...(0, job_placement_1.getContainerSecurity)(kubernetes, task.data.capAdd),
158
162
  },
159
163
  ],
164
+ ...(0, job_placement_1.getPodRuntimeClass)({ name: task.name, capAdd: task.data.capAdd }, kubernetes),
160
165
  };
161
166
  }
162
167
  exports.getTaskJobContainers = getTaskJobContainers;
@@ -171,29 +176,67 @@ function kubernetesTaskRuntime(task, kubernetes) {
171
176
  return Promise.resolve();
172
177
  },
173
178
  async restore(environment, path, stateKey) {
179
+ // a cache hit of a service run at the state that services of the run mount (a move whose
180
+ // dep did not change): its outputs are there, and stay as they are while pods read them
181
+ if (kubernetes.server?.serviceRun &&
182
+ stateKey &&
183
+ (await (0, ensure_persistent_data_1.getStateDirsInUse)(instance, kubernetes.namespace, claim)).has((0, volumes_1.getStateFilesDir)(stateKey))) {
184
+ task.status.write('info', `${task.name}: services of the run use its outputs of this state, they stay`);
185
+ return;
186
+ }
174
187
  if (!kept) {
175
- await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path);
188
+ await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path, { stateKey });
176
189
  return;
177
190
  }
178
191
  const reset = await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim);
179
- await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path, { reset });
192
+ await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path, { reset, stateKey });
180
193
  if (stateKey) {
181
194
  await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, stateKey);
182
195
  }
183
196
  },
184
- async archive(environment, path) {
185
- await (0, store_kubernetes_data_1.storeKubernetesData)(task, kubernetes, instance, environment, path);
197
+ async archive(environment, path, stateKey) {
198
+ await (0, store_kubernetes_data_1.storeKubernetesData)(task, kubernetes, instance, environment, path, stateKey);
186
199
  },
187
200
  async execute(environment, options) {
188
201
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.data.envs);
189
- const persistence = await (0, volumes_1.getKubernetesPersistence)(task, kubernetes);
202
+ const persistence = await (0, volumes_1.getKubernetesPersistence)(task, kubernetes, options.stateKey);
190
203
  const workspace = (0, volumes_1.getWorkspacePodSpec)(kubernetes, persistence, { name: task.name, pool: task.data.pool });
204
+ // a shell of a sandbox: its commands go into the sandbox's runner, when it has one now
205
+ if ((0, sandbox_runner_1.canUseRunner)(task, kubernetes, persistence)) {
206
+ try {
207
+ const result = await (0, sandbox_runner_1.executeInRunner)(instance, task, kubernetes, persistence, options.abort);
208
+ if (result.type === 'crash') {
209
+ options.state.set({ stateKey: options.stateKey, type: 'crash', exitCode: result.exitCode });
210
+ return;
211
+ }
212
+ // stopped while the runner started: no Job for it either
213
+ if (result.type === 'canceled' || options.abort.aborted) {
214
+ options.state.set({ stateKey: options.stateKey, type: 'canceled' });
215
+ return;
216
+ }
217
+ if (result.type === 'done') {
218
+ return;
219
+ }
220
+ }
221
+ catch (e) {
222
+ options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
223
+ return;
224
+ }
225
+ }
191
226
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
227
+ // a service run writes the outputs of each state into a folder of their own (see
228
+ // getStateFilesDir), which pods of its services may mount: of the commit before during
229
+ // a move, or of this state after a move that went back
230
+ const files = kubernetes.server?.serviceRun ? { dir: (0, volumes_1.getStateFilesDir)(options.stateKey) } : undefined;
192
231
  try {
232
+ if (files && (await (0, ensure_persistent_data_1.getStateDirsInUse)(instance, kubernetes.namespace, claim)).has(files.dir)) {
233
+ task.status.write('info', `${task.name}: services of the run use its outputs of this state, they stay`);
234
+ return;
235
+ }
193
236
  // the task writes to it now: what it holds stops being the outputs of a state, and
194
237
  // goes, as a new claim would hold nothing
195
238
  const reset = kept && (await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim));
196
- await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence, { reset });
239
+ await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence, { reset, files });
197
240
  }
198
241
  catch (e) {
199
242
  options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
@@ -201,7 +244,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
201
244
  }
202
245
  const server = kubernetes.server;
203
246
  const { delivered, inJob } = (0, job_secrets_1.splitJobSecrets)(task.data.secrets, server);
204
- const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, delivered);
247
+ const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, kubernetes, delivered);
205
248
  const admitted = environment.hosted?.accounts;
206
249
  let secretHosts;
207
250
  try {
@@ -230,7 +273,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
230
273
  : {}),
231
274
  };
232
275
  const labels = {
233
- 'hammerkit.dev/id': task.id(),
276
+ ...(0, resources_1.getItemLabels)(task, kubernetes),
234
277
  'hammerkit.dev/state': options.stateKey,
235
278
  ...workspace.labels,
236
279
  [network_policy_1.RUN_LABEL]: scope.run,
@@ -258,7 +301,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
258
301
  return;
259
302
  }
260
303
  try {
261
- const podName = (0, resources_1.getResourceName)(task, '-job');
304
+ const podName = (0, resources_1.getResourceName)(task, kubernetes, '-job');
262
305
  let i = 0;
263
306
  for (const cmd of task.data.cmds) {
264
307
  const container = `cmd-${++i}`;
@@ -387,7 +430,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
387
430
  async stop() {
388
431
  const jobs = await instance.batchApi.listNamespacedJob({
389
432
  namespace: kubernetes.namespace,
390
- labelSelector: `hammerkit.dev/id=${task.id()}`,
433
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
391
434
  });
392
435
  for (const job of jobs.items) {
393
436
  if (job.metadata?.name) {
@@ -396,7 +439,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
396
439
  }
397
440
  const deployments = await instance.appsApi.listNamespacedDeployment({
398
441
  namespace: kubernetes.namespace,
399
- labelSelector: `hammerkit.dev/id=${task.id()}`,
442
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
400
443
  });
401
444
  for (const deploy of deployments.items) {
402
445
  if (deploy.metadata?.name) {
@@ -408,7 +451,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
408
451
  }
409
452
  const pods = await instance.coreApi.listNamespacedPod({
410
453
  namespace: kubernetes.namespace,
411
- labelSelector: `hammerkit.dev/id=${task.id()}`,
454
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
412
455
  });
413
456
  for (const pod of pods.items) {
414
457
  if (pod.metadata?.name) {
@@ -430,7 +473,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
430
473
  }
431
474
  const jobs = await instance.batchApi.listNamespacedJob({
432
475
  namespace: kubernetes.namespace,
433
- labelSelector: `hammerkit.dev/id=${task.id()}`,
476
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
434
477
  });
435
478
  const completedStates = jobs.items
436
479
  .filter((j) => j.status?.succeeded && j.metadata?.labels?.['hammerkit.dev/state'])
@@ -465,9 +508,13 @@ function kubernetesServiceRuntime(service, kubernetes) {
465
508
  // stops it and a run reuses it, as with a container on Docker. One whose
466
509
  // definition changed goes, with its limits, and is created anew.
467
510
  async initialize(state) {
511
+ if (kubernetes.server?.serviceRun) {
512
+ // a service run starts every service: one that did not change stays as it is (execute)
513
+ return;
514
+ }
468
515
  const deployments = await instance.appsApi.listNamespacedDeployment({
469
516
  namespace: kubernetes.namespace,
470
- labelSelector: `hammerkit.dev/id=${service.id()}`,
517
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
471
518
  });
472
519
  const deployment = deployments.items[0];
473
520
  if (!deployment?.metadata?.name) {
@@ -494,7 +541,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
494
541
  async stop() {
495
542
  const deployments = await instance.appsApi.listNamespacedDeployment({
496
543
  namespace: kubernetes.namespace,
497
- labelSelector: `hammerkit.dev/id=${service.id()}`,
544
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
498
545
  });
499
546
  for (const deployment of deployments.items) {
500
547
  if (deployment.metadata?.name) {
@@ -509,14 +556,70 @@ function kubernetesServiceRuntime(service, kubernetes) {
509
556
  await (0, network_policy_1.removeAllKubernetesItemNetworks)(instance, kubernetes, service);
510
557
  },
511
558
  async execute(environment, options) {
512
- const persistence = await (0, volumes_1.getKubernetesPersistence)(service, kubernetes);
559
+ const name = (0, resources_1.getResourceName)(service, kubernetes);
560
+ // a service run that moves to another commit: a service of the same definition and
561
+ // state is left running, another one is rolled from the Deployment it has now
562
+ let previous;
563
+ if (kubernetes.server?.serviceRun) {
564
+ previous = await readDeployment(instance, kubernetes.namespace, name);
565
+ if (previous && isRolledOut(previous, (0, service_definition_1.getServiceDefinitionHash)(service), options.stateKey)) {
566
+ service.status.write('info', `${service.name} did not change, it stays up`);
567
+ options.state.set({ type: 'running', dns: { containerId: name }, stateKey: options.stateKey, remote: null });
568
+ return;
569
+ }
570
+ }
571
+ const persistence = await (0, volumes_1.getKubernetesPersistence)(service, kubernetes, options.stateKey);
572
+ // the files of a service run's service go into a folder of their state, so the pod that
573
+ // runs keeps its own until the new one is up. One that watches its files (`continuous`)
574
+ // gets them synced into the folder its pod mounts: the pod that runs sees the changes.
575
+ const live = (0, volumes_1.isLiveService)(service, kubernetes);
576
+ // the files of the state it runs at now, to put back when the move does not get up
577
+ const before = live ? previous?.metadata?.labels?.['hammerkit.dev/state'] : undefined;
578
+ const files = kubernetes.server?.serviceRun
579
+ ? {
580
+ dir: (0, volumes_1.getStateFilesDir)(options.stateKey),
581
+ live,
582
+ keep: before ? [(0, volumes_1.getStateFilesDir)(before)] : [],
583
+ }
584
+ : undefined;
585
+ if (live && previous && before) {
586
+ const from = (0, volumes_1.getStateFilesDir)(before);
587
+ service_rollouts_1.serviceRollouts.add(kubernetes.namespace, name, previous, () => (0, ensure_persistent_data_1.restoreLiveFiles)(instance, kubernetes, service, persistence, from));
588
+ }
513
589
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
514
590
  await (0, ensure_kubernetes_service_exists_1.ensureKubernetesServiceExists)(instance, kubernetes, service);
515
- await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence);
591
+ await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence, { files });
516
592
  await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, service, service.data.secrets, environment);
517
- const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, { followLog: false, run: null });
518
- await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, service.data.secrets), options.stateKey, network);
519
- const name = (0, resources_1.getResourceName)(service);
593
+ // on a server, the run's: its runs share the namespace, and the service goes with the run
594
+ const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, {
595
+ followLog: false,
596
+ run: kubernetes.server ? (0, job_placement_1.getRunLabelValue)(kubernetes.server.run) : null,
597
+ });
598
+ // its output, as on Docker: while it starts, and while it runs unless it runs on
599
+ // without hammerkit (`up --daemon`)
600
+ const logs = (0, follow_pod_logs_1.followPodLogs)(instance, kubernetes.namespace, (0, resources_1.getItemSelector)(service, kubernetes), (0, ensure_kubernetes_deployment_exists_1.getServiceContainerName)(service), (line) => service.status.console('stdout', line), { onError: (text) => service.status.write('debug', text) });
601
+ try {
602
+ await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, kubernetes, service.data.secrets), options.stateKey, network, { previous, signal: options.abort });
603
+ }
604
+ catch (e) {
605
+ // the last words of a container that crashed, before why it failed
606
+ await logs.settle().catch(() => undefined);
607
+ logs.stop();
608
+ throw e;
609
+ }
610
+ if (options.daemon || options.abort.aborted || environment.abortCtrl.signal.aborted) {
611
+ logs.stop();
612
+ }
613
+ else {
614
+ // either ends it, and takes the other listener with it
615
+ const listening = new AbortController();
616
+ const stop = () => {
617
+ logs.stop();
618
+ listening.abort();
619
+ };
620
+ options.abort.addEventListener('abort', stop, { once: true, signal: listening.signal });
621
+ environment.abortCtrl.signal.addEventListener('abort', stop, { once: true, signal: listening.signal });
622
+ }
520
623
  options.state.set({
521
624
  type: 'running',
522
625
  dns: { containerId: name },
@@ -548,7 +651,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
548
651
  }
549
652
  const services = await instance.coreApi.listNamespacedService({
550
653
  namespace: kubernetes.namespace,
551
- labelSelector: `hammerkit.dev/id=${service.id()}`,
654
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
552
655
  });
553
656
  for (const service of services.items) {
554
657
  if (service.metadata?.name) {
@@ -564,7 +667,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
564
667
  async currentStateKey() {
565
668
  const deployments = await instance.appsApi.listNamespacedDeployment({
566
669
  namespace: kubernetes.namespace,
567
- labelSelector: `hammerkit.dev/id=${service.id()}`,
670
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
568
671
  });
569
672
  const deployment = deployments.items[0];
570
673
  return deployment?.metadata?.labels?.['hammerkit.dev/state'] ?? null;
@@ -572,6 +675,30 @@ function kubernetesServiceRuntime(service, kubernetes) {
572
675
  };
573
676
  }
574
677
  exports.kubernetesServiceRuntime = kubernetesServiceRuntime;
678
+ async function readDeployment(instance, namespace, name) {
679
+ try {
680
+ return await instance.appsApi.readNamespacedDeployment({ name, namespace });
681
+ }
682
+ catch (e) {
683
+ if ((0, apply_1.statusCodeOf)(e) === 404) {
684
+ return undefined;
685
+ }
686
+ throw e;
687
+ }
688
+ }
689
+ // A Deployment of this definition and state whose pods are all of it and ready
690
+ function isRolledOut(deployment, definition, stateKey) {
691
+ const labels = deployment.metadata?.labels ?? {};
692
+ const status = deployment.status;
693
+ return (!deployment.metadata?.deletionTimestamp &&
694
+ labels[ensure_kubernetes_deployment_exists_1.DEFINITION_LABEL] === definition &&
695
+ labels['hammerkit.dev/state'] === stateKey &&
696
+ (status?.observedGeneration ?? 0) >= (deployment.metadata?.generation ?? 0) &&
697
+ (status?.updatedReplicas ?? 0) >= 1 &&
698
+ (status?.availableReplicas ?? 0) >= 1 &&
699
+ (status?.replicas ?? 0) === (status?.updatedReplicas ?? 0));
700
+ }
701
+ exports.isRolledOut = isRolledOut;
575
702
  function kubernetesForwardRuntime(service) {
576
703
  return {
577
704
  async initialize(item) {
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isWorkTask = exports.isContainerWorkTask = void 0;
3
+ exports.isAggregateTask = exports.isWorkTask = exports.isContainerWorkTask = void 0;
4
4
  const isContainerWorkTask = (val) => val.type === 'container-task';
5
5
  exports.isContainerWorkTask = isContainerWorkTask;
6
6
  const isWorkTask = (val) => val.type === 'container-task' || val.type === 'local-task';
7
7
  exports.isWorkTask = isWorkTask;
8
+ // A task that only groups its dependencies: `deps`, and no `image`, `cmds`, `src`,
9
+ // `generates`, `secrets` or `needs`. It needs no runtime, hands on what its dependencies generate to what
10
+ // depends on it, and is up to date (or verified) when all its dependencies are.
11
+ const isAggregateTask = (val) => val.type === 'local-task' && val.aggregate === true;
12
+ exports.isAggregateTask = isAggregateTask;
8
13
  //# sourceMappingURL=work-task.js.map