hammerkit 1.26.0 → 1.27.0-next.10

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 (128) hide show
  1. package/build.schema.json +62 -1
  2. package/dist/cache/backends/local-cache-backend.js +10 -2
  3. package/dist/cache/backends/registry-cache-backend.js +2 -2
  4. package/dist/cache/explain.js +13 -11
  5. package/dist/cli.js +3 -0
  6. package/dist/docker/item-network.js +471 -0
  7. package/dist/docker/pull.js +4 -2
  8. package/dist/docker/remove-orphaned-containers.js +13 -6
  9. package/dist/environment/replace-env-variables.js +8 -6
  10. package/dist/executer/await-completed-dependencies.js +2 -1
  11. package/dist/executer/check-readiness.js +2 -2
  12. package/dist/executer/docker-service.js +24 -13
  13. package/dist/executer/docker-task.js +18 -29
  14. package/dist/executer/event-cache.js +24 -2
  15. package/dist/executer/execute-command.js +18 -5
  16. package/dist/executer/execute-docker.js +4 -1
  17. package/dist/executer/execution-steps.js +33 -7
  18. package/dist/executer/get-docker-executor.js +14 -7
  19. package/dist/executer/scheduler/enqueue-next.js +17 -3
  20. package/dist/executer/watch-loop.js +2 -2
  21. package/dist/executer/watch-state-key.js +1 -1
  22. package/dist/file/within-root.js +42 -0
  23. package/dist/harness/connect.js +99 -0
  24. package/dist/harness/format.js +73 -0
  25. package/dist/harness/mcp-command.js +59 -0
  26. package/dist/harness/mcp.js +432 -0
  27. package/dist/harness/opencode.js +492 -0
  28. package/dist/harness/pi.js +217 -0
  29. package/dist/harness/sandbox-client.js +411 -0
  30. package/dist/index.js +33 -36
  31. package/dist/kubernetes/apply.js +42 -1
  32. package/dist/kubernetes/await-running-state.js +33 -1
  33. package/dist/kubernetes/claim-layout.js +71 -0
  34. package/dist/kubernetes/ensure-http-route.js +1 -1
  35. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +46 -28
  36. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +132 -5
  37. package/dist/kubernetes/ensure-kubernetes-service-exists.js +27 -11
  38. package/dist/kubernetes/ensure-persistent-data.js +75 -24
  39. package/dist/kubernetes/get-service-ip.js +16 -2
  40. package/dist/kubernetes/job-placement.js +99 -0
  41. package/dist/kubernetes/job-secrets.js +320 -0
  42. package/dist/kubernetes/kept-claims.js +203 -0
  43. package/dist/kubernetes/kubernetes-instance.js +10 -2
  44. package/dist/kubernetes/network-gate.js +71 -0
  45. package/dist/kubernetes/network-policy.js +583 -0
  46. package/dist/kubernetes/remove-persistent-data.js +3 -1
  47. package/dist/kubernetes/resources.js +37 -4
  48. package/dist/kubernetes/restore-kubernetes-data.js +28 -7
  49. package/dist/kubernetes/secrets.js +7 -9
  50. package/dist/kubernetes/store-kubernetes-data.js +1 -1
  51. package/dist/kubernetes/volumes.js +104 -12
  52. package/dist/log.js +1 -1
  53. package/dist/main.js +41 -0
  54. package/dist/network/egress-proxy.js +407 -0
  55. package/dist/network/port-relay.js +35 -0
  56. package/dist/optimizer/get-work-cache-stats.js +16 -0
  57. package/dist/optimizer/work-task-cache-description.js +5 -2
  58. package/dist/planner/service-definition.js +5 -0
  59. package/dist/planner/utils/append-work-dependencies.js +5 -2
  60. package/dist/planner/utils/append-work-environment.js +2 -0
  61. package/dist/planner/utils/append-work-service.js +5 -0
  62. package/dist/planner/utils/append-work-task.js +20 -1
  63. package/dist/planner/utils/assert-item-paths.js +50 -0
  64. package/dist/planner/utils/hosted-volumes.js +35 -0
  65. package/dist/planner/utils/task-arguments.js +3 -0
  66. package/dist/planner/work-network.js +193 -0
  67. package/dist/planner/work-runtime-docker.js +43 -1
  68. package/dist/planner/work-runtime-kubernetes.js +370 -111
  69. package/dist/planner/work-runtime-local.js +2 -1
  70. package/dist/program.js +33 -5
  71. package/dist/remote/annotations.js +115 -0
  72. package/dist/remote/bundle-text.js +24 -0
  73. package/dist/remote/bundle.js +9 -1
  74. package/dist/remote/remote-commands.js +15 -3
  75. package/dist/remote/remote-work-commands.js +272 -71
  76. package/dist/remote/server-client.js +18 -5
  77. package/dist/remote/source-manifest.js +15 -2
  78. package/dist/remote/token-source.js +67 -0
  79. package/dist/schema/build-file-base-task-schema.js +7 -0
  80. package/dist/schema/build-file-container-service-schema.js +8 -0
  81. package/dist/schema/build-file-container-task-schema.js +3 -0
  82. package/dist/schema/build-file-environment-schema.js +9 -0
  83. package/dist/schema/network-schema.js +78 -0
  84. package/dist/schema/reference-parser.js +2 -0
  85. package/dist/schema/schema-parser.js +16 -3
  86. package/dist/schema/work-scope-parser.js +5 -0
  87. package/dist/secrets/command-secret-provider.js +73 -10
  88. package/dist/secrets/github-app-token-command.js +26 -0
  89. package/dist/secrets/github-app-token.js +191 -0
  90. package/dist/server/accounts-api.js +9 -2
  91. package/dist/server/definitions-api.js +69 -47
  92. package/dist/server/definitions.js +76 -51
  93. package/dist/server/executor.js +156 -41
  94. package/dist/server/git-checkout.js +72 -8
  95. package/dist/server/kubernetes-backend.js +603 -0
  96. package/dist/server/kubernetes-sandbox-storage.js +997 -0
  97. package/dist/server/oauth.js +12 -0
  98. package/dist/server/plan.js +88 -2
  99. package/dist/server/policy.js +104 -15
  100. package/dist/server/proposals-api.js +24 -40
  101. package/dist/server/proposals.js +12 -25
  102. package/dist/server/repo-access.js +37 -6
  103. package/dist/server/run-logs.js +217 -0
  104. package/dist/server/run-plan.js +48 -2
  105. package/dist/server/run-request.js +29 -10
  106. package/dist/server/run-store.js +5 -0
  107. package/dist/server/run-worker-command.js +13 -1
  108. package/dist/server/run-worker.js +121 -24
  109. package/dist/server/runs.js +191 -10
  110. package/dist/server/sandbox-docker.js +62 -0
  111. package/dist/server/sandbox-files-api.js +269 -0
  112. package/dist/server/sandbox-files.js +1054 -0
  113. package/dist/server/sandbox-patch.js +481 -0
  114. package/dist/server/sandbox-pod-command.js +15 -0
  115. package/dist/server/sandbox-pod.js +146 -0
  116. package/dist/server/sandbox-storage.js +462 -0
  117. package/dist/server/sandboxes-api.js +216 -0
  118. package/dist/server/sandboxes.js +519 -0
  119. package/dist/server/server-command.js +39 -20
  120. package/dist/server/server-config.js +443 -22
  121. package/dist/server/server.js +219 -40
  122. package/dist/utils/abort-on-signals.js +46 -6
  123. package/dist/utils/run-id.js +12 -0
  124. package/dist/utils/tar.js +25 -0
  125. package/harness/opencode.ts +11 -0
  126. package/harness/pi.ts +7 -0
  127. package/package.json +8 -2
  128. package/dist/remote/text-diff.js +0 -89
@@ -3,18 +3,18 @@ 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 = void 0;
6
+ exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = void 0;
7
7
  const abort_1 = require("../executer/abort");
8
- const get_service_ip_1 = require("../kubernetes/get-service-ip");
9
- const work_item_1 = require("./work-item");
10
8
  const kubernetes_service_1 = require("../executer/kubernetes-service");
11
9
  const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
12
10
  const replace_env_variables_1 = require("../environment/replace-env-variables");
13
11
  const apply_1 = require("../kubernetes/apply");
14
12
  const ensure_kubernetes_service_exists_1 = require("../kubernetes/ensure-kubernetes-service-exists");
15
13
  const ensure_kubernetes_deployment_exists_1 = require("../kubernetes/ensure-kubernetes-deployment-exists");
14
+ const service_definition_1 = require("./service-definition");
16
15
  const ensure_namespace_1 = require("../kubernetes/ensure-namespace");
17
16
  const ensure_persistent_data_1 = require("../kubernetes/ensure-persistent-data");
17
+ const network_gate_1 = require("../kubernetes/network-gate");
18
18
  const await_running_state_1 = require("../kubernetes/await-running-state");
19
19
  const volumes_1 = require("../kubernetes/volumes");
20
20
  const resources_1 = require("../kubernetes/resources");
@@ -26,152 +26,377 @@ const task_arguments_1 = require("./utils/task-arguments");
26
26
  const restore_kubernetes_data_1 = require("../kubernetes/restore-kubernetes-data");
27
27
  const store_kubernetes_data_1 = require("../kubernetes/store-kubernetes-data");
28
28
  const remove_persistent_data_1 = require("../kubernetes/remove-persistent-data");
29
+ const kept_claims_1 = require("../kubernetes/kept-claims");
29
30
  const container_resources_1 = require("../kubernetes/container-resources");
30
31
  const secrets_1 = require("../kubernetes/secrets");
32
+ const job_secrets_1 = require("../kubernetes/job-secrets");
33
+ const job_placement_1 = require("../kubernetes/job-placement");
34
+ const network_policy_1 = require("../kubernetes/network-policy");
35
+ const run_id_1 = require("../utils/run-id");
36
+ const work_network_1 = require("./work-network");
37
+ const work_secret_1 = require("./work-secret");
31
38
  async function listJobNames(instance, kubernetes, task) {
32
39
  const jobs = await instance.batchApi.listNamespacedJob({
33
40
  namespace: kubernetes.namespace,
34
- labelSelector: `hammerkit.dev/id=${task.id()}`,
41
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
35
42
  });
36
43
  return jobs.items.flatMap((job) => (job.metadata?.name ? [job.metadata.name] : []));
37
44
  }
45
+ // The Secret of the values the server hands over and the one of the credentials the
46
+ // pod reads its own secrets with
47
+ async function removeTaskSecrets(instance, kubernetes, task) {
48
+ await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task);
49
+ await (0, job_secrets_1.removeJobCredentials)(instance, kubernetes, task);
50
+ }
51
+ // The newest pod of a Job
52
+ async function getJobPod(instance, kubernetes, job) {
53
+ const pods = await instance.coreApi.listNamespacedPod({
54
+ namespace: kubernetes.namespace,
55
+ labelSelector: `job-name=${job}`,
56
+ });
57
+ return pods.items
58
+ .slice()
59
+ .sort((a, b) => (a.metadata?.creationTimestamp?.toISOString() ?? '').localeCompare(b.metadata?.creationTimestamp?.toISOString() ?? ''))
60
+ .pop();
61
+ }
62
+ // a task's log is kept to this, the run's own cap is smaller
63
+ const MAX_JOB_LOG_BYTES = 4 * 1024 * 1024;
64
+ // What the command printed, as the task's output: read from the cluster once the Job
65
+ // has ended. Without a pod (it never started) there is nothing to say.
66
+ async function writeJobLogs(instance, kubernetes, task, job, container) {
67
+ try {
68
+ const pod = await getJobPod(instance, kubernetes, job);
69
+ if (!pod?.metadata?.name) {
70
+ return;
71
+ }
72
+ const log = await instance.coreApi.readNamespacedPodLog({
73
+ name: pod.metadata.name,
74
+ namespace: kubernetes.namespace,
75
+ container,
76
+ limitBytes: MAX_JOB_LOG_BYTES,
77
+ });
78
+ for (const line of String(log ?? '').split(/\r?\n/)) {
79
+ if (line.length > 0) {
80
+ task.status.console('stdout', line);
81
+ }
82
+ }
83
+ }
84
+ catch (e) {
85
+ task.status.write('debug', `the log of ${job} could not be read: ${(0, log_1.getErrorMessage)(e)}`);
86
+ }
87
+ }
88
+ // the command of a Job exited with this code, as a crash on docker or a host
89
+ class JobCrash extends Error {
90
+ constructor(exitCode) {
91
+ super(`exit code ${exitCode}`);
92
+ this.exitCode = exitCode;
93
+ }
94
+ }
95
+ // Why a Job failed: the network gate, an init container that could not read a secret, which
96
+ // names the secret, never its output (which is the provider's and can hold the value), or the
97
+ // exit code of the command.
98
+ async function describeFailedJob(instance, kubernetes, task, job, container, secrets) {
99
+ const pod = await getJobPod(instance, kubernetes, job).catch(() => undefined);
100
+ for (const status of pod?.status?.initContainerStatuses ?? []) {
101
+ const terminated = status.state?.terminated ?? status.lastState?.terminated;
102
+ if (!terminated || terminated.exitCode === 0) {
103
+ continue;
104
+ }
105
+ if (status.name === network_gate_1.NETWORK_GATE_CONTAINER) {
106
+ // the gate says why in its termination message
107
+ return {
108
+ type: 'error',
109
+ message: `${task}: ${terminated.message?.trim() || 'the network policy did not take hold'}`,
110
+ };
111
+ }
112
+ const secret = secrets.find((s) => `secret-${s.index}` === status.name);
113
+ if (secret) {
114
+ return {
115
+ type: 'error',
116
+ message: `secret ${(0, work_secret_1.getSecretName)(secret.secret)} (${(0, work_secret_1.describeSecretSource)(secret.secret)}) could not be read: ${terminated.reason ?? 'exit code'} ${terminated.exitCode}`,
117
+ };
118
+ }
119
+ }
120
+ const command = pod?.status?.containerStatuses?.find((status) => status.name === container);
121
+ const terminated = command?.state?.terminated ?? command?.lastState?.terminated;
122
+ if (terminated && terminated.exitCode !== 0) {
123
+ return { type: 'crash', exitCode: terminated.exitCode };
124
+ }
125
+ return undefined;
126
+ }
127
+ exports.describeFailedJob = describeFailedJob;
128
+ /**
129
+ * The containers of the Job of a command: the network gate first, nothing runs before the
130
+ * item's policy holds, then the init containers that read its secrets, then the command. The
131
+ * 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.
133
+ */
134
+ function getTaskJobContainers(options) {
135
+ const { task, cmd, envs, secrets, network, secretSpec, kubernetes } = options;
136
+ // the arguments after `--` keep their `$` as text, Kubernetes expands `$(NAME)`
137
+ const command = secretSpec.command(task.data.shell, (0, task_arguments_1.getKubernetesCommandText)(cmd));
138
+ const initContainers = [...network.initContainers, ...secretSpec.initContainers];
139
+ return {
140
+ ...(initContainers.length > 0 ? { initContainers } : {}),
141
+ containers: [
142
+ {
143
+ image: task.data.image,
144
+ // through the task's shell, as on Docker
145
+ command: command.command,
146
+ args: command.args,
147
+ workingDir: cmd.cwd,
148
+ env: [
149
+ ...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
150
+ ...secrets.env,
151
+ ...network.env,
152
+ ...command.env,
153
+ ],
154
+ name: options.container,
155
+ volumeMounts: [...options.mounts, ...secrets.mounts, ...secretSpec.mounts],
156
+ ...(0, container_resources_1.getKubernetesResources)(task.data.resources),
157
+ ...(0, job_placement_1.getContainerSecurity)(kubernetes),
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ exports.getTaskJobContainers = getTaskJobContainers;
38
163
  function kubernetesTaskRuntime(task, kubernetes) {
39
164
  const instance = (0, kubernetes_instance_1.createKubernetesInstances)(kubernetes);
165
+ // on a server that keeps them, the task's claim stays after the run with the state of its
166
+ // outputs, and the next run of the same source mounts them as they are (see kept-claims)
167
+ const kept = (0, kept_claims_1.isKeptClaim)(task, kubernetes);
168
+ const claim = (0, volumes_1.getVolumeName)(task, kubernetes);
40
169
  return {
41
170
  initialize() {
42
171
  return Promise.resolve();
43
172
  },
44
- async restore(environment, path) {
45
- await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path);
173
+ async restore(environment, path, stateKey) {
174
+ if (!kept) {
175
+ await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path);
176
+ return;
177
+ }
178
+ 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 });
180
+ if (stateKey) {
181
+ await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, stateKey);
182
+ }
46
183
  },
47
184
  async archive(environment, path) {
48
185
  await (0, store_kubernetes_data_1.storeKubernetesData)(task, kubernetes, instance, environment, path);
49
186
  },
50
187
  async execute(environment, options) {
51
188
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.data.envs);
52
- const persistence = await (0, volumes_1.getKubernetesPersistence)(task);
189
+ const persistence = await (0, volumes_1.getKubernetesPersistence)(task, kubernetes);
190
+ const workspace = (0, volumes_1.getWorkspacePodSpec)(kubernetes, persistence, { name: task.name, pool: task.data.pool });
53
191
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
54
- await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence);
55
- const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, task.data.secrets);
56
192
  try {
57
- await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, task, task.data.secrets, environment);
193
+ // the task writes to it now: what it holds stops being the outputs of a state, and
194
+ // goes, as a new claim would hold nothing
195
+ 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 });
197
+ }
198
+ catch (e) {
199
+ options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
200
+ return;
201
+ }
202
+ const server = kubernetes.server;
203
+ const { delivered, inJob } = (0, job_secrets_1.splitJobSecrets)(task.data.secrets, server);
204
+ const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, kubernetes, delivered);
205
+ const admitted = environment.hosted?.accounts;
206
+ let secretHosts;
207
+ try {
208
+ secretHosts = (0, job_secrets_1.getSecretHosts)(task, inJob, server);
209
+ if (server) {
210
+ (0, job_secrets_1.assertJobAccounts)(inJob, server, admitted);
211
+ }
212
+ await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, task, delivered, environment);
58
213
  }
59
214
  catch (e) {
215
+ await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
60
216
  options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
61
217
  return;
62
218
  }
219
+ // the run its pods and limits belong to: on a server, the server's run; on a
220
+ // sandbox, its proxy stays with the sandbox for the next runs
221
+ const scope = {
222
+ run: server ? (0, job_placement_1.getRunLabelValue)(server.run) : (0, run_id_1.getRunId)(),
223
+ ...(server?.workspace
224
+ ? {
225
+ sandbox: {
226
+ label: server.workspace.label,
227
+ ...(server.workspace.pod ? { pod: server.workspace.pod } : {}),
228
+ },
229
+ }
230
+ : {}),
231
+ };
232
+ const labels = {
233
+ ...(0, resources_1.getItemLabels)(task, kubernetes),
234
+ 'hammerkit.dev/state': options.stateKey,
235
+ ...workspace.labels,
236
+ [network_policy_1.RUN_LABEL]: scope.run,
237
+ ...(0, network_policy_1.getNetworkLabels)(kubernetes, task),
238
+ };
239
+ const placement = server ? (0, job_placement_1.getPodPlacement)(server, task.data.pool) : null;
63
240
  // The last job of a run stays: its state label is what currentStateKey
64
241
  // reads. Jobs from an earlier run of this task go before this one starts.
65
242
  for (const name of await listJobNames(instance, kubernetes, task)) {
66
243
  await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, name, options.abort);
67
244
  }
68
- // the services the task needs, by the names it lists them under
69
- const hostAliases = [];
70
- for (const need of task.needs) {
71
- if (!(0, work_item_1.isContainerWorkServiceItem)(need.service)) {
72
- continue;
73
- }
74
- const ip = await (0, get_service_ip_1.getServiceIp)(instance, kubernetes, need.service);
75
- if (!ip) {
76
- throw new Error(`unable to get service ip for ${need.name}`);
77
- }
78
- hostAliases.push({ ip, hostnames: [need.name] });
245
+ // the services the task needs by the names it lists them under, and the
246
+ // NetworkPolicy and proxy of a limited network, both of this run
247
+ let network;
248
+ try {
249
+ network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, task, {
250
+ followLog: true,
251
+ secretHosts,
252
+ ...scope,
253
+ });
79
254
  }
80
- const podName = `${task.name}-${options.stateKey}`;
81
- let i = 0;
82
- for (const cmd of task.data.cmds) {
83
- const spec = {
84
- kind: 'Job',
85
- apiVersion: 'batch/v1',
86
- metadata: {
87
- namespace: kubernetes.namespace,
88
- name: podName,
89
- annotations: {
90
- 'hammerkit.dev/version': (0, version_1.getVersion)(),
91
- },
92
- labels: {
93
- 'hammerkit.dev/id': task.id(),
94
- 'hammerkit.dev/state': options.stateKey,
255
+ catch (e) {
256
+ await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
257
+ options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
258
+ return;
259
+ }
260
+ try {
261
+ const podName = (0, resources_1.getResourceName)(task, kubernetes, '-job');
262
+ let i = 0;
263
+ for (const cmd of task.data.cmds) {
264
+ const container = `cmd-${++i}`;
265
+ let secretSpec;
266
+ try {
267
+ // made for each Job right before it, so a later command's token is as fresh
268
+ const credentials = await (0, job_secrets_1.ensureJobCredentials)(instance, kubernetes, task, inJob, admitted);
269
+ secretSpec = (0, job_secrets_1.getJobSecretSpec)(task, inJob, server, credentials, environment, network.secretEnv);
270
+ }
271
+ catch (e) {
272
+ await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
273
+ options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
274
+ return;
275
+ }
276
+ const spec = {
277
+ kind: 'Job',
278
+ apiVersion: 'batch/v1',
279
+ metadata: {
280
+ namespace: kubernetes.namespace,
281
+ name: podName,
282
+ annotations: {
283
+ 'hammerkit.dev/version': (0, version_1.getVersion)(),
284
+ },
285
+ labels,
95
286
  },
96
- },
97
- spec: {
98
- template: {
99
- metadata: {
100
- annotations: {
101
- 'hammerkit.dev/version': (0, version_1.getVersion)(),
287
+ spec: {
288
+ template: {
289
+ metadata: {
290
+ annotations: {
291
+ 'hammerkit.dev/version': (0, version_1.getVersion)(),
292
+ },
293
+ labels,
102
294
  },
103
- labels: {
104
- 'hammerkit.dev/id': task.id(),
105
- 'hammerkit.dev/state': options.stateKey,
295
+ spec: {
296
+ ...getTaskJobContainers({
297
+ task,
298
+ cmd,
299
+ container,
300
+ envs,
301
+ secrets,
302
+ network,
303
+ secretSpec,
304
+ mounts: persistence.mounts.map((m) => m.mount),
305
+ kubernetes,
306
+ }),
307
+ volumes: [
308
+ ...persistence.volumes,
309
+ ...persistence.workspace,
310
+ ...secrets.volumes,
311
+ ...secretSpec.volumes,
312
+ ],
313
+ ...(workspace.affinity ? { affinity: workspace.affinity } : {}),
314
+ hostAliases: network.hostAliases,
315
+ restartPolicy: 'Never',
316
+ // on a server the pod is the build's: no token of the namespace, and the
317
+ // nodes of its pool
318
+ ...(placement
319
+ ? {
320
+ automountServiceAccountToken: false,
321
+ nodeSelector: placement.nodeSelector,
322
+ tolerations: placement.tolerations,
323
+ }
324
+ : {}),
106
325
  },
107
326
  },
108
- spec: {
109
- containers: [
110
- {
111
- image: task.data.image,
112
- // through the task's shell, as on Docker
113
- command: [task.data.shell, '-c'],
114
- args: [(0, task_arguments_1.getKubernetesCommandText)(cmd)],
115
- workingDir: cmd.cwd,
116
- env: [
117
- ...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
118
- ...secrets.env,
119
- ],
120
- name: `cmd-${++i}`,
121
- volumeMounts: [...persistence.mounts.map((m) => m.mount), ...secrets.mounts],
122
- ...(0, container_resources_1.getKubernetesResources)(task.data.resources),
123
- },
124
- ],
125
- volumes: [...persistence.volumes, ...secrets.volumes],
126
- hostAliases,
127
- restartPolicy: 'Never',
128
- },
327
+ backoffLimit: server ? 0 : 1,
328
+ parallelism: 1,
129
329
  },
130
- backoffLimit: 1,
131
- parallelism: 1,
132
- },
133
- };
134
- try {
135
- const pod = await (0, apply_1.apply)(instance, spec);
136
- if (!pod.status?.succeeded) {
137
- await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort);
138
- }
139
- task.status.write('debug', 'pod completed');
140
- if (i < task.data.cmds.length) {
141
- await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, podName, options.abort);
330
+ };
331
+ try {
332
+ const pod = await (0, apply_1.apply)(instance, spec);
333
+ if (!pod.status?.succeeded) {
334
+ try {
335
+ await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort);
336
+ }
337
+ catch (e) {
338
+ if (!(e instanceof abort_1.AbortError)) {
339
+ await writeJobLogs(instance, kubernetes, task, podName, container);
340
+ const failure = await describeFailedJob(instance, kubernetes, task.name, podName, container, inJob);
341
+ if (failure?.type === 'crash') {
342
+ throw new JobCrash(failure.exitCode);
343
+ }
344
+ throw new Error(failure?.message ?? (0, log_1.getErrorMessage)(e));
345
+ }
346
+ throw e;
347
+ }
348
+ }
349
+ await writeJobLogs(instance, kubernetes, task, podName, container);
350
+ task.status.write('debug', 'pod completed');
351
+ // its init containers are done with the credentials
352
+ await (0, job_secrets_1.removeJobCredentials)(instance, kubernetes, task);
353
+ if (i < task.data.cmds.length) {
354
+ await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, podName, options.abort);
355
+ }
142
356
  }
143
- }
144
- catch (e) {
145
- if (e instanceof abort_1.AbortError) {
146
- options.state.set({ stateKey: options.stateKey, type: 'canceled' });
357
+ catch (e) {
358
+ if (e instanceof abort_1.AbortError) {
359
+ // at once, so its policy can go (in finally) once the pod ended
360
+ await (0, await_running_state_1.stopJob)(instance, kubernetes, podName).catch((e) => task.status.write('error', `the job could not be stopped: ${(0, log_1.getErrorMessage)(e)}`));
361
+ await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
362
+ options.state.set({ stateKey: options.stateKey, type: 'canceled' });
363
+ return;
364
+ }
365
+ await (0, await_running_state_1.deleteJob)(instance, kubernetes, podName).catch(() => undefined);
366
+ options.state.set(e instanceof JobCrash
367
+ ? { stateKey: options.stateKey, type: 'crash', exitCode: e.exitCode }
368
+ : { stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
369
+ await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
147
370
  return;
148
371
  }
149
- await (0, await_running_state_1.deleteJob)(instance, kubernetes, podName).catch(() => undefined);
150
- options.state.set({
151
- stateKey: options.stateKey,
152
- type: 'error',
153
- errorMessage: (0, log_1.getErrorMessage)(e),
154
- });
155
- await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task).catch(() => undefined);
156
- return;
372
+ }
373
+ // the finished job stays for its state, the values it read need not
374
+ await removeTaskSecrets(instance, kubernetes, task);
375
+ if (kept) {
376
+ await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, options.stateKey);
377
+ }
378
+ }
379
+ finally {
380
+ network.close();
381
+ // once the run's pods ended: a cancelled job's pod is still terminating
382
+ if ((0, work_network_1.isLimitedNetwork)((0, work_network_1.getEffectiveNetwork)(task.data))) {
383
+ await (0, network_policy_1.releaseKubernetesItemNetwork)(instance, kubernetes, task, scope).catch((e) => task.status.write('error', `remove of network policy failed ${(0, log_1.getErrorMessage)(e)}`));
157
384
  }
158
385
  }
159
- // the finished job stays for its state, the values it read need not
160
- await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task);
161
386
  },
162
387
  async stop() {
163
388
  const jobs = await instance.batchApi.listNamespacedJob({
164
389
  namespace: kubernetes.namespace,
165
- labelSelector: `hammerkit.dev/id=${task.id()}`,
390
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
166
391
  });
167
- for (const pod of jobs.items) {
168
- if (pod.metadata?.name) {
169
- await instance.batchApi.deleteNamespacedJob({ name: pod.metadata.name, namespace: kubernetes.namespace });
392
+ for (const job of jobs.items) {
393
+ if (job.metadata?.name) {
394
+ await (0, await_running_state_1.stopJob)(instance, kubernetes, job.metadata.name);
170
395
  }
171
396
  }
172
397
  const deployments = await instance.appsApi.listNamespacedDeployment({
173
398
  namespace: kubernetes.namespace,
174
- labelSelector: `hammerkit.dev/id=${task.id()}`,
399
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
175
400
  });
176
401
  for (const deploy of deployments.items) {
177
402
  if (deploy.metadata?.name) {
@@ -183,23 +408,29 @@ function kubernetesTaskRuntime(task, kubernetes) {
183
408
  }
184
409
  const pods = await instance.coreApi.listNamespacedPod({
185
410
  namespace: kubernetes.namespace,
186
- labelSelector: `hammerkit.dev/id=${task.id()}`,
411
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
187
412
  });
188
413
  for (const pod of pods.items) {
189
414
  if (pod.metadata?.name) {
190
415
  await instance.coreApi.deleteNamespacedPod({ name: pod.metadata.name, namespace: kubernetes.namespace });
191
416
  }
192
417
  }
418
+ // the limits go last, once no pod of the task runs
419
+ await (0, network_policy_1.removeAllKubernetesItemNetworks)(instance, kubernetes, task);
193
420
  },
194
421
  async remove() {
195
422
  await this.stop();
196
423
  await (0, remove_persistent_data_1.removePersistentData)(instance, kubernetes, task);
197
- await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task);
424
+ await removeTaskSecrets(instance, kubernetes, task);
198
425
  },
199
426
  async currentStateKey() {
427
+ if (kept) {
428
+ // the server removes the Jobs of a run at its end, the claim says
429
+ return (0, kept_claims_1.useClaimState)(instance, kubernetes, claim);
430
+ }
200
431
  const jobs = await instance.batchApi.listNamespacedJob({
201
432
  namespace: kubernetes.namespace,
202
- labelSelector: `hammerkit.dev/id=${task.id()}`,
433
+ labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
203
434
  });
204
435
  const completedStates = jobs.items
205
436
  .filter((j) => j.status?.succeeded && j.metadata?.labels?.['hammerkit.dev/state'])
@@ -211,11 +442,11 @@ function kubernetesTaskRuntime(task, kubernetes) {
211
442
  // outputs live in the task's claim; without it the finished job left
212
443
  // nothing to reuse
213
444
  try {
214
- const claim = await instance.coreApi.readNamespacedPersistentVolumeClaim({
215
- name: (0, volumes_1.getVolumeName)(task),
445
+ const found = await instance.coreApi.readNamespacedPersistentVolumeClaim({
446
+ name: claim,
216
447
  namespace: kubernetes.namespace,
217
448
  });
218
- return claim.metadata?.deletionTimestamp ? null : stateKey;
449
+ return found.metadata?.deletionTimestamp ? null : stateKey;
219
450
  }
220
451
  catch (e) {
221
452
  if ((0, apply_1.statusCodeOf)(e) === 404) {
@@ -230,8 +461,29 @@ exports.kubernetesTaskRuntime = kubernetesTaskRuntime;
230
461
  function kubernetesServiceRuntime(service, kubernetes) {
231
462
  const instance = (0, kubernetes_instance_1.createKubernetesInstances)(kubernetes);
232
463
  return {
233
- initialize() {
234
- return Promise.resolve();
464
+ // A Deployment left by an earlier `up` is the running service, so `down`
465
+ // stops it and a run reuses it, as with a container on Docker. One whose
466
+ // definition changed goes, with its limits, and is created anew.
467
+ async initialize(state) {
468
+ const deployments = await instance.appsApi.listNamespacedDeployment({
469
+ namespace: kubernetes.namespace,
470
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
471
+ });
472
+ const deployment = deployments.items[0];
473
+ if (!deployment?.metadata?.name) {
474
+ return;
475
+ }
476
+ if (deployment.metadata.labels?.[ensure_kubernetes_deployment_exists_1.DEFINITION_LABEL] !== (0, service_definition_1.getServiceDefinitionHash)(service)) {
477
+ service.status.write('info', `${service.name} changed since it was started, recreating it`);
478
+ await this.stop();
479
+ return;
480
+ }
481
+ state.set({
482
+ type: 'running',
483
+ dns: { containerId: deployment.metadata.name },
484
+ stateKey: deployment.metadata.labels?.['hammerkit.dev/state'] ?? '',
485
+ remote: null,
486
+ });
235
487
  },
236
488
  async restore(environment, path) {
237
489
  await (0, restore_kubernetes_data_1.restoreKubernetesData)(service, kubernetes, instance, environment, path);
@@ -242,7 +494,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
242
494
  async stop() {
243
495
  const deployments = await instance.appsApi.listNamespacedDeployment({
244
496
  namespace: kubernetes.namespace,
245
- labelSelector: `hammerkit.dev/id=${service.id()}`,
497
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
246
498
  });
247
499
  for (const deployment of deployments.items) {
248
500
  if (deployment.metadata?.name) {
@@ -253,15 +505,22 @@ function kubernetesServiceRuntime(service, kubernetes) {
253
505
  }
254
506
  }
255
507
  await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, service);
508
+ // the limits go last, once the service's pods ended
509
+ await (0, network_policy_1.removeAllKubernetesItemNetworks)(instance, kubernetes, service);
256
510
  },
257
511
  async execute(environment, options) {
258
- const persistence = await (0, volumes_1.getKubernetesPersistence)(service);
512
+ const persistence = await (0, volumes_1.getKubernetesPersistence)(service, kubernetes);
259
513
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
260
514
  await (0, ensure_kubernetes_service_exists_1.ensureKubernetesServiceExists)(instance, kubernetes, service);
261
515
  await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence);
262
516
  await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, service, service.data.secrets, environment);
263
- await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, service.data.secrets), options.stateKey);
264
- const name = (0, resources_1.getResourceName)(service);
517
+ // on a server, the run's: its runs share the namespace, and the service goes with the run
518
+ const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, {
519
+ followLog: false,
520
+ run: kubernetes.server ? (0, job_placement_1.getRunLabelValue)(kubernetes.server.run) : null,
521
+ });
522
+ await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, kubernetes, service.data.secrets), options.stateKey, network);
523
+ const name = (0, resources_1.getResourceName)(service, kubernetes);
265
524
  options.state.set({
266
525
  type: 'running',
267
526
  dns: { containerId: name },
@@ -293,7 +552,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
293
552
  }
294
553
  const services = await instance.coreApi.listNamespacedService({
295
554
  namespace: kubernetes.namespace,
296
- labelSelector: `hammerkit.dev/id=${service.id()}`,
555
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
297
556
  });
298
557
  for (const service of services.items) {
299
558
  if (service.metadata?.name) {
@@ -309,7 +568,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
309
568
  async currentStateKey() {
310
569
  const deployments = await instance.appsApi.listNamespacedDeployment({
311
570
  namespace: kubernetes.namespace,
312
- labelSelector: `hammerkit.dev/id=${service.id()}`,
571
+ labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
313
572
  });
314
573
  const deployment = deployments.items[0];
315
574
  return deployment?.metadata?.labels?.['hammerkit.dev/state'] ?? null;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getLocalWorkRuntime = exports.getStateFilename = void 0;
7
7
  const tar_1 = require("tar");
8
+ const tar_2 = require("../utils/tar");
8
9
  const path_1 = require("path");
9
10
  const local_task_1 = require("../executer/local-task");
10
11
  const event_cache_1 = require("../executer/event-cache");
@@ -121,7 +122,7 @@ async function archiveLocal(environment, task, path) {
121
122
  for (const generatedArchive of (0, event_cache_1.getArchivePaths)(task.data, path)) {
122
123
  await environment.file.writeStream(generatedArchive.filename,
123
124
  // tar's stream is a Minipass stream; Readable.from adapts it to a Node one
124
- stream_1.Readable.from((0, tar_1.create)({ cwd: task.data.cwd, gzip: true }, [(0, path_1.relative)(task.data.cwd, generatedArchive.path)])));
125
+ stream_1.Readable.from((0, tar_2.packArchive)({ cwd: task.data.cwd, gzip: true }, [(0, path_1.relative)(task.data.cwd, generatedArchive.path)])));
125
126
  }
126
127
  }
127
128
  async function restoreLocal(environment, task, path) {