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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# About
|
|
2
2
|
|
|
3
|
-
<
|
|
3
|
+
<img src="website/public/logo.png" width="144" alt="hammerkit">
|
|
4
4
|
|
|
5
5
|
## What is hammerkit?
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ Hammerkit is a build tool intended to build software projects in **containers**
|
|
|
9
9
|
* the build tools come from the container image, so they are the same across all machines.
|
|
10
10
|
* the build process is isolated since every file in the container has to be declared as a source or an output. Therefore, side effects can be reduced.
|
|
11
11
|
|
|
12
|
-
For a fuller pitch — including how it compares to Make, npm scripts, Taskfile and Earthly — see [Why hammerkit](docs/why-hammerkit
|
|
12
|
+
For a fuller pitch — including how it compares to Make, npm scripts, Taskfile and Earthly — see [Why hammerkit](https://hammerkit.dev/docs/why-hammerkit).
|
|
13
13
|
|
|
14
14
|
There are a lot of containerized CI systems that allow writing containerized builds today, but they have shortcomings that hammerkit tries to address:
|
|
15
15
|
|
|
@@ -23,4 +23,4 @@ Hammerkit, on the other hand, knows the source and output files of each build st
|
|
|
23
23
|
|
|
24
24
|
## Where to go next?
|
|
25
25
|
|
|
26
|
-
Check out the [docs](https://
|
|
26
|
+
Check out the [docs on hammerkit.dev](https://hammerkit.dev) for more info about how hammerkit can be used.
|
package/build.schema.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"number"
|
|
13
13
|
]
|
|
14
14
|
},
|
|
15
|
-
"description": "Environment values for the current build task\nhttps://
|
|
15
|
+
"description": "Environment values for the current build task\nhttps://hammerkit.dev/docs/build-file/environment-variables"
|
|
16
16
|
},
|
|
17
17
|
"tasks": {
|
|
18
18
|
"type": "object",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"envs": {
|
|
63
63
|
"$ref": "#/definitions/hammerkit/properties/envs",
|
|
64
|
-
"description": "Environment values for the current build task\nhttps://
|
|
64
|
+
"description": "Environment values for the current build task\nhttps://hammerkit.dev/docs/build-file/environment-variables"
|
|
65
65
|
},
|
|
66
66
|
"labels": {
|
|
67
67
|
"type": "object",
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
},
|
|
281
281
|
"envs": {
|
|
282
282
|
"$ref": "#/definitions/hammerkit/properties/envs",
|
|
283
|
-
"description": "Environment values for the current build task\nhttps://
|
|
283
|
+
"description": "Environment values for the current build task\nhttps://hammerkit.dev/docs/build-file/environment-variables"
|
|
284
284
|
},
|
|
285
285
|
"mounts": {
|
|
286
286
|
"type": "array",
|
|
@@ -309,6 +309,12 @@
|
|
|
309
309
|
"cmd": {
|
|
310
310
|
"type": "string"
|
|
311
311
|
},
|
|
312
|
+
"workdir": {
|
|
313
|
+
"type": "string"
|
|
314
|
+
},
|
|
315
|
+
"shell": {
|
|
316
|
+
"type": "string"
|
|
317
|
+
},
|
|
312
318
|
"volumes": {
|
|
313
319
|
"type": "array",
|
|
314
320
|
"items": {
|
|
@@ -326,6 +332,9 @@
|
|
|
326
332
|
"properties": {
|
|
327
333
|
"cmd": {
|
|
328
334
|
"type": "string"
|
|
335
|
+
},
|
|
336
|
+
"timeout": {
|
|
337
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/timeout"
|
|
329
338
|
}
|
|
330
339
|
},
|
|
331
340
|
"required": [
|
|
@@ -431,18 +440,11 @@
|
|
|
431
440
|
"kubeconfig": {
|
|
432
441
|
"type": "string"
|
|
433
442
|
},
|
|
434
|
-
"
|
|
443
|
+
"httpRoutes": {
|
|
435
444
|
"type": "array",
|
|
436
445
|
"items": {
|
|
437
446
|
"type": "object",
|
|
438
447
|
"properties": {
|
|
439
|
-
"kind": {
|
|
440
|
-
"type": "string",
|
|
441
|
-
"enum": [
|
|
442
|
-
"ingress",
|
|
443
|
-
"httproute"
|
|
444
|
-
]
|
|
445
|
-
},
|
|
446
448
|
"host": {
|
|
447
449
|
"type": "string"
|
|
448
450
|
},
|
|
@@ -464,10 +466,14 @@
|
|
|
464
466
|
},
|
|
465
467
|
"required": [
|
|
466
468
|
"host",
|
|
467
|
-
"service"
|
|
469
|
+
"service",
|
|
470
|
+
"gateway"
|
|
468
471
|
],
|
|
469
472
|
"additionalProperties": false
|
|
470
473
|
}
|
|
474
|
+
},
|
|
475
|
+
"ingresses": {
|
|
476
|
+
"not": {}
|
|
471
477
|
}
|
|
472
478
|
},
|
|
473
479
|
"required": [
|
|
@@ -608,7 +614,7 @@
|
|
|
608
614
|
}
|
|
609
615
|
},
|
|
610
616
|
"additionalProperties": false,
|
|
611
|
-
"description": "Build file with support for containerization\nhttps://
|
|
617
|
+
"description": "Build file with support for containerization\nhttps://hammerkit.dev/docs/build-file"
|
|
612
618
|
}
|
|
613
619
|
},
|
|
614
620
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
package/dist/docker/package.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.longestCommonPrefix = exports.packageWorkTree = exports.getDefaultPlatform = void 0;
|
|
3
|
+
exports.getServiceInstructions = exports.longestCommonPrefix = exports.packageWorkTree = exports.getDefaultPlatform = void 0;
|
|
4
4
|
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
5
5
|
const work_item_1 = require("../planner/work-item");
|
|
6
|
+
const work_service_1 = require("../planner/work-service");
|
|
6
7
|
const execute_docker_1 = require("../executer/execute-docker");
|
|
7
8
|
const node_os_1 = require("node:os");
|
|
8
9
|
const path_1 = require("path");
|
|
@@ -199,9 +200,17 @@ function getServiceInstructions(service, options) {
|
|
|
199
200
|
? service.data.volumes.map((v) => `RUN chown -R 1000:1000 ${toContainerPath((0, path_1.relative)(dependencyCwd, v.containerPath))}`)
|
|
200
201
|
: []),
|
|
201
202
|
options.overrideUser ? 'USER 1000:1000' : '',
|
|
203
|
+
// last, so the COPY destinations above stay relative to the build directory
|
|
204
|
+
service.data.workdir ? `WORKDIR ${service.data.workdir}` : '',
|
|
202
205
|
];
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
// exec form, JSON-encoded so arguments with quotes or a whole shell command
|
|
207
|
+
// survive intact
|
|
208
|
+
const command = (0, work_service_1.getServiceCommand)(service.data);
|
|
209
|
+
if (command.entrypoint) {
|
|
210
|
+
instructions.push(`ENTRYPOINT ${JSON.stringify(command.entrypoint)}`);
|
|
211
|
+
}
|
|
212
|
+
if (command.cmd) {
|
|
213
|
+
instructions.push(`CMD ${JSON.stringify(command.cmd)}`);
|
|
205
214
|
}
|
|
206
215
|
return {
|
|
207
216
|
id: service.id(),
|
|
@@ -221,6 +230,7 @@ function getServiceInstructions(service, options) {
|
|
|
221
230
|
],
|
|
222
231
|
};
|
|
223
232
|
}
|
|
233
|
+
exports.getServiceInstructions = getServiceInstructions;
|
|
224
234
|
function getDependencyInstructions(cwd, task, taskIds) {
|
|
225
235
|
return task.deps.map((dep) => {
|
|
226
236
|
if (taskIds[dep.id()]) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeOrphanedContainers = exports.isProcessAlive = exports.isOrphanedContainer = void 0;
|
|
4
|
+
const os_1 = require("os");
|
|
5
|
+
const remove_container_1 = require("./remove-container");
|
|
6
|
+
// A hammerkit container nothing will remove any more: a task container (they
|
|
7
|
+
// never outlive their run) or a service of a run that is gone, started by a
|
|
8
|
+
// hammerkit process of this machine that no longer exists, and the paused
|
|
9
|
+
// state records hammerkit kept before 1.9. Services of `up --daemon` and
|
|
10
|
+
// containers of other machines or of running processes stay.
|
|
11
|
+
function isOrphanedContainer(container, isProcessAlive) {
|
|
12
|
+
const labels = container.Labels;
|
|
13
|
+
const type = labels['hammerkit-type'];
|
|
14
|
+
if (type === 'task' && container.State === 'paused') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (type !== 'task' && type !== 'service') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (labels['hammerkit-daemon'] === 'true') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (labels['hammerkit-host'] !== (0, os_1.hostname)()) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const pid = parseInt(labels['hammerkit-pid'] ?? '', 10);
|
|
27
|
+
if (!pid) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return !isProcessAlive(pid);
|
|
31
|
+
}
|
|
32
|
+
exports.isOrphanedContainer = isOrphanedContainer;
|
|
33
|
+
function isProcessAlive(pid) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
// EPERM: it exists, it just belongs to another user
|
|
40
|
+
return e.code === 'EPERM';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.isProcessAlive = isProcessAlive;
|
|
44
|
+
async function removeOrphanedContainers(docker) {
|
|
45
|
+
const containers = await docker.listContainers({ all: true, filters: { label: ['app=hammerkit'] } });
|
|
46
|
+
const orphaned = containers.filter((c) => isOrphanedContainer(c, isProcessAlive));
|
|
47
|
+
for (const container of orphaned) {
|
|
48
|
+
await (0, remove_container_1.removeContainer)(docker.getContainer(container.Id));
|
|
49
|
+
}
|
|
50
|
+
return orphaned.map((c) => c.Id);
|
|
51
|
+
}
|
|
52
|
+
exports.removeOrphanedContainers = removeOrphanedContainers;
|
|
53
|
+
//# sourceMappingURL=remove-orphaned-containers.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRunLabels = void 0;
|
|
4
|
+
const os_1 = require("os");
|
|
5
|
+
// Which hammerkit process on which machine started a container. `clean` uses
|
|
6
|
+
// them to tell a container a killed run left behind from one still in use.
|
|
7
|
+
function getRunLabels() {
|
|
8
|
+
return {
|
|
9
|
+
'hammerkit-pid': process.pid.toString(),
|
|
10
|
+
'hammerkit-host': (0, os_1.hostname)(),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
exports.getRunLabels = getRunLabels;
|
|
14
|
+
//# sourceMappingURL=run-labels.js.map
|
|
@@ -5,40 +5,23 @@ const remove_container_1 = require("./remove-container");
|
|
|
5
5
|
const log_1 = require("../log");
|
|
6
6
|
const execute_docker_1 = require("../executer/execute-docker");
|
|
7
7
|
const work_instance_id_1 = require("../planner/work-instance-id");
|
|
8
|
-
|
|
8
|
+
const remove_orphaned_containers_1 = require("./remove-orphaned-containers");
|
|
9
|
+
// Runs the callback in a fresh container and removes it afterwards, whatever
|
|
10
|
+
// the outcome. What state the outputs hold is recorded outside docker (see
|
|
11
|
+
// container-task-state.ts), so no container outlives the run.
|
|
12
|
+
async function usingContainer(docker, item, createOptions, callback) {
|
|
13
|
+
await removeLeftoverContainers(docker, item);
|
|
9
14
|
let container = null;
|
|
10
|
-
let succeeded = false;
|
|
11
|
-
let result;
|
|
12
15
|
try {
|
|
13
16
|
container = await docker.createContainer(createOptions);
|
|
14
17
|
item.status.write('debug', `starting container with image ${item.data.image}`);
|
|
15
18
|
await (0, execute_docker_1.startContainer)(item.status, container);
|
|
16
|
-
|
|
17
|
-
succeeded = result !== false;
|
|
18
|
-
return result;
|
|
19
|
+
return await callback(container);
|
|
19
20
|
}
|
|
20
21
|
finally {
|
|
21
22
|
try {
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
await container.pause();
|
|
25
|
-
}
|
|
26
|
-
const containers = await docker.listContainers({
|
|
27
|
-
all: true,
|
|
28
|
-
filters: { label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(item)}`] },
|
|
29
|
-
});
|
|
30
|
-
for (const container of containers) {
|
|
31
|
-
item.status.write('debug', `found container ${container.Id}`);
|
|
32
|
-
if (container.Labels['hammerkit-state'] != stateKey) {
|
|
33
|
-
const oldContainer = docker.getContainer(container.Id);
|
|
34
|
-
await (0, remove_container_1.removeContainer)(oldContainer);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
if (container) {
|
|
40
|
-
await (0, remove_container_1.removeContainer)(container);
|
|
41
|
-
}
|
|
23
|
+
if (container) {
|
|
24
|
+
await (0, remove_container_1.removeContainer)(container);
|
|
42
25
|
}
|
|
43
26
|
}
|
|
44
27
|
catch (e) {
|
|
@@ -47,4 +30,20 @@ async function usingContainer(docker, item, createOptions, stateKey, callback) {
|
|
|
47
30
|
}
|
|
48
31
|
}
|
|
49
32
|
exports.usingContainer = usingContainer;
|
|
33
|
+
// Containers of this item nothing will remove: the paused state records
|
|
34
|
+
// hammerkit kept before 1.9, and stopped ones of a hammerkit process that is
|
|
35
|
+
// gone. A container of a live process (one another call of this run has just
|
|
36
|
+
// created, say) stays.
|
|
37
|
+
async function removeLeftoverContainers(docker, item) {
|
|
38
|
+
const containers = await docker.listContainers({
|
|
39
|
+
all: true,
|
|
40
|
+
filters: { label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(item)}`] },
|
|
41
|
+
});
|
|
42
|
+
const leftovers = containers.filter((c) => c.State === 'paused' ||
|
|
43
|
+
(c.State !== 'running' && !(0, remove_orphaned_containers_1.isProcessAlive)(parseInt(c.Labels['hammerkit-pid'] ?? '', 10) || process.pid)));
|
|
44
|
+
for (const leftover of leftovers) {
|
|
45
|
+
item.status.write('debug', `remove leftover container ${leftover.Id}`);
|
|
46
|
+
await (0, remove_container_1.removeContainer)(docker.getContainer(leftover.Id));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
50
49
|
//# sourceMappingURL=using-container.js.map
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkReadiness = void 0;
|
|
4
4
|
const execute_docker_1 = require("./execute-docker");
|
|
5
|
-
async function checkReadiness(status,
|
|
6
|
-
const result = await (0, execute_docker_1.execCommand)(status, environment, container, undefined,
|
|
5
|
+
async function checkReadiness(status, command, environment, container, user, abort) {
|
|
6
|
+
const result = await (0, execute_docker_1.execCommand)(status, environment, container, undefined, command, user, 2000, abort);
|
|
7
7
|
if (result.type === 'timeout') {
|
|
8
8
|
return false;
|
|
9
9
|
}
|
|
@@ -12,11 +12,11 @@ async function checkReadiness(status, healthCheck, environment, container, abort
|
|
|
12
12
|
}
|
|
13
13
|
else {
|
|
14
14
|
if (result.result.ExitCode === 0) {
|
|
15
|
-
status.write('debug', `healthcheck ${
|
|
15
|
+
status.write('debug', `healthcheck ${command.join(' ')} succeeded`);
|
|
16
16
|
return true;
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
|
-
status.write('debug', `healthcheck ${
|
|
19
|
+
status.write('debug', `healthcheck ${command.join(' ')} failed with ${result.result.ExitCode}`);
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeContainerTaskState = exports.writeContainerTaskState = exports.readContainerTaskState = exports.getContainerTaskStateFile = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
6
|
+
const work_instance_id_1 = require("../planner/work-instance-id");
|
|
7
|
+
// The record says "the outputs of this container task belong to this state
|
|
8
|
+
// key", like the state file of a local task. It lives in the hammerkit
|
|
9
|
+
// directory, not the project, so writing it never touches a task's src.
|
|
10
|
+
function getContainerTaskStateFile(task) {
|
|
11
|
+
return (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'state', (0, work_instance_id_1.getWorkInstanceId)(task));
|
|
12
|
+
}
|
|
13
|
+
exports.getContainerTaskStateFile = getContainerTaskStateFile;
|
|
14
|
+
async function readContainerTaskState(environment, task) {
|
|
15
|
+
const stateFile = getContainerTaskStateFile(task);
|
|
16
|
+
if (!(await environment.file.exists(stateFile))) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return (await environment.file.read(stateFile)).trim() || null;
|
|
20
|
+
}
|
|
21
|
+
exports.readContainerTaskState = readContainerTaskState;
|
|
22
|
+
async function writeContainerTaskState(environment, task, stateKey) {
|
|
23
|
+
await environment.file.createDirectory((0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'state'));
|
|
24
|
+
await environment.file.writeFile(getContainerTaskStateFile(task), stateKey);
|
|
25
|
+
}
|
|
26
|
+
exports.writeContainerTaskState = writeContainerTaskState;
|
|
27
|
+
async function removeContainerTaskState(environment, task) {
|
|
28
|
+
const stateFile = getContainerTaskStateFile(task);
|
|
29
|
+
if (await environment.file.exists(stateFile)) {
|
|
30
|
+
await environment.file.remove(stateFile);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.removeContainerTaskState = removeContainerTaskState;
|
|
34
|
+
//# sourceMappingURL=container-task-state.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dockerService = void 0;
|
|
4
|
+
const work_service_1 = require("../planner/work-service");
|
|
5
|
+
const run_labels_1 = require("../docker/run-labels");
|
|
4
6
|
const abort_1 = require("./abort");
|
|
5
7
|
const execute_docker_1 = require("./execute-docker");
|
|
6
8
|
const stream_1 = require("../docker/stream");
|
|
@@ -8,11 +10,14 @@ const abort_event_1 = require("../utils/abort-event");
|
|
|
8
10
|
const log_1 = require("../log");
|
|
9
11
|
const remove_container_1 = require("../docker/remove-container");
|
|
10
12
|
const check_readiness_1 = require("./check-readiness");
|
|
13
|
+
const main_process_user_1 = require("./main-process-user");
|
|
11
14
|
const execution_steps_1 = require("./execution-steps");
|
|
12
15
|
const docker_task_1 = require("./docker-task");
|
|
13
16
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
14
17
|
const get_service_containers_1 = require("./get-service-containers");
|
|
15
18
|
const work_instance_id_1 = require("../planner/work-instance-id");
|
|
19
|
+
const work_healthcheck_1 = require("../planner/work-healthcheck");
|
|
20
|
+
const HEALTHCHECK_INTERVAL_MS = 1000;
|
|
16
21
|
async function dockerService(docker, item, options, environment) {
|
|
17
22
|
let container = null;
|
|
18
23
|
try {
|
|
@@ -26,22 +31,25 @@ async function dockerService(docker, item, options, environment) {
|
|
|
26
31
|
const network = (0, docker_task_1.getNeedsNetwork)(serviceContainers, item.needs);
|
|
27
32
|
item.status.write('debug', `create container with image ${item.data.image}`);
|
|
28
33
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
|
|
34
|
+
const command = (0, work_service_1.getServiceCommand)(item.data);
|
|
29
35
|
container = await docker.createContainer({
|
|
30
36
|
Image: item.data.image,
|
|
31
37
|
Env: Object.keys(envs).map((k) => `${k}=${envs[k]}`),
|
|
32
38
|
Labels: {
|
|
33
39
|
app: 'hammerkit',
|
|
34
40
|
'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
|
|
35
|
-
|
|
41
|
+
...(0, run_labels_1.getRunLabels)(),
|
|
36
42
|
'hammerkit-type': 'service',
|
|
37
43
|
'hammerkit-state': options.stateKey,
|
|
44
|
+
'hammerkit-daemon': options.daemon ? 'true' : 'false',
|
|
38
45
|
},
|
|
39
46
|
ExposedPorts: item.data.ports.reduce((map, port) => {
|
|
40
47
|
map[`${port.containerPort}/tcp`] = {};
|
|
41
48
|
return map;
|
|
42
49
|
}, {}),
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
Entrypoint: command.entrypoint ?? undefined,
|
|
51
|
+
Cmd: command.cmd ?? undefined,
|
|
52
|
+
WorkingDir: (0, execute_docker_1.convertToPosixPath)((0, work_service_1.getServiceWorkingDir)(item.data)),
|
|
45
53
|
HostConfig: {
|
|
46
54
|
ExtraHosts: network.hosts,
|
|
47
55
|
Links: network.links,
|
|
@@ -70,11 +78,20 @@ async function dockerService(docker, item, options, environment) {
|
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
80
|
else {
|
|
81
|
+
const healthcheck = item.data.healthcheck;
|
|
82
|
+
const deadline = Date.now() + healthcheck.timeout;
|
|
73
83
|
let ready = false;
|
|
74
84
|
do {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
// The first check waits one interval too, so an entrypoint that drops
|
|
86
|
+
// privileges (`exec su-exec app …`) has done so before anything runs.
|
|
87
|
+
await new Promise((resolve) => setTimeout(() => resolve(), HEALTHCHECK_INTERVAL_MS));
|
|
88
|
+
if (options.abort.aborted) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
const user = await (0, main_process_user_1.getMainProcessUser)(container);
|
|
92
|
+
ready = await (0, check_readiness_1.checkReadiness)(item.status, (0, work_service_1.getHealthcheckCommand)(item.data) ?? [], environment, container, user, options.abort);
|
|
93
|
+
if (!ready && Date.now() >= deadline) {
|
|
94
|
+
throw new Error((0, work_healthcheck_1.getHealthcheckTimeoutMessage)(item.name, healthcheck));
|
|
78
95
|
}
|
|
79
96
|
} while (!ready && !options.abort.aborted);
|
|
80
97
|
if (ready) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.dockerTask = exports.getNeedsNetwork = void 0;
|
|
3
|
+
exports.dockerTask = exports.buildCreateOptions = exports.getNeedsNetwork = void 0;
|
|
4
|
+
const run_labels_1 = require("../docker/run-labels");
|
|
4
5
|
const service_dns_1 = require("./service-dns");
|
|
5
6
|
const execute_docker_1 = require("./execute-docker");
|
|
6
7
|
const abort_1 = require("./abort");
|
|
@@ -13,6 +14,8 @@ const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
|
13
14
|
const get_container_binds_1 = require("./get-container-binds");
|
|
14
15
|
const get_service_containers_1 = require("./get-service-containers");
|
|
15
16
|
const work_instance_id_1 = require("../planner/work-instance-id");
|
|
17
|
+
const get_outputs_to_reset_1 = require("../planner/utils/get-outputs-to-reset");
|
|
18
|
+
const container_task_state_1 = require("./container-task-state");
|
|
16
19
|
function getNeedsNetwork(serviceContainers, needs) {
|
|
17
20
|
const links = [];
|
|
18
21
|
const hosts = [];
|
|
@@ -35,18 +38,22 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment) {
|
|
|
35
38
|
const network = getNeedsNetwork(serviceContainers, item.needs);
|
|
36
39
|
const binds = (0, get_container_binds_1.getContainerBinds)(item);
|
|
37
40
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
|
|
41
|
+
// Running as the host's uid (Linux), the user has no home in the image and
|
|
42
|
+
// Docker sets HOME=/, which isn't writable: tools then fail to create their
|
|
43
|
+
// caches. Set on the container only, so it's not part of the cache key.
|
|
44
|
+
const home = item.data.user && !('HOME' in envs) ? { HOME: '/tmp' } : {};
|
|
38
45
|
return {
|
|
39
46
|
abortSignal: environment.abortCtrl.signal,
|
|
40
47
|
Image: item.data.image,
|
|
41
48
|
Tty: true,
|
|
42
49
|
Entrypoint: [item.data.shell],
|
|
43
50
|
Cmd: ['-c', 'sleep 3600'],
|
|
44
|
-
Env: Object.entries(envs).map(([key, value]) => `${key}=${value}`),
|
|
51
|
+
Env: Object.entries({ ...envs, ...home }).map(([key, value]) => `${key}=${value}`),
|
|
45
52
|
WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
|
|
46
53
|
Labels: {
|
|
47
54
|
app: 'hammerkit',
|
|
48
55
|
'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
|
|
49
|
-
|
|
56
|
+
...(0, run_labels_1.getRunLabels)(),
|
|
50
57
|
'hammerkit-type': 'task',
|
|
51
58
|
'hammerkit-state': stateKey,
|
|
52
59
|
},
|
|
@@ -58,9 +65,18 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment) {
|
|
|
58
65
|
},
|
|
59
66
|
};
|
|
60
67
|
}
|
|
68
|
+
exports.buildCreateOptions = buildCreateOptions;
|
|
61
69
|
async function dockerTask(docker, item, environment, options) {
|
|
62
70
|
item.status.write('info', `execute ${item.name} in container`);
|
|
63
71
|
try {
|
|
72
|
+
// A run that fails overwrites outputs, so the previous record goes first,
|
|
73
|
+
// and only a run that succeeded writes a new one.
|
|
74
|
+
await (0, container_task_state_1.removeContainerTaskState)(environment, item);
|
|
75
|
+
const outputsToReset = (0, get_outputs_to_reset_1.getOutputsToReset)(item.data);
|
|
76
|
+
// outputs on the host: file outputs (bind mounts) and exported directories
|
|
77
|
+
for (const generate of outputsToReset.filter((g) => g.isFile || g.export)) {
|
|
78
|
+
await environment.file.remove(generate.path);
|
|
79
|
+
}
|
|
64
80
|
await (0, execution_steps_1.prepareMounts)(item, environment);
|
|
65
81
|
(0, abort_1.checkForAbort)(options.abort);
|
|
66
82
|
await (0, execution_steps_1.pullImage)(item, docker);
|
|
@@ -70,8 +86,11 @@ async function dockerTask(docker, item, environment, options) {
|
|
|
70
86
|
const serviceContainers = (0, get_service_containers_1.getServiceContainers)(item.needs);
|
|
71
87
|
const containerOptions = buildCreateOptions(item, options.stateKey, serviceContainers, environment);
|
|
72
88
|
(0, print_container_options_1.printContainerOptions)(item.status, containerOptions);
|
|
73
|
-
await (0, using_container_1.usingContainer)(docker, item, containerOptions,
|
|
89
|
+
const succeeded = await (0, using_container_1.usingContainer)(docker, item, containerOptions, async (container) => {
|
|
74
90
|
await (0, execution_steps_1.setUserPermissions)(item, container, environment);
|
|
91
|
+
for (const generate of outputsToReset.filter((g) => !g.isFile)) {
|
|
92
|
+
await (0, execute_docker_1.clearContainerDirectory)(item.status, environment, container, generate.path);
|
|
93
|
+
}
|
|
75
94
|
for (const cmd of item.data.cmds) {
|
|
76
95
|
(0, abort_1.checkForAbort)(options.abort);
|
|
77
96
|
item.status.write('info', `execute cmd "${cmd.cmd}" in container`);
|
|
@@ -112,6 +131,9 @@ async function dockerTask(docker, item, environment, options) {
|
|
|
112
131
|
}
|
|
113
132
|
return true;
|
|
114
133
|
});
|
|
134
|
+
if (succeeded) {
|
|
135
|
+
await (0, container_task_state_1.writeContainerTaskState)(environment, item, options.stateKey);
|
|
136
|
+
}
|
|
115
137
|
}
|
|
116
138
|
catch (e) {
|
|
117
139
|
if (e instanceof abort_1.AbortError) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.cleanCache = exports.storeCache = exports.getArchivePaths = exports.restoreCache = void 0;
|
|
4
|
+
const remove_orphaned_containers_1 = require("../docker/remove-orphaned-containers");
|
|
5
|
+
const execute_docker_1 = require("./execute-docker");
|
|
4
6
|
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
5
7
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
6
8
|
const path_1 = require("path");
|
|
@@ -90,6 +92,12 @@ async function cleanCache(workTree, environment, options) {
|
|
|
90
92
|
await service.data.caching.backend.clear(service.id(), environment);
|
|
91
93
|
}
|
|
92
94
|
}
|
|
95
|
+
if (workTree.environment.type === 'docker') {
|
|
96
|
+
const removed = await (0, remove_orphaned_containers_1.removeOrphanedContainers)((0, execute_docker_1.getContainerCli)(workTree.environment));
|
|
97
|
+
for (const id of removed) {
|
|
98
|
+
environment.console.info(`removed container ${id.substring(0, 12)} left behind by a stopped run`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
exports.cleanCache = cleanCache;
|
|
95
103
|
//# sourceMappingURL=event-cache.js.map
|
|
@@ -6,6 +6,12 @@ const os_1 = require("os");
|
|
|
6
6
|
const log_1 = require("../log");
|
|
7
7
|
const abort_1 = require("./abort");
|
|
8
8
|
const abort_event_1 = require("../utils/abort-event");
|
|
9
|
+
const environment_config_1 = require("../utils/environment-config");
|
|
10
|
+
// how long a local command's process group gets to exit after SIGTERM before
|
|
11
|
+
// it is killed
|
|
12
|
+
function getStopTimeout() {
|
|
13
|
+
return (0, environment_config_1.getEnvironmentConfig)('HAMMERKIT_STOP_TIMEOUT_MS', 10000);
|
|
14
|
+
}
|
|
9
15
|
async function executeCommand(status, abortSignal, cwd, command, envs, environment) {
|
|
10
16
|
const windows = (0, os_1.platform)() === 'win32';
|
|
11
17
|
return new Promise((resolve, reject) => {
|
|
@@ -48,14 +54,38 @@ async function executeCommand(status, abortSignal, cwd, command, envs, environme
|
|
|
48
54
|
ps.kill();
|
|
49
55
|
return;
|
|
50
56
|
}
|
|
51
|
-
|
|
52
|
-
process.kill(-ps.pid, 'SIGTERM');
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// the group is already gone
|
|
56
|
-
}
|
|
57
|
+
stopGroup(ps.pid);
|
|
57
58
|
});
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
exports.executeCommand = executeCommand;
|
|
62
|
+
// SIGTERM the whole group, then SIGKILL whatever is still in it after
|
|
63
|
+
// HAMMERKIT_STOP_TIMEOUT_MS. The shell exiting doesn't end the wait: a child that ignores
|
|
64
|
+
// SIGTERM would otherwise outlive hammerkit.
|
|
65
|
+
function stopGroup(pid) {
|
|
66
|
+
if (!signalGroup(pid, 'SIGTERM')) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const deadline = Date.now() + getStopTimeout();
|
|
70
|
+
const poll = setInterval(() => {
|
|
71
|
+
if (!signalGroup(pid, 0)) {
|
|
72
|
+
clearInterval(poll);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (Date.now() >= deadline) {
|
|
76
|
+
signalGroup(pid, 'SIGKILL');
|
|
77
|
+
clearInterval(poll);
|
|
78
|
+
}
|
|
79
|
+
}, 100);
|
|
80
|
+
}
|
|
81
|
+
function signalGroup(pid, signal) {
|
|
82
|
+
try {
|
|
83
|
+
process.kill(-pid, signal);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// the group is already gone
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
61
91
|
//# sourceMappingURL=execute-command.js.map
|
|
@@ -3,16 +3,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.execCommand = exports.convertToPosixPath = exports.startContainer = exports.getContainerCli = void 0;
|
|
6
|
+
exports.clearContainerDirectory = exports.execCommand = exports.convertToPosixPath = exports.startContainer = exports.getContainerCli = exports.createDockerClient = void 0;
|
|
7
7
|
const stream_1 = require("../docker/stream");
|
|
8
8
|
const dockerode_1 = __importDefault(require("dockerode"));
|
|
9
9
|
const path_1 = require("path");
|
|
10
10
|
const os_1 = require("os");
|
|
11
11
|
const abort_event_1 = require("../utils/abort-event");
|
|
12
12
|
let dockerInstance = null;
|
|
13
|
+
// Without options dockerode reads DOCKER_HOST and the TLS variables; passing
|
|
14
|
+
// `{ host: undefined }` would overwrite the host it parsed from DOCKER_HOST.
|
|
15
|
+
function createDockerClient(host) {
|
|
16
|
+
return host ? new dockerode_1.default({ host }) : new dockerode_1.default();
|
|
17
|
+
}
|
|
18
|
+
exports.createDockerClient = createDockerClient;
|
|
13
19
|
function getContainerCli(workEnvironment) {
|
|
14
20
|
if (!dockerInstance) {
|
|
15
|
-
dockerInstance =
|
|
21
|
+
dockerInstance = createDockerClient(workEnvironment.host);
|
|
16
22
|
}
|
|
17
23
|
return dockerInstance;
|
|
18
24
|
}
|
|
@@ -127,4 +133,14 @@ async function execCommand(status, environment, container, cwd, cmd, user, timeo
|
|
|
127
133
|
});
|
|
128
134
|
}
|
|
129
135
|
exports.execCommand = execCommand;
|
|
136
|
+
// Empty a directory inside a running container, as root. Volumes are emptied
|
|
137
|
+
// this way rather than recreated: Docker refuses to remove a volume while any
|
|
138
|
+
// container (a running dependent's, say) still mounts it.
|
|
139
|
+
async function clearContainerDirectory(status, environment, container, path) {
|
|
140
|
+
const result = await execCommand(status, environment, container, '/', ['sh', '-c', 'rm -rf "$1"/* "$1"/.[!.]* "$1"/..?*', 'sh', convertToPosixPath(path)], null, undefined, environment.abortCtrl.signal);
|
|
141
|
+
if (result.type !== 'result' || result.result.ExitCode !== 0) {
|
|
142
|
+
throw new Error(`unable to empty ${path}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.clearContainerDirectory = clearContainerDirectory;
|
|
130
146
|
//# sourceMappingURL=execute-docker.js.map
|
|
@@ -32,8 +32,12 @@ async function executeWorkService(work, environment, options) {
|
|
|
32
32
|
});
|
|
33
33
|
await (0, await_completed_dependencies_1.awaitRunningNeeds)(work, work.needs.map((n) => n.service), abort);
|
|
34
34
|
if (options.type === 'execute') {
|
|
35
|
+
// rejects when the run is aborted; unhandled, that rejection would
|
|
36
|
+
// crash hammerkit before the service container is removed
|
|
35
37
|
(0, await_completed_dependencies_1.awaitNoRequirements)(work, abort).then(() => {
|
|
36
38
|
stop();
|
|
39
|
+
}, () => {
|
|
40
|
+
// aborted: the service is stopping anyway
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
43
|
await work.runtime.execute(environment, {
|