hammerkit 1.7.0 → 1.9.0
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/README.md +3 -3
- package/build.schema.json +19 -13
- package/dist/docker/package.js +13 -3
- package/dist/docker/remove-orphaned-containers.js +53 -0
- package/dist/docker/run-labels.js +14 -0
- package/dist/docker/using-container.js +25 -26
- package/dist/executer/check-readiness.js +4 -4
- package/dist/executer/container-task-state.js +34 -0
- package/dist/executer/docker-service.js +23 -6
- package/dist/executer/docker-task.js +26 -4
- package/dist/executer/event-cache.js +8 -0
- package/dist/executer/execute-command.js +36 -6
- package/dist/executer/execute-docker.js +18 -2
- package/dist/executer/execute-work-service.js +4 -0
- package/dist/executer/execute-work-task.js +17 -2
- package/dist/executer/execution-steps.js +3 -8
- package/dist/executer/get-container-binds.js +13 -1
- package/dist/executer/main-process-user.js +52 -0
- package/dist/index.js +15 -3
- package/dist/kubernetes/apply.js +6 -7
- package/dist/kubernetes/await-running-state.js +48 -44
- package/dist/kubernetes/ensure-http-route.js +7 -10
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +27 -6
- package/dist/kubernetes/ensure-namespace.js +9 -10
- package/dist/kubernetes/ensure-persistent-data.js +6 -5
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/kubernetes-instance.js +0 -4
- package/dist/kubernetes/remove-persistent-data.js +16 -6
- package/dist/kubernetes/resolve-pod-name.js +7 -7
- package/dist/planner/unreferenced-build-file-envs.js +35 -0
- package/dist/planner/utils/append-work-environment.js +1 -1
- package/dist/planner/utils/append-work-service.js +26 -2
- package/dist/planner/utils/get-outputs-to-reset.js +20 -0
- package/dist/planner/utils/parse-work-generate.js +2 -2
- package/dist/planner/validate.js +10 -0
- package/dist/planner/work-healthcheck.js +8 -0
- package/dist/planner/work-instance-id.js +3 -3
- package/dist/planner/work-runtime-docker.js +19 -19
- package/dist/planner/work-runtime-kubernetes.js +66 -48
- package/dist/planner/work-runtime-local.js +22 -5
- package/dist/planner/work-service-id.js +3 -0
- package/dist/planner/work-service.js +27 -1
- package/dist/program.js +6 -6
- package/dist/schema/build-file-container-service-schema.js +8 -0
- package/dist/schema/build-file-environment-schema-http-route.js +15 -0
- package/dist/schema/build-file-environment-schema.js +8 -2
- package/dist/schema/build-file-schema.js +1 -1
- package/dist/schema/build-file-service-container-healthcheck.js +4 -0
- package/dist/schema/envs-schema.js +1 -1
- package/dist/utils/abort-on-signals.js +17 -0
- package/package.json +9 -9
- package/dist/kubernetes/ensure-ingress.js +0 -54
- package/dist/schema/build-file-environment-schema-ingress.js +0 -14
|
@@ -51,6 +51,15 @@ function withDeadline(parent, timeout) {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
// A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
|
|
55
|
+
// Having no src of its own it is never a cache hit, so its dependencies are
|
|
56
|
+
// always checked and their outputs restored. When every one of them was a hit,
|
|
57
|
+
// nothing ran: report the group as cached too. Reporting only, the cache
|
|
58
|
+
// decision is untouched.
|
|
59
|
+
function onlyGroupsCacheHits(work) {
|
|
60
|
+
return (work.data.cmds.length === 0 &&
|
|
61
|
+
work.deps.every((dep) => dep.state.current.type === 'completed' && dep.state.current.cached));
|
|
62
|
+
}
|
|
54
63
|
async function executeWorkTask(work, environment, options,
|
|
55
64
|
// when set, the task was not requested and only runs if one of these tasks
|
|
56
65
|
// depending on it misses the cache (see executeWorkTree)
|
|
@@ -139,12 +148,13 @@ dependents = null) {
|
|
|
139
148
|
if (work.state.current.type === 'running') {
|
|
140
149
|
work.status.write('debug', 'completed for state key ' + cacheState.stateKey);
|
|
141
150
|
await pushToBackend(work, environment, cacheState, options);
|
|
151
|
+
const cached = onlyGroupsCacheHits(work);
|
|
142
152
|
work.state.set({
|
|
143
153
|
stateKey: cacheState.stateKey,
|
|
144
154
|
type: 'completed',
|
|
145
|
-
cached
|
|
155
|
+
cached,
|
|
146
156
|
duration: (0, states_1.getDuration)(started),
|
|
147
|
-
missCauses,
|
|
157
|
+
missCauses: cached ? undefined : missCauses,
|
|
148
158
|
});
|
|
149
159
|
}
|
|
150
160
|
else {
|
|
@@ -170,6 +180,11 @@ dependents = null) {
|
|
|
170
180
|
errorMessage: (0, log_1.getErrorMessage)(e),
|
|
171
181
|
stateKey: null,
|
|
172
182
|
});
|
|
183
|
+
// Stop the run like a failing command does: dependents wait for this task
|
|
184
|
+
// to complete, which it now never will.
|
|
185
|
+
if (!options.watch) {
|
|
186
|
+
environment.abortCtrl.abort();
|
|
187
|
+
}
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
190
|
}
|
|
@@ -26,18 +26,13 @@ async function prepareVolume(item, docker) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
if ((0, work_task_1.isContainerWorkTask)(item.data)) {
|
|
29
|
+
// a task's outputs are emptied inside its container (see dockerTask)
|
|
29
30
|
for (const generate of item.data.generates) {
|
|
30
31
|
if (generate.isFile) {
|
|
31
32
|
continue;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await (0, get_docker_executor_1.recreateVolume)(docker, item.status, generate.volumeName);
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
item.status.write('debug', 'ensure volume exists');
|
|
39
|
-
await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, generate.volumeName);
|
|
40
|
-
}
|
|
34
|
+
item.status.write('debug', 'ensure volume exists');
|
|
35
|
+
await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, generate.volumeName);
|
|
41
36
|
}
|
|
42
37
|
}
|
|
43
38
|
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getContainerBinds = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
function isInside(path, parent) {
|
|
6
|
+
const rel = (0, path_1.relative)(parent, path);
|
|
7
|
+
return rel !== '' && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel);
|
|
8
|
+
}
|
|
4
9
|
function getContainerBinds(item) {
|
|
10
|
+
const sources = item.data.src.map((s) => s.absolutePath);
|
|
5
11
|
const items = [
|
|
6
12
|
...item.data.mounts,
|
|
7
|
-
|
|
13
|
+
// A source inside another source (a dependency's `app/app.csproj` under the
|
|
14
|
+
// task's own `app`) is already visible through the outer bind. Binding it
|
|
15
|
+
// again would nest bind mounts, which Docker Desktop then refuses to let the
|
|
16
|
+
// host delete while the container exists.
|
|
17
|
+
...sources
|
|
18
|
+
.filter((path) => !sources.some((other) => isInside(path, other)))
|
|
19
|
+
.map((path) => ({ localPath: path, containerPath: path })),
|
|
8
20
|
...item.data.generates.filter((g) => !g.isFile).map((v) => ({ localPath: v.volumeName, containerPath: v.path })),
|
|
9
21
|
...item.data.generates.filter((g) => g.isFile).map((v) => ({ localPath: v.path, containerPath: v.path })),
|
|
10
22
|
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getMainProcessUser = exports.parseProcessStatusUser = void 0;
|
|
4
|
+
const stream_1 = require("stream");
|
|
5
|
+
// `uid:gid` from the text of /proc/<pid>/status (effective ids), or null.
|
|
6
|
+
function parseProcessStatusUser(status) {
|
|
7
|
+
const uid = status.match(/^Uid:\s+\d+\s+(\d+)/m);
|
|
8
|
+
const gid = status.match(/^Gid:\s+\d+\s+(\d+)/m);
|
|
9
|
+
if (!uid || !gid) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return `${uid[1]}:${gid[1]}`;
|
|
13
|
+
}
|
|
14
|
+
exports.parseProcessStatusUser = parseProcessStatusUser;
|
|
15
|
+
// The user the container's main process (PID 1) runs as, read inside the
|
|
16
|
+
// container so user namespaces and rootless docker report the container's ids.
|
|
17
|
+
// Healthchecks run as this user, like a Kubernetes exec probe runs as the
|
|
18
|
+
// container's user: a check run as root could otherwise create files the
|
|
19
|
+
// service then cannot read (RabbitMQ's Erlang cookie). Null when it can't be
|
|
20
|
+
// read (no `cat` in the image, a windows container); the check then runs as
|
|
21
|
+
// the exec default, as before.
|
|
22
|
+
async function getMainProcessUser(container, timeoutMs = 2000) {
|
|
23
|
+
try {
|
|
24
|
+
const exec = await container.exec({ Cmd: ['cat', '/proc/1/status'], AttachStdout: true, AttachStderr: true });
|
|
25
|
+
const stream = await exec.start({ hijack: true, stdin: false });
|
|
26
|
+
const stdout = new stream_1.PassThrough();
|
|
27
|
+
const chunks = [];
|
|
28
|
+
stdout.on('data', (chunk) => chunks.push(chunk));
|
|
29
|
+
container.modem.demuxStream(stream, stdout, new stream_1.PassThrough());
|
|
30
|
+
const finished = await Promise.race([
|
|
31
|
+
new Promise((resolve) => {
|
|
32
|
+
stream.on('end', () => resolve(true));
|
|
33
|
+
stream.on('error', () => resolve(false));
|
|
34
|
+
}),
|
|
35
|
+
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs)),
|
|
36
|
+
]);
|
|
37
|
+
if (!finished) {
|
|
38
|
+
stream.destroy();
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const result = await exec.inspect();
|
|
42
|
+
if (result.ExitCode !== 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return parseProcessStatusUser(Buffer.concat(chunks).toString('utf8'));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.getMainProcessUser = getMainProcessUser;
|
|
52
|
+
//# sourceMappingURL=main-process-user.js.map
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,18 @@ const get_file_context_1 = require("./file/get-file-context");
|
|
|
6
6
|
const work_item_status_1 = require("./planner/work-item-status");
|
|
7
7
|
const empty_writable_1 = require("./utils/empty-writable");
|
|
8
8
|
const run_program_1 = require("./run-program");
|
|
9
|
+
const abort_on_signals_1 = require("./utils/abort-on-signals");
|
|
9
10
|
const abortCtrl = new AbortController();
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
(0, abort_on_signals_1.abortOnSignals)(process, abortCtrl);
|
|
12
|
+
let settled = false;
|
|
13
|
+
// A run that never settles (a task waiting on something that can't happen)
|
|
14
|
+
// lets the event loop drain, and Node then exits 0: CI would report success
|
|
15
|
+
// for a build that never finished. Fail loudly instead.
|
|
16
|
+
process.on('beforeExit', () => {
|
|
17
|
+
if (!settled) {
|
|
18
|
+
process.stderr.write('hammerkit stopped before the run finished\n');
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
|
12
21
|
});
|
|
13
22
|
(0, run_program_1.runProgram)({
|
|
14
23
|
cwd: process.cwd(),
|
|
@@ -20,7 +29,10 @@ process.on('SIGINT', function () {
|
|
|
20
29
|
stdout: process.stdout,
|
|
21
30
|
stderr: process.stderr,
|
|
22
31
|
stdoutColumns: process.stdout.columns,
|
|
23
|
-
}, process.argv, false).
|
|
32
|
+
}, process.argv, false).then(() => {
|
|
33
|
+
settled = true;
|
|
34
|
+
}, () => {
|
|
35
|
+
settled = true;
|
|
24
36
|
process.exit(1);
|
|
25
37
|
});
|
|
26
38
|
//# sourceMappingURL=index.js.map
|
package/dist/kubernetes/apply.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.apply = exports.statusCodeOf = void 0;
|
|
4
|
+
// The client throws an ApiException carrying the HTTP status as `code`; watch
|
|
5
|
+
// errors carry it as `statusCode`.
|
|
4
6
|
function statusCodeOf(e) {
|
|
5
7
|
const err = e;
|
|
6
|
-
return err?.
|
|
8
|
+
return typeof err?.code === 'number' ? err.code : err?.statusCode;
|
|
7
9
|
}
|
|
8
10
|
exports.statusCodeOf = statusCodeOf;
|
|
9
11
|
async function apply(instance, spec) {
|
|
10
12
|
try {
|
|
11
13
|
await instance.objectApi.read(spec);
|
|
12
|
-
|
|
13
|
-
return response.body;
|
|
14
|
+
return await instance.objectApi.patch(spec);
|
|
14
15
|
}
|
|
15
16
|
catch (e) {
|
|
16
17
|
try {
|
|
17
|
-
|
|
18
|
-
return response.body;
|
|
18
|
+
return await instance.objectApi.create(spec);
|
|
19
19
|
}
|
|
20
20
|
catch (createError) {
|
|
21
21
|
// Two work items can apply the same object concurrently (e.g. a volume
|
|
@@ -23,8 +23,7 @@ async function apply(instance, spec) {
|
|
|
23
23
|
// lost the create race, the object now exists — read it back instead of
|
|
24
24
|
// failing the whole item.
|
|
25
25
|
if (statusCodeOf(createError) === 409) {
|
|
26
|
-
|
|
27
|
-
return response.body;
|
|
26
|
+
return await instance.objectApi.read(spec);
|
|
28
27
|
}
|
|
29
28
|
throw createError;
|
|
30
29
|
}
|
|
@@ -4,25 +4,21 @@ exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.deleteJob =
|
|
|
4
4
|
const abort_1 = require("../executer/abort");
|
|
5
5
|
const abort_event_1 = require("../utils/abort-event");
|
|
6
6
|
const apply_1 = require("./apply");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
resolve();
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
});
|
|
7
|
+
const sleep_1 = require("../utils/sleep");
|
|
8
|
+
// The waits below poll instead of using the client's Watch: Watch aborts every
|
|
9
|
+
// request after 30s, which a pod or deployment still pulling its image outlasts.
|
|
10
|
+
async function awaitRunningState(instance, env, name, phase, pollInterval = 1000) {
|
|
11
|
+
for (;;) {
|
|
12
|
+
const pod = await instance.coreApi.readNamespacedPod({ name, namespace: env.namespace });
|
|
13
|
+
const current = pod.status?.phase;
|
|
14
|
+
if (current === phase) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (current === 'Succeeded' || current === 'Failed') {
|
|
18
|
+
throw new Error(`pod ${name} ended in phase ${current} before reaching ${phase}`);
|
|
19
|
+
}
|
|
20
|
+
await (0, sleep_1.sleep)(pollInterval);
|
|
21
|
+
}
|
|
26
22
|
}
|
|
27
23
|
exports.awaitRunningState = awaitRunningState;
|
|
28
24
|
// Wait for a job to finish by polling its status: resolves once it succeeded,
|
|
@@ -35,8 +31,8 @@ async function awaitJobCompletion(instance, env, name, abort, pollInterval = 100
|
|
|
35
31
|
await deleteJob(instance, env, name);
|
|
36
32
|
throw new abort_1.AbortError();
|
|
37
33
|
}
|
|
38
|
-
const job = await instance.batchApi.readNamespacedJobStatus(name, env.namespace);
|
|
39
|
-
const status = job.
|
|
34
|
+
const job = await instance.batchApi.readNamespacedJobStatus({ name, namespace: env.namespace });
|
|
35
|
+
const status = job.status;
|
|
40
36
|
if (status?.succeeded) {
|
|
41
37
|
return;
|
|
42
38
|
}
|
|
@@ -66,7 +62,12 @@ function sleep(ms, abort) {
|
|
|
66
62
|
}
|
|
67
63
|
async function deleteJob(instance, env, name) {
|
|
68
64
|
try {
|
|
69
|
-
await instance.batchApi.deleteNamespacedJob(
|
|
65
|
+
await instance.batchApi.deleteNamespacedJob({
|
|
66
|
+
name,
|
|
67
|
+
namespace: env.namespace,
|
|
68
|
+
gracePeriodSeconds: 0,
|
|
69
|
+
propagationPolicy: 'Background',
|
|
70
|
+
});
|
|
70
71
|
}
|
|
71
72
|
catch (e) {
|
|
72
73
|
if ((0, apply_1.statusCodeOf)(e) !== 404) {
|
|
@@ -75,29 +76,32 @@ async function deleteJob(instance, env, name) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
exports.deleteJob = deleteJob;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
// Waits for a ready replica. The deadline only starts once a pod runs, so an
|
|
80
|
+
// image pull doesn't count against it (as on docker, where it starts after
|
|
81
|
+
// the container started).
|
|
82
|
+
async function awaitDeployRunningState(instance, env, name, readiness, pollInterval = 1000, now = Date.now) {
|
|
83
|
+
let runningSince = null;
|
|
84
|
+
for (;;) {
|
|
85
|
+
const deployment = await instance.appsApi.readNamespacedDeployment({ name, namespace: env.namespace });
|
|
86
|
+
if ((deployment.status?.readyReplicas ?? 0) > 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (readiness.timeout !== null) {
|
|
90
|
+
if (runningSince === null) {
|
|
91
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
92
|
+
namespace: env.namespace,
|
|
93
|
+
labelSelector: readiness.podSelector,
|
|
94
|
+
});
|
|
95
|
+
if (pods.items.some((pod) => pod.status?.phase === 'Running')) {
|
|
96
|
+
runningSince = now();
|
|
97
|
+
}
|
|
95
98
|
}
|
|
96
|
-
else {
|
|
97
|
-
|
|
99
|
+
else if (now() - runningSince >= readiness.timeout) {
|
|
100
|
+
throw new Error(readiness.timeoutMessage);
|
|
98
101
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
102
|
+
}
|
|
103
|
+
await (0, sleep_1.sleep)(pollInterval);
|
|
104
|
+
}
|
|
101
105
|
}
|
|
102
106
|
exports.awaitDeployRunningState = awaitDeployRunningState;
|
|
103
107
|
// Delete a job and wait until it is gone: its name is reused for the task's next
|
|
@@ -107,7 +111,7 @@ async function deleteJobAndWait(instance, env, name, abort, pollInterval = 500)
|
|
|
107
111
|
await deleteJob(instance, env, name);
|
|
108
112
|
for (;;) {
|
|
109
113
|
try {
|
|
110
|
-
await instance.batchApi.readNamespacedJob(name, env.namespace);
|
|
114
|
+
await instance.batchApi.readNamespacedJob({ name, namespace: env.namespace });
|
|
111
115
|
}
|
|
112
116
|
catch (e) {
|
|
113
117
|
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
@@ -5,15 +5,12 @@ const apply_1 = require("./apply");
|
|
|
5
5
|
const resources_1 = require("./resources");
|
|
6
6
|
exports.HTTP_ROUTE_API_VERSION = 'gateway.networking.k8s.io/v1';
|
|
7
7
|
exports.HTTP_ROUTE_KIND = 'HTTPRoute';
|
|
8
|
-
async function ensureHttpRoute(instance, env,
|
|
9
|
-
if (!ingress.gateway) {
|
|
10
|
-
throw new Error(`httproute for ${ingress.host} requires a "gateway" (the parent Gateway name)`);
|
|
11
|
-
}
|
|
8
|
+
async function ensureHttpRoute(instance, env, route, service) {
|
|
12
9
|
const resource = {
|
|
13
10
|
apiVersion: exports.HTTP_ROUTE_API_VERSION,
|
|
14
11
|
kind: exports.HTTP_ROUTE_KIND,
|
|
15
12
|
metadata: {
|
|
16
|
-
name:
|
|
13
|
+
name: route.host,
|
|
17
14
|
namespace: env.namespace,
|
|
18
15
|
labels: {
|
|
19
16
|
'hammerkit.dev/id': service.id(),
|
|
@@ -22,25 +19,25 @@ async function ensureHttpRoute(instance, env, ingress, service) {
|
|
|
22
19
|
spec: {
|
|
23
20
|
parentRefs: [
|
|
24
21
|
{
|
|
25
|
-
name:
|
|
26
|
-
...(
|
|
22
|
+
name: route.gateway,
|
|
23
|
+
...(route.gatewayNamespace ? { namespace: route.gatewayNamespace } : {}),
|
|
27
24
|
},
|
|
28
25
|
],
|
|
29
|
-
hostnames: [
|
|
26
|
+
hostnames: [route.host],
|
|
30
27
|
rules: [
|
|
31
28
|
{
|
|
32
29
|
matches: [
|
|
33
30
|
{
|
|
34
31
|
path: {
|
|
35
32
|
type: 'PathPrefix',
|
|
36
|
-
value:
|
|
33
|
+
value: route.path ?? '/',
|
|
37
34
|
},
|
|
38
35
|
},
|
|
39
36
|
],
|
|
40
37
|
backendRefs: [
|
|
41
38
|
{
|
|
42
39
|
name: (0, resources_1.getResourceName)(service),
|
|
43
|
-
port:
|
|
40
|
+
port: route.servicePort ?? service.data.ports[0].containerPort,
|
|
44
41
|
},
|
|
45
42
|
],
|
|
46
43
|
},
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ensureKubernetesDeploymentExists = void 0;
|
|
4
4
|
const work_item_1 = require("../planner/work-item");
|
|
5
|
+
const work_service_1 = require("../planner/work-service");
|
|
5
6
|
const apply_1 = require("./apply");
|
|
6
7
|
const get_service_ip_1 = require("./get-service-ip");
|
|
7
8
|
const version_1 = require("../version");
|
|
8
9
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
9
10
|
const await_running_state_1 = require("./await-running-state");
|
|
10
11
|
const resources_1 = require("./resources");
|
|
12
|
+
const work_healthcheck_1 = require("../planner/work-healthcheck");
|
|
11
13
|
async function ensureKubernetesDeploymentExists(instance, env, service, persistence, stateKey) {
|
|
12
14
|
const hostAliases = [];
|
|
13
15
|
for (const need of service.needs) {
|
|
@@ -25,10 +27,12 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
25
27
|
}
|
|
26
28
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
|
|
27
29
|
const name = (0, resources_1.getResourceName)(service);
|
|
28
|
-
const
|
|
30
|
+
const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
|
|
31
|
+
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data);
|
|
32
|
+
const probe = healthcheckCommand
|
|
29
33
|
? {
|
|
30
34
|
exec: {
|
|
31
|
-
command:
|
|
35
|
+
command: healthcheckCommand,
|
|
32
36
|
},
|
|
33
37
|
periodSeconds: 5,
|
|
34
38
|
failureThreshold: 3,
|
|
@@ -68,13 +72,12 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
68
72
|
{
|
|
69
73
|
name: service.name.replace(/:/, '-'),
|
|
70
74
|
image: service.data.image,
|
|
71
|
-
workingDir: service.data
|
|
75
|
+
workingDir: (0, work_service_1.getServiceWorkingDir)(service.data),
|
|
72
76
|
env: Object.entries(envs).map(([key, value]) => ({
|
|
73
77
|
name: key,
|
|
74
78
|
value: value,
|
|
75
79
|
})),
|
|
76
|
-
|
|
77
|
-
args: service.data.cmd ? service.data.cmd.parsed.args : undefined,
|
|
80
|
+
...getKubernetesCommand(serviceCommand),
|
|
78
81
|
ports: service.data.ports.map((p) => ({
|
|
79
82
|
containerPort: p.containerPort,
|
|
80
83
|
})),
|
|
@@ -99,7 +102,25 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
99
102
|
if (deploy.status?.readyReplicas ?? 0 > 0) {
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
102
|
-
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name
|
|
105
|
+
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
|
|
106
|
+
podSelector: `hammerkit.dev/id=${service.id()}`,
|
|
107
|
+
timeout: service.data.healthcheck?.timeout ?? null,
|
|
108
|
+
timeoutMessage: service.data.healthcheck
|
|
109
|
+
? (0, work_healthcheck_1.getHealthcheckTimeoutMessage)(service.name, service.data.healthcheck)
|
|
110
|
+
: '',
|
|
111
|
+
});
|
|
103
112
|
}
|
|
104
113
|
exports.ensureKubernetesDeploymentExists = ensureKubernetesDeploymentExists;
|
|
114
|
+
// Kubernetes `command` replaces the image entrypoint and `args` its command.
|
|
115
|
+
// A shell service sets both; an exec-form `cmd` keeps the established mapping
|
|
116
|
+
// (first token as `command`, the rest as `args`).
|
|
117
|
+
function getKubernetesCommand(command) {
|
|
118
|
+
if (command.entrypoint && command.cmd) {
|
|
119
|
+
return { command: command.entrypoint, args: command.cmd };
|
|
120
|
+
}
|
|
121
|
+
if (command.cmd) {
|
|
122
|
+
return { command: [command.cmd[0]], args: command.cmd.slice(1) };
|
|
123
|
+
}
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
105
126
|
//# sourceMappingURL=ensure-kubernetes-deployment-exists.js.map
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ensureNamespace = void 0;
|
|
4
|
-
|
|
5
|
-
const err = e;
|
|
6
|
-
return err?.statusCode ?? err?.response?.statusCode ?? err?.body?.code;
|
|
7
|
-
}
|
|
4
|
+
const apply_1 = require("./apply");
|
|
8
5
|
/**
|
|
9
6
|
* Idempotently make sure `namespace` exists. hammerkit does not otherwise
|
|
10
7
|
* create namespaces — deployments/services/jobs are applied straight into
|
|
@@ -16,25 +13,27 @@ function statusCodeOf(e) {
|
|
|
16
13
|
*/
|
|
17
14
|
async function ensureNamespace(instance, namespace) {
|
|
18
15
|
try {
|
|
19
|
-
await instance.coreApi.readNamespace(namespace);
|
|
16
|
+
await instance.coreApi.readNamespace({ name: namespace });
|
|
20
17
|
return;
|
|
21
18
|
}
|
|
22
19
|
catch (e) {
|
|
23
|
-
if (statusCodeOf(e) !== 404) {
|
|
20
|
+
if ((0, apply_1.statusCodeOf)(e) !== 404) {
|
|
24
21
|
throw e;
|
|
25
22
|
}
|
|
26
23
|
}
|
|
27
24
|
try {
|
|
28
25
|
await instance.coreApi.createNamespace({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
body: {
|
|
27
|
+
metadata: {
|
|
28
|
+
name: namespace,
|
|
29
|
+
labels: { 'hammerkit.dev/managed': 'true' },
|
|
30
|
+
},
|
|
32
31
|
},
|
|
33
32
|
});
|
|
34
33
|
}
|
|
35
34
|
catch (e) {
|
|
36
35
|
// another worker created it between our read and create
|
|
37
|
-
if (statusCodeOf(e) !== 409) {
|
|
36
|
+
if ((0, apply_1.statusCodeOf)(e) !== 409) {
|
|
38
37
|
throw e;
|
|
39
38
|
}
|
|
40
39
|
}
|
|
@@ -10,6 +10,7 @@ const sleep_1 = require("../utils/sleep");
|
|
|
10
10
|
const version_1 = require("../version");
|
|
11
11
|
const log_1 = require("../log");
|
|
12
12
|
const ensure_kubernetes_persistent_volume_claim_exists_1 = require("./ensure-kubernetes-persistent-volume-claim-exists");
|
|
13
|
+
const stream_1 = require("stream");
|
|
13
14
|
// A previous run that crashed between creating an upload/download pod and its
|
|
14
15
|
// `finally` cleanup leaves a pod behind under the same deterministic name. apply()
|
|
15
16
|
// would then patch that stale pod in place — and if it is stuck in a terminal
|
|
@@ -17,7 +18,7 @@ const ensure_kubernetes_persistent_volume_claim_exists_1 = require("./ensure-kub
|
|
|
17
18
|
// and wait for it to disappear before scheduling a fresh one.
|
|
18
19
|
async function removeStalePod(instance, namespace, name) {
|
|
19
20
|
try {
|
|
20
|
-
await instance.coreApi.deleteNamespacedPod(name, namespace,
|
|
21
|
+
await instance.coreApi.deleteNamespacedPod({ name, namespace, gracePeriodSeconds: 0 });
|
|
21
22
|
}
|
|
22
23
|
catch (e) {
|
|
23
24
|
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
@@ -27,7 +28,7 @@ async function removeStalePod(instance, namespace, name) {
|
|
|
27
28
|
}
|
|
28
29
|
for (let attempt = 0; attempt < 30; attempt++) {
|
|
29
30
|
try {
|
|
30
|
-
await instance.coreApi.readNamespacedPod(name, namespace);
|
|
31
|
+
await instance.coreApi.readNamespacedPod({ name, namespace });
|
|
31
32
|
}
|
|
32
33
|
catch (e) {
|
|
33
34
|
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
@@ -100,7 +101,7 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
|
|
|
100
101
|
}
|
|
101
102
|
finally {
|
|
102
103
|
service.status.console('stdout', `delete pod ${name}`);
|
|
103
|
-
await instance.coreApi.deleteNamespacedPod(name, env.namespace);
|
|
104
|
+
await instance.coreApi.deleteNamespacedPod({ name, namespace: env.namespace });
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
exports.getPodForPersistence = getPodForPersistence;
|
|
@@ -122,12 +123,12 @@ async function ensurePersistentData(instance, env, environment, service, persist
|
|
|
122
123
|
continue;
|
|
123
124
|
}
|
|
124
125
|
const fileItem = (0, path_1.basename)(source.localPath);
|
|
125
|
-
const data = (0, tar_1.create)({
|
|
126
|
+
const data = stream_1.Readable.from((0, tar_1.create)({
|
|
126
127
|
cwd: (0, path_1.dirname)(source.localPath),
|
|
127
128
|
filter(path) {
|
|
128
129
|
return source.matcher(path, source.localPath);
|
|
129
130
|
},
|
|
130
|
-
}, [fileItem]);
|
|
131
|
+
}, [fileItem]));
|
|
131
132
|
service.status.console('stdout', `upload ${source.localPath}`);
|
|
132
133
|
const targetPath = `/dev/hammerkit/${service.id()}`;
|
|
133
134
|
const res = await instance.exec.exec(env.namespace, name, 'volume', ['tar', 'xf', '-', '-C', targetPath], process.stdout, process.stderr, data, false, (status) => {
|
|
@@ -4,8 +4,8 @@ exports.getServiceIp = void 0;
|
|
|
4
4
|
const resources_1 = require("./resources");
|
|
5
5
|
async function getServiceIp(instance, env, workService) {
|
|
6
6
|
const name = (0, resources_1.getResourceName)(workService);
|
|
7
|
-
const svc = await instance.coreApi.readNamespacedService(name, env.namespace);
|
|
8
|
-
return svc.
|
|
7
|
+
const svc = await instance.coreApi.readNamespacedService({ name, namespace: env.namespace });
|
|
8
|
+
return svc.spec?.clusterIP ?? null;
|
|
9
9
|
}
|
|
10
10
|
exports.getServiceIp = getServiceIp;
|
|
11
11
|
//# sourceMappingURL=get-service-ip.js.map
|
|
@@ -16,14 +16,10 @@ function createKubernetesInstances(kubernetes) {
|
|
|
16
16
|
const coreApi = kc.makeApiClient(client_node_1.CoreV1Api);
|
|
17
17
|
const batchApi = kc.makeApiClient(client_node_1.BatchV1Api);
|
|
18
18
|
const appsApi = kc.makeApiClient(client_node_1.AppsV1Api);
|
|
19
|
-
const networkingApi = kc.makeApiClient(client_node_1.NetworkingV1Api);
|
|
20
19
|
const objectApi = kc.makeApiClient(client_node_1.KubernetesObjectApi);
|
|
21
20
|
const exec = new client_node_1.Exec(kc);
|
|
22
|
-
const watch = new client_node_1.Watch(kc);
|
|
23
21
|
return {
|
|
24
22
|
objectApi,
|
|
25
|
-
networkingApi,
|
|
26
|
-
watch,
|
|
27
23
|
exec,
|
|
28
24
|
appsApi,
|
|
29
25
|
coreApi,
|
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.removePersistentData = void 0;
|
|
4
4
|
async function removePersistentData(instance, kubernetes, task) {
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const labelSelector = `hammerkit.dev/id=${task.id()}`;
|
|
6
|
+
const configmaps = await instance.coreApi.listNamespacedConfigMap({ namespace: kubernetes.namespace, labelSelector });
|
|
7
|
+
for (const configMap of configmaps.items) {
|
|
7
8
|
if (configMap.metadata?.name) {
|
|
8
|
-
await instance.coreApi.deleteNamespacedConfigMap(
|
|
9
|
+
await instance.coreApi.deleteNamespacedConfigMap({
|
|
10
|
+
name: configMap.metadata.name,
|
|
11
|
+
namespace: kubernetes.namespace,
|
|
12
|
+
});
|
|
9
13
|
}
|
|
10
14
|
}
|
|
11
|
-
const pvcs = await instance.coreApi.listNamespacedPersistentVolumeClaim(
|
|
12
|
-
|
|
15
|
+
const pvcs = await instance.coreApi.listNamespacedPersistentVolumeClaim({
|
|
16
|
+
namespace: kubernetes.namespace,
|
|
17
|
+
labelSelector,
|
|
18
|
+
});
|
|
19
|
+
for (const pvc of pvcs.items) {
|
|
13
20
|
if (pvc.metadata?.name) {
|
|
14
|
-
await instance.coreApi.deleteNamespacedPersistentVolumeClaim(
|
|
21
|
+
await instance.coreApi.deleteNamespacedPersistentVolumeClaim({
|
|
22
|
+
name: pvc.metadata.name,
|
|
23
|
+
namespace: kubernetes.namespace,
|
|
24
|
+
});
|
|
15
25
|
}
|
|
16
26
|
}
|
|
17
27
|
}
|