hammerkit 1.27.0-next.8 → 2.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.schema.json +176 -6
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/cache-sync.js +1 -1
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/archive-task-entry.js +4 -2
- 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/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +11 -4
- 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 +6 -3
- 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 +194 -23
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +76 -17
- package/dist/kubernetes/await-running-state.js +172 -13
- 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 +138 -18
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +212 -10
- 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 +9 -6
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +16 -8
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +96 -11
- package/dist/main.js +2 -0
- 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 +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -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/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-kubernetes.js +153 -26
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -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 +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -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 +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -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 +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +478 -70
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +969 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
|
@@ -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,9 +1,10 @@
|
|
|
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.getPodTemplateHash = exports.POD_ANNOTATION = exports.ROLLOUT_DEADLINE_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");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
7
8
|
const service_hostname_1 = require("../planner/utils/service-hostname");
|
|
8
9
|
const apply_1 = require("./apply");
|
|
9
10
|
const version_1 = require("../version");
|
|
@@ -15,13 +16,19 @@ const container_resources_1 = require("./container-resources");
|
|
|
15
16
|
const job_placement_1 = require("./job-placement");
|
|
16
17
|
const network_policy_1 = require("./network-policy");
|
|
17
18
|
const egress_proxy_1 = require("../network/egress-proxy");
|
|
19
|
+
const service_rollouts_1 = require("./service-rollouts");
|
|
18
20
|
// what the service was created from, to tell a changed one
|
|
19
21
|
exports.DEFINITION_LABEL = 'hammerkit.dev/definition';
|
|
22
|
+
// how often a starting container is checked: as the liveness probe
|
|
23
|
+
const STARTUP_PERIOD_SECONDS = 5;
|
|
20
24
|
async function ensureKubernetesDeploymentExists(instance, env, service, persistence, secrets, stateKey,
|
|
21
25
|
// the needed services by name, and the proxy of a limited network
|
|
22
|
-
network
|
|
26
|
+
network,
|
|
27
|
+
// a service run (env.server.serviceRun): the Deployment the service has now, put back when
|
|
28
|
+
// the new one does not get ready, and what stops the wait for it
|
|
29
|
+
rollout = {}) {
|
|
23
30
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
|
|
24
|
-
const name = (0, resources_1.getResourceName)(service);
|
|
31
|
+
const name = (0, resources_1.getResourceName)(service, env);
|
|
25
32
|
const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
|
|
26
33
|
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data, { kubernetes: true });
|
|
27
34
|
const probe = healthcheckCommand
|
|
@@ -34,6 +41,16 @@ network) {
|
|
|
34
41
|
failureThreshold: 3,
|
|
35
42
|
}
|
|
36
43
|
: undefined;
|
|
44
|
+
// the healthcheck's `timeout` for each start: until it passed once, the liveness probe
|
|
45
|
+
// doesn't kill a container that starts slowly, and the wait fails it with the timeout
|
|
46
|
+
const startupProbe = probe && service.data.healthcheck
|
|
47
|
+
? {
|
|
48
|
+
...probe,
|
|
49
|
+
// a check more than the timeout holds, and one more for the first check's offset: the
|
|
50
|
+
// wait fails it with the timeout before the kubelet restarts it
|
|
51
|
+
failureThreshold: Math.ceil(service.data.healthcheck.timeout / (STARTUP_PERIOD_SECONDS * 1000)) + 2,
|
|
52
|
+
}
|
|
53
|
+
: undefined;
|
|
37
54
|
const server = env.server;
|
|
38
55
|
const placement = server ? (0, job_placement_1.getPodPlacement)(server, undefined) : null;
|
|
39
56
|
const workspace = (0, volumes_1.getWorkspacePodSpec)(env, persistence);
|
|
@@ -47,25 +64,24 @@ network) {
|
|
|
47
64
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
48
65
|
},
|
|
49
66
|
labels: {
|
|
50
|
-
|
|
67
|
+
...(0, resources_1.getItemLabels)(service, env),
|
|
51
68
|
'hammerkit.dev/state': stateKey,
|
|
52
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
53
69
|
[exports.DEFINITION_LABEL]: (0, service_definition_1.getServiceDefinitionHash)(service),
|
|
54
70
|
},
|
|
55
71
|
},
|
|
56
72
|
spec: {
|
|
57
73
|
selector: {
|
|
58
|
-
matchLabels:
|
|
59
|
-
'hammerkit.dev/id': service.id(),
|
|
60
|
-
},
|
|
74
|
+
matchLabels: (0, resources_1.getItemLabels)(service, env),
|
|
61
75
|
},
|
|
62
76
|
replicas: 1,
|
|
77
|
+
// without a healthcheck a pod is ready as soon as its container runs, also one that
|
|
78
|
+
// exits a second later: it counts once it ran this long without a restart
|
|
79
|
+
minReadySeconds: probe ? 0 : exports.MIN_READY_SECONDS,
|
|
63
80
|
template: {
|
|
64
81
|
metadata: {
|
|
65
82
|
labels: {
|
|
66
|
-
|
|
83
|
+
...(0, resources_1.getItemLabels)(service, env),
|
|
67
84
|
'hammerkit.dev/state': stateKey,
|
|
68
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
69
85
|
...workspace.labels,
|
|
70
86
|
...(0, network_policy_1.getNetworkLabels)(env, service),
|
|
71
87
|
},
|
|
@@ -87,7 +103,7 @@ network) {
|
|
|
87
103
|
: {}),
|
|
88
104
|
containers: [
|
|
89
105
|
{
|
|
90
|
-
name: service
|
|
106
|
+
name: getServiceContainerName(service),
|
|
91
107
|
image: service.data.image,
|
|
92
108
|
workingDir: (0, work_service_1.getServiceWorkingDir)(service.data),
|
|
93
109
|
env: [
|
|
@@ -104,6 +120,7 @@ network) {
|
|
|
104
120
|
})),
|
|
105
121
|
readinessProbe: probe,
|
|
106
122
|
livenessProbe: probe,
|
|
123
|
+
startupProbe,
|
|
107
124
|
...(0, container_resources_1.getKubernetesResources)(service.data.resources),
|
|
108
125
|
volumeMounts: [...persistence.mounts.map((m) => m.mount), ...secrets.mounts],
|
|
109
126
|
...(0, job_placement_1.getContainerSecurity)(env),
|
|
@@ -122,19 +139,122 @@ network) {
|
|
|
122
139
|
},
|
|
123
140
|
},
|
|
124
141
|
};
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
|
|
130
|
-
podSelector: `hammerkit.dev/id=${service.id()}`,
|
|
142
|
+
const readiness = {
|
|
143
|
+
podSelector: (0, resources_1.getItemSelector)(service, env),
|
|
131
144
|
timeout: service.data.healthcheck?.timeout ?? null,
|
|
132
145
|
timeoutMessage: service.data.healthcheck
|
|
133
146
|
? (0, work_healthcheck_1.getHealthcheckTimeoutMessage)(service.name, service.data.healthcheck)
|
|
134
147
|
: '',
|
|
135
|
-
|
|
148
|
+
container: getServiceContainerName(service),
|
|
149
|
+
subject: `service ${service.name}`,
|
|
150
|
+
onWaiting: (text) => service.status.write('warn', `${service.name} ${text}`),
|
|
151
|
+
...(service.data.restarts !== null ? { restarts: service.data.restarts } : {}),
|
|
152
|
+
};
|
|
153
|
+
if (env.server?.serviceRun) {
|
|
154
|
+
await rollOut(instance, env, service, deployment, readiness, rollout);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const deploy = await (0, apply_1.apply)(instance, deployment);
|
|
158
|
+
if ((deploy.status?.availableReplicas ?? 0) > 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, readiness);
|
|
136
162
|
}
|
|
137
163
|
exports.ensureKubernetesDeploymentExists = ensureKubernetesDeploymentExists;
|
|
164
|
+
// how long the pods of a rolled service may take to get ready, as Kubernetes counts it
|
|
165
|
+
exports.ROLLOUT_DEADLINE_SECONDS = 600;
|
|
166
|
+
/**
|
|
167
|
+
* The Deployment of a service of a service run, which keeps it from one commit to the next:
|
|
168
|
+
* replaced as a whole (a merge would keep what the new one dropped, such as a variable) and
|
|
169
|
+
* rolled. Its requests go to the pods it has until a new one is ready. A service with
|
|
170
|
+
* volumes of its own stops first: two pods would write the same data. The new pod is asked
|
|
171
|
+
* to go where the old one runs (getClaimPodSpec), whose claims are usually attached to one
|
|
172
|
+
* node at a time. When the execution ends before all its services are up, the worker puts
|
|
173
|
+
* every Deployment it rolled back as it was (serviceRollouts).
|
|
174
|
+
*/
|
|
175
|
+
async function rollOut(instance, env, service, deployment, readiness, rollout) {
|
|
176
|
+
const spec = deployment.spec;
|
|
177
|
+
const template = spec.template;
|
|
178
|
+
const definition = (0, service_definition_1.getServiceDefinitionHash)(service);
|
|
179
|
+
const stateKey = template.metadata?.labels?.['hammerkit.dev/state'] ?? '';
|
|
180
|
+
const ownVolumes = service.data.volumes.some((volume) => !volume.inherited);
|
|
181
|
+
spec.strategy = ownVolumes
|
|
182
|
+
? { type: 'Recreate' }
|
|
183
|
+
: { type: 'RollingUpdate', rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } };
|
|
184
|
+
spec.progressDeadlineSeconds = exports.ROLLOUT_DEADLINE_SECONDS;
|
|
185
|
+
// the pods of this definition and state, apart from the ones it rolls from
|
|
186
|
+
template.metadata = {
|
|
187
|
+
...template.metadata,
|
|
188
|
+
labels: { ...template.metadata?.labels, [exports.DEFINITION_LABEL]: definition },
|
|
189
|
+
};
|
|
190
|
+
const pod = getPodTemplateHash(template, service.data.projectRoot);
|
|
191
|
+
deployment.metadata.annotations = { ...deployment.metadata.annotations, [exports.POD_ANNOTATION]: pod };
|
|
192
|
+
const previous = rollout.previous;
|
|
193
|
+
if (previous &&
|
|
194
|
+
previous.metadata?.annotations?.[exports.POD_ANNOTATION] === pod &&
|
|
195
|
+
previous.metadata?.labels?.[exports.DEFINITION_LABEL] === definition &&
|
|
196
|
+
isAvailable(previous)) {
|
|
197
|
+
// its pod would be the same but for the state it runs at: a service that watches its
|
|
198
|
+
// files has them already (syncLiveFiles), and its pod stays
|
|
199
|
+
service_rollouts_1.serviceRollouts.add(env.namespace, deployment.metadata.name, previous);
|
|
200
|
+
await relabelDeployment(instance, env.namespace, deployment.metadata.name, stateKey);
|
|
201
|
+
service.status.write('info', `${service.name} watches its files, it got the changed ones and stays up`);
|
|
202
|
+
// it goes on running with them: else the move does not get up, and gets them back
|
|
203
|
+
await (0, await_running_state_1.watchKeptPods)(instance, env, deployment.metadata.name, { ...readiness, signal: rollout.signal });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// put back, or removed, by the worker when the execution ends before its services are up
|
|
207
|
+
service_rollouts_1.serviceRollouts.add(env.namespace, deployment.metadata.name, rollout.previous);
|
|
208
|
+
const applied = await (0, apply_1.replaceDeployment)(instance, deployment);
|
|
209
|
+
if (rollout.previous) {
|
|
210
|
+
service.status.write('info', `${service.name} changed, its new pod takes over once it is ready`);
|
|
211
|
+
}
|
|
212
|
+
await (0, await_running_state_1.awaitDeployRollout)(instance, env, deployment.metadata.name, {
|
|
213
|
+
...readiness,
|
|
214
|
+
podSelector: `${readiness.podSelector},hammerkit.dev/state=${stateKey},${exports.DEFINITION_LABEL}=${definition}`,
|
|
215
|
+
generation: applied.metadata?.generation ?? 0,
|
|
216
|
+
signal: rollout.signal,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// the pod a Deployment of a service run makes, but for the state it runs at
|
|
220
|
+
exports.POD_ANNOTATION = 'hammerkit.dev/pod';
|
|
221
|
+
// A move executes from a checkout of its own (`root`), which the pod's paths name: where the
|
|
222
|
+
// checkout is does not count, as for getServiceDefinitionHash
|
|
223
|
+
function getPodTemplateHash(template, root) {
|
|
224
|
+
const labels = { ...template.metadata?.labels };
|
|
225
|
+
delete labels['hammerkit.dev/state'];
|
|
226
|
+
const json = JSON.stringify({ ...template, metadata: { ...template.metadata, labels } });
|
|
227
|
+
const escaped = root ? JSON.stringify(root).slice(1, -1) : '';
|
|
228
|
+
return (0, crypto_1.createHash)('sha256')
|
|
229
|
+
.update(escaped ? json.split(escaped).join('<checkout>') : json)
|
|
230
|
+
.digest('hex')
|
|
231
|
+
.slice(0, 32);
|
|
232
|
+
}
|
|
233
|
+
exports.getPodTemplateHash = getPodTemplateHash;
|
|
234
|
+
// a Deployment whose pods are all of its template and ready
|
|
235
|
+
function isAvailable(deployment) {
|
|
236
|
+
const status = deployment.status;
|
|
237
|
+
return (!deployment.metadata?.deletionTimestamp &&
|
|
238
|
+
(status?.observedGeneration ?? 0) >= (deployment.metadata?.generation ?? 0) &&
|
|
239
|
+
(status?.updatedReplicas ?? 0) >= 1 &&
|
|
240
|
+
(status?.availableReplicas ?? 0) >= 1 &&
|
|
241
|
+
(status?.replicas ?? 0) === (status?.updatedReplicas ?? 0));
|
|
242
|
+
}
|
|
243
|
+
// The Deployment runs at the state `stateKey` now: its template, and so its pods, stay
|
|
244
|
+
async function relabelDeployment(instance, namespace, name, stateKey) {
|
|
245
|
+
await instance.appsApi.patchNamespacedDeployment({
|
|
246
|
+
name,
|
|
247
|
+
namespace,
|
|
248
|
+
body: [{ op: 'add', path: '/metadata/labels/hammerkit.dev~1state', value: stateKey }],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// how long the container of a service without a healthcheck has to run before it counts as up
|
|
252
|
+
exports.MIN_READY_SECONDS = 10;
|
|
253
|
+
// The container of a service's pod that runs its command, beside the `debug` one.
|
|
254
|
+
function getServiceContainerName(service) {
|
|
255
|
+
return service.name.replace(/:/, '-');
|
|
256
|
+
}
|
|
257
|
+
exports.getServiceContainerName = getServiceContainerName;
|
|
138
258
|
// Kubernetes `command` replaces the image entrypoint and `args` its command.
|
|
139
259
|
// A shell service sets both; an exec-form `cmd` keeps the established mapping
|
|
140
260
|
// (first token as `command`, the rest as `args`).
|
|
@@ -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
|
})),
|