hammerkit 1.27.0-next.3 → 1.27.0-next.30

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 (119) hide show
  1. package/build.schema.json +188 -0
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry-cache-backend.js +2 -2
  4. package/dist/cache/explain.js +17 -0
  5. package/dist/cli.js +2 -0
  6. package/dist/docker/item-network.js +13 -5
  7. package/dist/executer/deadline.js +17 -3
  8. package/dist/executer/deferred-restore.js +36 -0
  9. package/dist/executer/docker-service.js +30 -8
  10. package/dist/executer/docker-task.js +21 -1
  11. package/dist/executer/event-cache.js +20 -2
  12. package/dist/executer/execute-work-service.js +2 -0
  13. package/dist/executer/execute-work-task.js +10 -3
  14. package/dist/executer/execution-steps.js +25 -4
  15. package/dist/executer/reset-work-tree.js +4 -0
  16. package/dist/executer/scheduler/enqueue-next.js +57 -6
  17. package/dist/executer/scheduler/state-key.js +42 -5
  18. package/dist/executer/watch-loop.js +4 -1
  19. package/dist/harness/connect.js +20 -6
  20. package/dist/harness/mcp-command.js +22 -6
  21. package/dist/harness/mcp.js +63 -114
  22. package/dist/harness/opencode.js +158 -50
  23. package/dist/harness/pi.js +29 -0
  24. package/dist/harness/sandbox-client.js +168 -17
  25. package/dist/harness/service-tools.js +141 -0
  26. package/dist/harness/task-tools.js +221 -0
  27. package/dist/kubernetes/apply.js +27 -16
  28. package/dist/kubernetes/await-running-state.js +59 -13
  29. package/dist/kubernetes/claim-layout.js +71 -0
  30. package/dist/kubernetes/cluster-state.js +245 -0
  31. package/dist/kubernetes/ensure-http-route.js +1 -1
  32. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +35 -12
  33. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
  34. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  35. package/dist/kubernetes/ensure-persistent-data.js +30 -20
  36. package/dist/kubernetes/follow-pod-logs.js +182 -0
  37. package/dist/kubernetes/get-service-ip.js +2 -2
  38. package/dist/kubernetes/job-placement.js +36 -12
  39. package/dist/kubernetes/job-secrets.js +33 -19
  40. package/dist/kubernetes/kept-claims.js +22 -10
  41. package/dist/kubernetes/network-policy.js +85 -27
  42. package/dist/kubernetes/remove-persistent-data.js +3 -1
  43. package/dist/kubernetes/resources.js +25 -3
  44. package/dist/kubernetes/restore-kubernetes-data.js +9 -3
  45. package/dist/kubernetes/sandbox-runner.js +628 -0
  46. package/dist/kubernetes/secrets.js +7 -11
  47. package/dist/kubernetes/volumes.js +6 -3
  48. package/dist/main.js +2 -0
  49. package/dist/network/egress-proxy.js +45 -6
  50. package/dist/optimizer/calculate-checksum.js +3 -2
  51. package/dist/optimizer/check-key.js +94 -0
  52. package/dist/optimizer/check-keys.js +64 -0
  53. package/dist/optimizer/get-work-cache-stats.js +17 -3
  54. package/dist/optimizer/work-task-cache-description.js +51 -1
  55. package/dist/planner/utils/append-work-dependencies.js +10 -5
  56. package/dist/planner/utils/append-work-service.js +19 -1
  57. package/dist/planner/utils/append-work-task.js +18 -3
  58. package/dist/planner/utils/get-work-runtime.js +6 -1
  59. package/dist/planner/utils/normalize-path.js +19 -1
  60. package/dist/planner/utils/parse-work-mount.js +7 -1
  61. package/dist/planner/utils/parse-work-port.js +4 -1
  62. package/dist/planner/utils/task-arguments.js +3 -0
  63. package/dist/planner/validate.js +22 -0
  64. package/dist/planner/work-network.js +3 -1
  65. package/dist/planner/work-runtime-aggregate.js +32 -0
  66. package/dist/planner/work-runtime-docker.js +2 -1
  67. package/dist/planner/work-runtime-kubernetes.js +124 -48
  68. package/dist/planner/work-runtime-local.js +2 -1
  69. package/dist/planner/work-task.js +6 -1
  70. package/dist/program.js +31 -10
  71. package/dist/remote/device-login.js +7 -2
  72. package/dist/remote/host-config.js +69 -3
  73. package/dist/remote/remote-commands.js +30 -7
  74. package/dist/remote/remote-work-commands.js +426 -45
  75. package/dist/remote/server-client.js +15 -2
  76. package/dist/remote/token-source.js +64 -26
  77. package/dist/run-program.js +9 -0
  78. package/dist/schema/build-file-base-task-schema.js +8 -1
  79. package/dist/schema/build-file-container-service-schema.js +12 -0
  80. package/dist/schema/build-file-container-task-schema.js +16 -1
  81. package/dist/schema/build-file-include-schema.js +18 -4
  82. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  83. package/dist/schema/build-file-task-schema.js +5 -1
  84. package/dist/schema/capabilities-schema.js +56 -0
  85. package/dist/schema/reference-parser.js +120 -11
  86. package/dist/schema/schema-parser.js +92 -41
  87. package/dist/schema/work-scope-parser.js +33 -10
  88. package/dist/secrets/command-secret-provider.js +3 -1
  89. package/dist/secrets/github-app-token-command.js +3 -1
  90. package/dist/secrets/secret-catalog.js +43 -19
  91. package/dist/server/accounts-api.js +3 -1
  92. package/dist/server/approvals-api.js +1 -1
  93. package/dist/server/checks-api.js +83 -0
  94. package/dist/server/checks.js +116 -0
  95. package/dist/server/definitions-api.js +66 -3
  96. package/dist/server/endpoints.js +739 -0
  97. package/dist/server/executor.js +57 -5
  98. package/dist/server/kubernetes-backend.js +367 -65
  99. package/dist/server/kubernetes-sandbox-storage.js +48 -3
  100. package/dist/server/oauth.js +12 -1
  101. package/dist/server/plan.js +188 -103
  102. package/dist/server/policy.js +32 -2
  103. package/dist/server/repo-access.js +5 -0
  104. package/dist/server/run-plan.js +50 -19
  105. package/dist/server/run-request.js +58 -3
  106. package/dist/server/run-worker.js +245 -12
  107. package/dist/server/runs.js +410 -27
  108. package/dist/server/sandbox-pod.js +28 -0
  109. package/dist/server/sandbox-storage.js +23 -0
  110. package/dist/server/sandboxes-api.js +10 -4
  111. package/dist/server/sandboxes.js +8 -0
  112. package/dist/server/server-command.js +39 -20
  113. package/dist/server/server-config.js +255 -14
  114. package/dist/server/server.js +269 -15
  115. package/dist/server/service-runs.js +82 -0
  116. package/dist/utils/abort-on-signals.js +46 -6
  117. package/dist/utils/tar.js +36 -0
  118. package/dist/utils/terminal-prompt.js +53 -0
  119. package/package.json +1 -1
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readClusterState = exports.describeEvent = exports.describePod = exports.getPodWaiting = exports.describeLastExit = exports.getPodFailure = exports.CRASHES_TO_FAIL = void 0;
4
+ const log_1 = require("../log");
5
+ const apply_1 = require("./apply");
6
+ const job_placement_1 = require("./job-placement");
7
+ const network_gate_1 = require("./network-gate");
8
+ const resources_1 = require("./resources");
9
+ // what a pod's or event's message is cut to
10
+ const MAX_MESSAGE = 500;
11
+ // how many events of a run are said, the newest
12
+ const MAX_EVENTS = 20;
13
+ // events the result of a task says better: a Job of a failed command
14
+ const SAID_ELSEWHERE = ['BackoffLimitExceeded'];
15
+ // A container restarted this often (ended three times) is taken as one that keeps crashing,
16
+ // unless its service sets `restarts`: once may be a race with a dependency or a slow first start
17
+ exports.CRASHES_TO_FAIL = 2;
18
+ // what a pull error says when the image is not there to pull, or not for this cluster:
19
+ // waiting does not change it. Others (a registry that limits, a token not there yet) may pass.
20
+ const IMAGE_GONE = /not found|manifest unknown|unauthorized|denied|forbidden|invalid reference|no such host/i;
21
+ /**
22
+ * Why a pod of a service will not become ready without a change, as the cluster says it:
23
+ * an image that can not be pulled, a container that keeps crashing or can not be created,
24
+ * an init container that failed. Null while it may still get there. A container fails once
25
+ * it was restarted `restarts` times and crashed again.
26
+ *
27
+ * Only what the cluster wrote is quoted: the termination message of a container is its
28
+ * own output (and may hold a value it read), except for the network gate's, which says why
29
+ * the policy did not take hold.
30
+ */
31
+ function getPodFailure(pod, container, restarts = exports.CRASHES_TO_FAIL) {
32
+ for (const status of pod.status?.initContainerStatuses ?? []) {
33
+ const terminated = status.state?.terminated ?? status.lastState?.terminated;
34
+ if (!terminated || terminated.exitCode === 0) {
35
+ continue;
36
+ }
37
+ if (status.name === network_gate_1.NETWORK_GATE_CONTAINER) {
38
+ return {
39
+ text: `the network policy did not take hold: ${cut(terminated.message?.trim() || 'the gate failed')}`,
40
+ final: true,
41
+ };
42
+ }
43
+ if (status.restartCount >= exports.CRASHES_TO_FAIL) {
44
+ return { text: `its init container ${status.name} failed with exit code ${terminated.exitCode}`, final: true };
45
+ }
46
+ }
47
+ for (const status of mainStatuses(pod, container)) {
48
+ const failure = getContainerFailure(status, restartsEnded(pod, status), restarts);
49
+ if (failure) {
50
+ return failure;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ exports.getPodFailure = getPodFailure;
56
+ function getContainerFailure(status, restartsEnded, restarts) {
57
+ const waiting = status.state?.waiting;
58
+ const reason = waiting?.reason;
59
+ const said = waiting?.message ? `: ${cut(waiting.message)}` : '';
60
+ switch (reason) {
61
+ // the first pull: final only when the registry said the image is not there, as older
62
+ // kubelets leave that out of the ImagePullBackOff that follows
63
+ case 'ErrImagePull':
64
+ return IMAGE_GONE.test(waiting?.message ?? '')
65
+ ? { text: `its image ${status.image} could not be pulled (${reason})${said}`, final: true }
66
+ : null;
67
+ case 'ImagePullBackOff':
68
+ return {
69
+ text: `its image ${status.image} could not be pulled (${reason})${said}`,
70
+ final: IMAGE_GONE.test(waiting?.message ?? ''),
71
+ };
72
+ case 'ErrImageNeverPull':
73
+ case 'InvalidImageName':
74
+ return { text: `its image ${status.image} could not be pulled (${reason})${said}`, final: true };
75
+ case 'CreateContainerConfigError':
76
+ case 'CreateContainerError':
77
+ case 'RunContainerError':
78
+ return { text: `its container could not be started (${reason})${said}`, final: false };
79
+ case 'CrashLoopBackOff':
80
+ return status.restartCount >= restarts
81
+ ? { text: `it keeps crashing: ${describeLastExit(status)}`, final: true }
82
+ : null;
83
+ }
84
+ // a kubelet may leave an instance that ended as it is until its back-off is over, and say
85
+ // CrashLoopBackOff only when it looks at the pod again, a minute later: the same crash
86
+ if (restartsEnded && status.state?.terminated && status.restartCount >= restarts) {
87
+ return { text: `it keeps crashing: ${describeLastExit(status)}`, final: true };
88
+ }
89
+ return null;
90
+ }
91
+ // How the last instance of a container ended, as the cluster says it.
92
+ function describeLastExit(status) {
93
+ // the instance that ended now, else the one before the one that waits or runs
94
+ const last = status.state?.terminated ?? status.lastState?.terminated;
95
+ const restarts = `, restarted ${status.restartCount} time${status.restartCount === 1 ? '' : 's'}`;
96
+ if (!last) {
97
+ return `it ended${restarts}`;
98
+ }
99
+ if (last.reason === 'OOMKilled') {
100
+ return `it was killed for using more memory than its limit (OOMKilled)${restarts}`;
101
+ }
102
+ // the kubelet kills a container whose liveness probe (its healthcheck) keeps failing
103
+ const killed = last.exitCode === 137 || last.exitCode === 143 ? ', killed as when its healthcheck kept failing' : '';
104
+ return `it exited with code ${last.exitCode}${last.reason ? ` (${last.reason})` : ''}${killed}${restarts}`;
105
+ }
106
+ exports.describeLastExit = describeLastExit;
107
+ /**
108
+ * What a pod of a service waits for, in a few words, while it is not ready: a node, its
109
+ * image, its container. Undefined when it waits for nothing the cluster names.
110
+ */
111
+ function getPodWaiting(pod, container) {
112
+ const scheduled = pod.status?.conditions?.find((condition) => condition.type === 'PodScheduled');
113
+ if (scheduled?.status === 'False') {
114
+ return `waits for a node: ${cut(scheduled.message ?? scheduled.reason ?? 'not scheduled yet')}`;
115
+ }
116
+ for (const status of mainStatuses(pod, container)) {
117
+ const waiting = status.state?.waiting;
118
+ const said = waiting?.message ? `: ${cut(waiting.message)}` : '';
119
+ switch (waiting?.reason) {
120
+ case 'ErrImagePull':
121
+ case 'ImagePullBackOff':
122
+ return `can not pull its image ${status.image} yet${said}`;
123
+ case 'CreateContainerConfigError':
124
+ case 'CreateContainerError':
125
+ case 'RunContainerError':
126
+ return `can not start its container yet (${waiting.reason})${said}`;
127
+ case 'CrashLoopBackOff':
128
+ return `crashed and starts again: ${describeLastExit(status)}`;
129
+ }
130
+ const ended = status.state?.terminated;
131
+ if (ended && restartsEnded(pod, status)) {
132
+ return `${ended.exitCode === 0 ? 'ended' : 'crashed'} and starts again: ${describeLastExit(status)}`;
133
+ }
134
+ if (status.restartCount > 0 && status.state?.running && !status.ready) {
135
+ return `restarted: ${describeLastExit(status)}`;
136
+ }
137
+ }
138
+ return undefined;
139
+ }
140
+ exports.getPodWaiting = getPodWaiting;
141
+ // Whether the kubelet starts a container that ended again: not one of a pod that finished (a
142
+ // task's Job, an evicted pod, which its ReplicaSet replaces) or that never restarts.
143
+ function restartsEnded(pod, status) {
144
+ const phase = pod.status?.phase;
145
+ if (phase === 'Succeeded' || phase === 'Failed') {
146
+ return false;
147
+ }
148
+ const policy = pod.spec?.restartPolicy ?? 'Always';
149
+ return policy === 'Always' || (policy === 'OnFailure' && status.state?.terminated?.exitCode !== 0);
150
+ }
151
+ function mainStatuses(pod, container) {
152
+ return (pod.status?.containerStatuses ?? []).filter((status) => !container || status.name === container);
153
+ }
154
+ // One line of a pod that is not as it should be, or null for one that runs, ready, or ended well.
155
+ function describePod(pod) {
156
+ const name = pod.metadata?.name ?? '?';
157
+ const phase = pod.status?.phase ?? 'Unknown';
158
+ const failure = getPodFailure(pod)?.text;
159
+ const waiting = getPodWaiting(pod);
160
+ const unready = (pod.status?.containerStatuses ?? [])
161
+ .filter((status) => !status.ready && !status.state?.terminated)
162
+ .map((status) => status.name);
163
+ const crashed = (pod.status?.containerStatuses ?? []).filter((status) => (status.state?.terminated && status.state.terminated.exitCode !== 0) || status.restartCount > 0);
164
+ if (!failure && !waiting && crashed.length === 0 && (phase === 'Succeeded' || unready.length === 0)) {
165
+ return null;
166
+ }
167
+ const said = failure ?? waiting;
168
+ const parts = [phase, ...(said ? [said] : [])];
169
+ for (const status of crashed) {
170
+ if (!said?.includes(describeLastExit(status))) {
171
+ parts.push(`${status.name}: ${describeLastExit(status)}`);
172
+ }
173
+ }
174
+ if (!said && unready.length > 0) {
175
+ parts.push(`not ready: ${unready.join(', ')}`);
176
+ }
177
+ return `pod ${name}: ${parts.join(', ')}`;
178
+ }
179
+ exports.describePod = describePod;
180
+ /**
181
+ * The message of a warning event, as far as it is the cluster's: a failed probe says the
182
+ * output of the command, which is the service's, and is left out.
183
+ */
184
+ function describeEvent(event) {
185
+ const object = event.involvedObject;
186
+ const message = (event.message ?? '').replace(/^((Readiness|Liveness|Startup) probe (failed|errored)).*$/s, '$1');
187
+ const count = (event.count ?? 1) > 1 ? ` (${event.count} times)` : '';
188
+ return `${event.reason ?? 'Warning'} ${object.kind?.toLowerCase() ?? 'object'}/${object.name ?? '?'}: ${cut(message.trim())}${count}`;
189
+ }
190
+ exports.describeEvent = describeEvent;
191
+ /**
192
+ * What the cluster said of a run that failed, into its log before the server removes its
193
+ * objects: its pods that are not as they should be, and the warning events of its objects.
194
+ * In the namespace of a service run everything is the run's; in a caller's namespace, which
195
+ * runs share, only the objects of the run's items (named by their key) are.
196
+ */
197
+ async function readClusterState(instance, kubernetes, workTree, ownNamespace) {
198
+ const namespace = kubernetes.namespace;
199
+ const keys = [...Object.values(workTree.tasks), ...Object.values(workTree.services)].map((item) => (0, resources_1.getItemKey)(item, kubernetes));
200
+ const ours = (name) => ownNamespace || keys.some((key) => name?.includes(key));
201
+ const lines = [];
202
+ try {
203
+ const labels = (0, job_placement_1.getRunLabels)(kubernetes);
204
+ const labelSelector = Object.entries(labels)
205
+ .map(([key, value]) => `${key}=${value}`)
206
+ .join(',');
207
+ const pods = await instance.coreApi.listNamespacedPod({
208
+ namespace,
209
+ ...(labelSelector && !ownNamespace ? { labelSelector } : {}),
210
+ });
211
+ for (const pod of pods.items) {
212
+ const line = ours(pod.metadata?.name) ? describePod(pod) : null;
213
+ if (line) {
214
+ lines.push(line);
215
+ }
216
+ }
217
+ }
218
+ catch (e) {
219
+ lines.push(`its pods could not be read: ${(0, log_1.getErrorMessage)(e)}`);
220
+ }
221
+ try {
222
+ const events = await instance.coreApi.listNamespacedEvent({ namespace, fieldSelector: 'type=Warning' });
223
+ const warnings = events.items
224
+ .filter((event) => ours(event.involvedObject.name) && !SAID_ELSEWHERE.includes(event.reason ?? ''))
225
+ .sort((a, b) => eventTime(a) - eventTime(b))
226
+ .slice(-MAX_EVENTS);
227
+ lines.push(...warnings.map((event) => `event ${describeEvent(event)}`));
228
+ }
229
+ catch (e) {
230
+ lines.push((0, apply_1.statusCodeOf)(e) === 403
231
+ ? `its events could not be read: the runRole of the server needs list on events (see the backend docs)`
232
+ : `its events could not be read: ${(0, log_1.getErrorMessage)(e)}`);
233
+ }
234
+ return lines;
235
+ }
236
+ exports.readClusterState = readClusterState;
237
+ function eventTime(event) {
238
+ const time = event.lastTimestamp ?? event.eventTime ?? event.firstTimestamp ?? event.metadata?.creationTimestamp;
239
+ return time ? new Date(time).getTime() : 0;
240
+ }
241
+ function cut(text) {
242
+ const line = text.replace(/\s+/g, ' ');
243
+ return line.length > MAX_MESSAGE ? `${line.slice(0, MAX_MESSAGE)}…` : line;
244
+ }
245
+ //# sourceMappingURL=cluster-state.js.map
@@ -36,7 +36,7 @@ async function ensureHttpRoute(instance, env, route, service) {
36
36
  ],
37
37
  backendRefs: [
38
38
  {
39
- name: (0, resources_1.getResourceName)(service),
39
+ name: (0, resources_1.getResourceName)(service, env),
40
40
  port: route.servicePort ?? service.data.ports[0].containerPort,
41
41
  },
42
42
  ],
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensureKubernetesDeploymentExists = exports.DEFINITION_LABEL = void 0;
3
+ exports.getServiceContainerName = exports.MIN_READY_SECONDS = exports.ensureKubernetesDeploymentExists = exports.DEFINITION_LABEL = void 0;
4
4
  const service_definition_1 = require("../planner/service-definition");
5
5
  const work_service_1 = require("../planner/work-service");
6
6
  const volumes_1 = require("./volumes");
@@ -17,11 +17,13 @@ const network_policy_1 = require("./network-policy");
17
17
  const egress_proxy_1 = require("../network/egress-proxy");
18
18
  // what the service was created from, to tell a changed one
19
19
  exports.DEFINITION_LABEL = 'hammerkit.dev/definition';
20
+ // how often a starting container is checked: as the liveness probe
21
+ const STARTUP_PERIOD_SECONDS = 5;
20
22
  async function ensureKubernetesDeploymentExists(instance, env, service, persistence, secrets, stateKey,
21
23
  // the needed services by name, and the proxy of a limited network
22
24
  network) {
23
25
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
24
- const name = (0, resources_1.getResourceName)(service);
26
+ const name = (0, resources_1.getResourceName)(service, env);
25
27
  const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
26
28
  const serviceCommand = (0, work_service_1.getServiceCommand)(service.data, { kubernetes: true });
27
29
  const probe = healthcheckCommand
@@ -34,6 +36,16 @@ network) {
34
36
  failureThreshold: 3,
35
37
  }
36
38
  : undefined;
39
+ // the healthcheck's `timeout` for each start: until it passed once, the liveness probe
40
+ // doesn't kill a container that starts slowly, and the wait fails it with the timeout
41
+ const startupProbe = probe && service.data.healthcheck
42
+ ? {
43
+ ...probe,
44
+ // a check more than the timeout holds, and one more for the first check's offset: the
45
+ // wait fails it with the timeout before the kubelet restarts it
46
+ failureThreshold: Math.ceil(service.data.healthcheck.timeout / (STARTUP_PERIOD_SECONDS * 1000)) + 2,
47
+ }
48
+ : undefined;
37
49
  const server = env.server;
38
50
  const placement = server ? (0, job_placement_1.getPodPlacement)(server, undefined) : null;
39
51
  const workspace = (0, volumes_1.getWorkspacePodSpec)(env, persistence);
@@ -47,25 +59,24 @@ network) {
47
59
  'hammerkit.dev/version': (0, version_1.getVersion)(),
48
60
  },
49
61
  labels: {
50
- 'hammerkit.dev/id': service.id(),
62
+ ...(0, resources_1.getItemLabels)(service, env),
51
63
  'hammerkit.dev/state': stateKey,
52
- ...(0, job_placement_1.getRunLabels)(env),
53
64
  [exports.DEFINITION_LABEL]: (0, service_definition_1.getServiceDefinitionHash)(service),
54
65
  },
55
66
  },
56
67
  spec: {
57
68
  selector: {
58
- matchLabels: {
59
- 'hammerkit.dev/id': service.id(),
60
- },
69
+ matchLabels: (0, resources_1.getItemLabels)(service, env),
61
70
  },
62
71
  replicas: 1,
72
+ // without a healthcheck a pod is ready as soon as its container runs, also one that
73
+ // exits a second later: it counts once it ran this long without a restart
74
+ minReadySeconds: probe ? 0 : exports.MIN_READY_SECONDS,
63
75
  template: {
64
76
  metadata: {
65
77
  labels: {
66
- 'hammerkit.dev/id': service.id(),
78
+ ...(0, resources_1.getItemLabels)(service, env),
67
79
  'hammerkit.dev/state': stateKey,
68
- ...(0, job_placement_1.getRunLabels)(env),
69
80
  ...workspace.labels,
70
81
  ...(0, network_policy_1.getNetworkLabels)(env, service),
71
82
  },
@@ -87,7 +98,7 @@ network) {
87
98
  : {}),
88
99
  containers: [
89
100
  {
90
- name: service.name.replace(/:/, '-'),
101
+ name: getServiceContainerName(service),
91
102
  image: service.data.image,
92
103
  workingDir: (0, work_service_1.getServiceWorkingDir)(service.data),
93
104
  env: [
@@ -104,6 +115,7 @@ network) {
104
115
  })),
105
116
  readinessProbe: probe,
106
117
  livenessProbe: probe,
118
+ startupProbe,
107
119
  ...(0, container_resources_1.getKubernetesResources)(service.data.resources),
108
120
  volumeMounts: [...persistence.mounts.map((m) => m.mount), ...secrets.mounts],
109
121
  ...(0, job_placement_1.getContainerSecurity)(env),
@@ -123,18 +135,29 @@ network) {
123
135
  },
124
136
  };
125
137
  const deploy = await (0, apply_1.apply)(instance, deployment);
126
- if (deploy.status?.readyReplicas ?? 0 > 0) {
138
+ if ((deploy.status?.availableReplicas ?? 0) > 0) {
127
139
  return;
128
140
  }
129
141
  await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
130
- podSelector: `hammerkit.dev/id=${service.id()}`,
142
+ podSelector: (0, resources_1.getItemSelector)(service, env),
131
143
  timeout: service.data.healthcheck?.timeout ?? null,
132
144
  timeoutMessage: service.data.healthcheck
133
145
  ? (0, work_healthcheck_1.getHealthcheckTimeoutMessage)(service.name, service.data.healthcheck)
134
146
  : '',
147
+ container: getServiceContainerName(service),
148
+ subject: `service ${service.name}`,
149
+ onWaiting: (text) => service.status.write('warn', `${service.name} ${text}`),
150
+ ...(service.data.restarts !== null ? { restarts: service.data.restarts } : {}),
135
151
  });
136
152
  }
137
153
  exports.ensureKubernetesDeploymentExists = ensureKubernetesDeploymentExists;
154
+ // how long the container of a service without a healthcheck has to run before it counts as up
155
+ exports.MIN_READY_SECONDS = 10;
156
+ // The container of a service's pod that runs its command, beside the `debug` one.
157
+ function getServiceContainerName(service) {
158
+ return service.name.replace(/:/, '-');
159
+ }
160
+ exports.getServiceContainerName = getServiceContainerName;
138
161
  // Kubernetes `command` replaces the image entrypoint and `args` its command.
139
162
  // A shell service sets both; an exec-form `cmd` keeps the established mapping
140
163
  // (first token as `command`, the rest as `args`).
@@ -65,7 +65,7 @@ async function ensureKubernetesPersistentVolumeClaimExists(instance, env, volume
65
65
  },
66
66
  },
67
67
  };
68
- service.status.console('stdout', `ensure volume ${volume.name}`);
68
+ service.status.write('debug', `ensure volume ${volume.name}`);
69
69
  // Don't block on the PVC reaching "Bound". Storage classes with
70
70
  // volumeBindingMode WaitForFirstConsumer (docker-desktop's "standard",
71
71
  // k3d/k3s local-path, most cloud defaults) keep the claim Pending until a pod
@@ -4,11 +4,10 @@ exports.ensureKubernetesServiceExists = void 0;
4
4
  const apply_1 = require("./apply");
5
5
  const resources_1 = require("./resources");
6
6
  const version_1 = require("../version");
7
- const job_placement_1 = require("./job-placement");
8
7
  // A service without ports gets a headless Service: a ClusterIP one needs a port,
9
8
  // and its tasks reach every port of the pod, as on docker.
10
9
  async function ensureKubernetesServiceExists(instance, env, service) {
11
- const name = (0, resources_1.getResourceName)(service);
10
+ const name = (0, resources_1.getResourceName)(service, env);
12
11
  const headless = service.data.ports.length === 0;
13
12
  const svc = {
14
13
  kind: 'Service',
@@ -19,21 +18,19 @@ async function ensureKubernetesServiceExists(instance, env, service) {
19
18
  annotations: {
20
19
  'hammerkit.dev/version': (0, version_1.getVersion)(),
21
20
  },
22
- labels: {
23
- 'hammerkit.dev/id': service.id(),
24
- ...(0, job_placement_1.getRunLabels)(env),
25
- },
21
+ labels: (0, resources_1.getItemLabels)(service, env),
26
22
  },
27
23
  spec: {
28
24
  type: 'ClusterIP',
29
25
  ...(headless ? { clusterIP: 'None' } : {}),
30
- selector: {
31
- 'hammerkit.dev/id': service.id(),
32
- },
26
+ // the pods of the service of this run, on a server where runs share the namespace
27
+ selector: (0, resources_1.getItemLabels)(service, env),
33
28
  ...(headless
34
29
  ? {}
35
30
  : {
31
+ // a Service of more than one port needs a name for each
36
32
  ports: service.data.ports.map((port) => ({
33
+ name: `tcp-${port.hostPort ?? port.containerPort}`,
37
34
  port: port.hostPort ?? port.containerPort,
38
35
  targetPort: port.containerPort,
39
36
  })),
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensurePersistentData = exports.clearClaim = exports.getPodForPersistence = void 0;
3
+ exports.ensurePersistentData = exports.packSource = exports.clearClaim = exports.getPodForPersistence = void 0;
4
4
  const apply_1 = require("./apply");
5
5
  const path_1 = require("path");
6
- const tar_1 = require("tar");
6
+ const tar_1 = require("../utils/tar");
7
7
  const resources_1 = require("./resources");
8
8
  const await_running_state_1 = require("./await-running-state");
9
9
  const sleep_1 = require("../utils/sleep");
@@ -67,17 +67,14 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
67
67
  for (const volume of persistence.volumes) {
68
68
  await (0, ensure_kubernetes_persistent_volume_claim_exists_1.ensureKubernetesPersistentVolumeClaimExists)(instance, env, volume, service, persistence.sizes[volume.name], persistence.kept.includes(volume.name));
69
69
  }
70
- const name = (0, resources_1.getResourceName)(service, type === 'write' ? '-upload' : '-download');
70
+ const name = (0, resources_1.getResourceName)(service, env, type === 'write' ? '-upload' : '-download');
71
71
  const podSpec = {
72
72
  kind: 'Pod',
73
73
  apiVersion: 'v1',
74
74
  metadata: {
75
75
  namespace: env.namespace,
76
76
  name,
77
- labels: {
78
- 'hammerkit.dev/id': service.id(),
79
- ...(0, job_placement_1.getRunLabels)(env),
80
- },
77
+ labels: (0, resources_1.getItemLabels)(service, env),
81
78
  annotations: {
82
79
  'hammerkit.dev/version': (0, version_1.getVersion)(),
83
80
  },
@@ -104,18 +101,18 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
104
101
  volumes: persistence.volumes,
105
102
  },
106
103
  };
107
- service.status.console('stdout', `create pod ${name}`);
104
+ service.status.write('debug', `create pod ${name}`);
108
105
  await removeStalePod(instance, env.namespace, name);
109
106
  const pod = await (0, apply_1.apply)(instance, podSpec);
110
107
  if (pod.status?.phase !== 'Running') {
111
- service.status.console('stdout', `await running state ${name}`);
108
+ service.status.write('debug', `await running state ${name}`);
112
109
  await (0, await_running_state_1.awaitRunningState)(instance, env, name, 'Running');
113
110
  }
114
111
  try {
115
112
  await fn(name);
116
113
  }
117
114
  finally {
118
- service.status.console('stdout', `delete pod ${name}`);
115
+ service.status.write('debug', `delete pod ${name}`);
119
116
  await instance.coreApi.deleteNamespacedPod({ name, namespace: env.namespace });
120
117
  }
121
118
  }
@@ -124,7 +121,7 @@ exports.getPodForPersistence = getPodForPersistence;
124
121
  // the task runs on it or outputs are restored into it, so it holds what a new one would
125
122
  async function clearClaim(instance, env, work, pod) {
126
123
  const targetPath = `/dev/hammerkit/${work.id()}`;
127
- work.status.console('stdout', `clear what an earlier run left in ${targetPath}`);
124
+ work.status.write('info', `clear what an earlier run left in ${targetPath}`);
128
125
  let failure = null;
129
126
  const res = await instance.exec.exec(env.namespace, pod, 'volume', ['find', targetPath, '-mindepth', '1', '-maxdepth', '1', '-exec', 'rm', '-rf', '{}', '+'], process.stdout, process.stderr, null, false, (status) => {
130
127
  if (status.status !== 'Success') {
@@ -137,6 +134,25 @@ async function clearClaim(instance, env, work, pod) {
137
134
  });
138
135
  }
139
136
  exports.clearClaim = clearClaim;
137
+ // The archive that uploads a source into the claim: its file or directory, at its place in
138
+ // the claim (see getClaimSubPath) instead of under its own name
139
+ function packSource(source) {
140
+ const fileItem = (0, path_1.basename)(source.localPath);
141
+ return stream_1.Readable.from((0, tar_1.packArchive)({
142
+ cwd: (0, path_1.dirname)(source.localPath),
143
+ filter(path) {
144
+ return source.matcher(path, source.localPath);
145
+ },
146
+ onWriteEntry(entry) {
147
+ entry.path = source.subPath + entry.path.slice(fileItem.length);
148
+ // the second of two hard-linked files links to the first by its path in the archive
149
+ if (entry.type === 'Link' && entry.linkpath?.startsWith(fileItem)) {
150
+ entry.linkpath = source.subPath + entry.linkpath.slice(fileItem.length);
151
+ }
152
+ },
153
+ }, [fileItem]));
154
+ }
155
+ exports.packSource = packSource;
140
156
  async function ensurePersistentData(instance, env, environment, service, persistence,
141
157
  // the kept claim of the work holds what an earlier run left, which goes first
142
158
  options = {}) {
@@ -153,7 +169,7 @@ options = {}) {
153
169
  if (persistence.sources.length === 0 && !options.reset) {
154
170
  return;
155
171
  }
156
- service.status.console('stdout', 'start upload container');
172
+ service.status.write('debug', 'start upload container');
157
173
  await getPodForPersistence(instance, env, service, persistence, 'write', async (name) => {
158
174
  if (options.reset) {
159
175
  await clearClaim(instance, env, service, name);
@@ -163,14 +179,8 @@ options = {}) {
163
179
  if (!(await environment.file.exists(source.localPath))) {
164
180
  continue;
165
181
  }
166
- const fileItem = (0, path_1.basename)(source.localPath);
167
- const data = stream_1.Readable.from((0, tar_1.create)({
168
- cwd: (0, path_1.dirname)(source.localPath),
169
- filter(path) {
170
- return source.matcher(path, source.localPath);
171
- },
172
- }, [fileItem]));
173
- service.status.console('stdout', `upload ${source.localPath}`);
182
+ const data = packSource(source);
183
+ service.status.write('info', `upload ${source.localPath}`);
174
184
  const targetPath = `/dev/hammerkit/${service.id()}`;
175
185
  const res = await instance.exec.exec(env.namespace, name, 'volume', ['tar', 'xf', '-', '-C', targetPath], process.stdout, process.stderr, data, false, (status) => {
176
186
  service.status.write('debug', `upload ${source.localPath} to ${name}: ${status}`);