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.
- package/build.schema.json +188 -0
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +10 -3
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +4 -1
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +168 -17
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/await-running-state.js +59 -13
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +35 -12
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/main.js +2 -0
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +19 -1
- package/dist/planner/utils/append-work-task.js +18 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +124 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +31 -10
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +30 -7
- package/dist/remote/remote-work-commands.js +426 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +8 -1
- package/dist/schema/build-file-container-service-schema.js +12 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +120 -11
- package/dist/schema/schema-parser.js +92 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +739 -0
- package/dist/server/executor.js +57 -5
- package/dist/server/kubernetes-backend.js +367 -65
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +188 -103
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +245 -12
- package/dist/server/runs.js +410 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +255 -14
- package/dist/server/server.js +269 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/dist/utils/terminal-prompt.js +53 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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?.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
31
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
167
|
-
|
|
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}`);
|