hammerkit 1.10.0 → 1.11.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 +52 -1
- package/dist/cli.js +1 -0
- package/dist/executer/container-secrets.js +59 -0
- package/dist/executer/deadline.js +29 -0
- package/dist/executer/docker-service.js +58 -38
- package/dist/executer/docker-task.js +37 -23
- package/dist/executer/execute-init-task.js +100 -0
- package/dist/executer/execute-work-service.js +30 -1
- package/dist/executer/execute-work-task.js +4 -25
- package/dist/executer/execute-work-tree.js +28 -2
- package/dist/executer/get-container-binds.js +8 -4
- package/dist/executer/local-task.js +6 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +13 -7
- package/dist/kubernetes/secrets.js +83 -0
- package/dist/kubernetes/volumes.js +1 -1
- package/dist/optimizer/work-task-cache-description.js +6 -0
- package/dist/parse-set-arguments.js +38 -0
- package/dist/planner/service-definition.js +40 -0
- package/dist/planner/utils/append-work-dependencies.js +2 -0
- package/dist/planner/utils/append-work-needs.js +7 -0
- package/dist/planner/utils/append-work-service.js +67 -1
- package/dist/planner/utils/append-work-task.js +6 -0
- package/dist/planner/utils/parse-work-generate.js +4 -2
- package/dist/planner/utils/parse-work-mount.js +7 -3
- package/dist/planner/utils/parse-work-volume.js +13 -5
- package/dist/planner/utils/plan-work-volume.js +1 -0
- package/dist/planner/utils/service-hostname.js +15 -0
- package/dist/planner/work-item-status.js +3 -1
- package/dist/planner/work-runtime-docker.js +8 -0
- package/dist/planner/work-runtime-kubernetes.js +40 -6
- package/dist/planner/work-secret.js +83 -0
- package/dist/program.js +9 -1
- package/dist/schema/build-file-base-task-schema.js +2 -0
- package/dist/schema/build-file-container-service-schema.js +5 -0
- package/dist/schema/build-file-secret-schema.js +22 -0
- package/dist/schema/build-file-volume-schema.js +3 -1
- package/dist/schema/reference-parser.js +14 -0
- package/dist/schema/work-scope-parser.js +17 -5
- package/dist/utils/redact.js +29 -0
- package/package.json +1 -1
package/build.schema.json
CHANGED
|
@@ -141,7 +141,15 @@
|
|
|
141
141
|
"type": "object",
|
|
142
142
|
"properties": {
|
|
143
143
|
"export": {
|
|
144
|
-
"
|
|
144
|
+
"anyOf": [
|
|
145
|
+
{
|
|
146
|
+
"type": "boolean"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"type": "string",
|
|
150
|
+
"const": "always"
|
|
151
|
+
}
|
|
152
|
+
]
|
|
145
153
|
},
|
|
146
154
|
"resetOnChange": {
|
|
147
155
|
"type": "boolean"
|
|
@@ -151,6 +159,9 @@
|
|
|
151
159
|
},
|
|
152
160
|
"name": {
|
|
153
161
|
"type": "string"
|
|
162
|
+
},
|
|
163
|
+
"readOnly": {
|
|
164
|
+
"type": "boolean"
|
|
154
165
|
}
|
|
155
166
|
},
|
|
156
167
|
"required": [
|
|
@@ -170,6 +181,33 @@
|
|
|
170
181
|
"timeout": {
|
|
171
182
|
"type": "string"
|
|
172
183
|
},
|
|
184
|
+
"secrets": {
|
|
185
|
+
"type": "array",
|
|
186
|
+
"items": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"properties": {
|
|
189
|
+
"from": {
|
|
190
|
+
"type": "string",
|
|
191
|
+
"pattern": "^(env|file):.+$"
|
|
192
|
+
},
|
|
193
|
+
"env": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
196
|
+
},
|
|
197
|
+
"path": {
|
|
198
|
+
"type": "string"
|
|
199
|
+
},
|
|
200
|
+
"cache": {
|
|
201
|
+
"type": "boolean"
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
"required": [
|
|
205
|
+
"from"
|
|
206
|
+
],
|
|
207
|
+
"additionalProperties": false,
|
|
208
|
+
"description": "secret from env:NAME or file:path, injected as env or a read-only file"
|
|
209
|
+
}
|
|
210
|
+
},
|
|
173
211
|
"image": {
|
|
174
212
|
"type": "string"
|
|
175
213
|
},
|
|
@@ -228,6 +266,9 @@
|
|
|
228
266
|
"timeout": {
|
|
229
267
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/timeout"
|
|
230
268
|
},
|
|
269
|
+
"secrets": {
|
|
270
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/secrets"
|
|
271
|
+
},
|
|
231
272
|
"platform": {
|
|
232
273
|
"type": "object",
|
|
233
274
|
"properties": {
|
|
@@ -341,6 +382,16 @@
|
|
|
341
382
|
"cmd"
|
|
342
383
|
],
|
|
343
384
|
"additionalProperties": false
|
|
385
|
+
},
|
|
386
|
+
"init": {
|
|
387
|
+
"type": "string",
|
|
388
|
+
"description": "task run once the service is healthy, before dependents start"
|
|
389
|
+
},
|
|
390
|
+
"secrets": {
|
|
391
|
+
"type": "array",
|
|
392
|
+
"items": {
|
|
393
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/secrets/items"
|
|
394
|
+
}
|
|
344
395
|
}
|
|
345
396
|
},
|
|
346
397
|
"required": [
|
package/dist/cli.js
CHANGED
|
@@ -57,6 +57,7 @@ class Cli {
|
|
|
57
57
|
cacheReadOnly: options?.cacheReadOnly ?? false,
|
|
58
58
|
timeout: options?.timeout ?? null,
|
|
59
59
|
skipDeps: options?.skipDeps ?? true,
|
|
60
|
+
wait: options?.wait ?? 'ready',
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
63
|
const result = (0, get_scheduler_execute_result_1.getSchedulerExecuteResult)(workTree);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeContainerSecrets = exports.resolveSecretEnvs = exports.prepareContainerSecrets = exports.getSecretDirectory = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const promises_1 = require("fs/promises");
|
|
6
|
+
const work_secret_1 = require("../planner/work-secret");
|
|
7
|
+
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
8
|
+
const execute_docker_1 = require("./execute-docker");
|
|
9
|
+
// Where values of env-sourced file targets are written while their container
|
|
10
|
+
// runs: owner-only, outside the project, removed with the container.
|
|
11
|
+
function getSecretDirectory(instanceId) {
|
|
12
|
+
return (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'secrets', instanceId);
|
|
13
|
+
}
|
|
14
|
+
exports.getSecretDirectory = getSecretDirectory;
|
|
15
|
+
// Resolve every secret before the container starts, so a missing source fails
|
|
16
|
+
// naming the secret and nothing has run yet.
|
|
17
|
+
async function prepareContainerSecrets(secrets, instanceId, environment) {
|
|
18
|
+
const result = { env: {}, binds: [] };
|
|
19
|
+
for (const secret of secrets) {
|
|
20
|
+
if (secret.target.type === 'env') {
|
|
21
|
+
result.env[secret.target.name] = await (0, work_secret_1.resolveSecretValue)(secret, environment);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (secret.source.type === 'file') {
|
|
25
|
+
await (0, work_secret_1.resolveSecretValue)(secret, environment);
|
|
26
|
+
result.binds.push(`${secret.source.path}:${(0, execute_docker_1.convertToPosixPath)(secret.target.path)}:ro`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const directory = getSecretDirectory(instanceId);
|
|
30
|
+
await (0, promises_1.mkdir)(directory, { recursive: true, mode: 0o700 });
|
|
31
|
+
await (0, promises_1.chmod)(directory, 0o700);
|
|
32
|
+
const file = (0, path_1.join)(directory, getSecretFileName(secret));
|
|
33
|
+
await (0, promises_1.writeFile)(file, await (0, work_secret_1.resolveSecretValue)(secret, environment), { mode: 0o600 });
|
|
34
|
+
// the mode only applies on create, a leftover file is tightened too
|
|
35
|
+
await (0, promises_1.chmod)(file, 0o600);
|
|
36
|
+
result.binds.push(`${file}:${(0, execute_docker_1.convertToPosixPath)(secret.target.path)}:ro`);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
exports.prepareContainerSecrets = prepareContainerSecrets;
|
|
41
|
+
// A local task only takes env targets, file targets are refused when planning.
|
|
42
|
+
async function resolveSecretEnvs(secrets, environment) {
|
|
43
|
+
const envs = {};
|
|
44
|
+
for (const secret of secrets) {
|
|
45
|
+
if (secret.target.type === 'env') {
|
|
46
|
+
envs[secret.target.name] = await (0, work_secret_1.resolveSecretValue)(secret, environment);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return envs;
|
|
50
|
+
}
|
|
51
|
+
exports.resolveSecretEnvs = resolveSecretEnvs;
|
|
52
|
+
async function removeContainerSecrets(instanceId) {
|
|
53
|
+
await (0, promises_1.rm)(getSecretDirectory(instanceId), { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
exports.removeContainerSecrets = removeContainerSecrets;
|
|
56
|
+
function getSecretFileName(secret) {
|
|
57
|
+
return (0, work_secret_1.getSecretName)(secret).replace(/[^A-Za-z0-9._-]+/g, '_');
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=container-secrets.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withDeadline = void 0;
|
|
4
|
+
const abort_event_1 = require("../utils/abort-event");
|
|
5
|
+
// An abort signal that follows `parent` and additionally fires after `timeout`
|
|
6
|
+
// ms, so a task deadline reuses the runtimes' existing abort/cleanup path.
|
|
7
|
+
function withDeadline(parent, timeout) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
let expired = false;
|
|
10
|
+
const parentListener = (0, abort_event_1.listenOnAbort)(parent, () => controller.abort());
|
|
11
|
+
const timer = timeout === null
|
|
12
|
+
? null
|
|
13
|
+
: setTimeout(() => {
|
|
14
|
+
expired = true;
|
|
15
|
+
controller.abort();
|
|
16
|
+
}, timeout);
|
|
17
|
+
return {
|
|
18
|
+
signal: controller.signal,
|
|
19
|
+
expired: () => expired,
|
|
20
|
+
clear() {
|
|
21
|
+
if (timer) {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
parentListener.close();
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
exports.withDeadline = withDeadline;
|
|
29
|
+
//# sourceMappingURL=deadline.js.map
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.dockerService = void 0;
|
|
3
|
+
exports.dockerService = exports.buildServiceCreateOptions = void 0;
|
|
4
4
|
const work_service_1 = require("../planner/work-service");
|
|
5
5
|
const run_labels_1 = require("../docker/run-labels");
|
|
6
|
+
const service_hostname_1 = require("../planner/utils/service-hostname");
|
|
7
|
+
const service_definition_1 = require("../planner/service-definition");
|
|
6
8
|
const abort_1 = require("./abort");
|
|
7
9
|
const execute_docker_1 = require("./execute-docker");
|
|
8
10
|
const stream_1 = require("../docker/stream");
|
|
@@ -13,11 +15,59 @@ const check_readiness_1 = require("./check-readiness");
|
|
|
13
15
|
const main_process_user_1 = require("./main-process-user");
|
|
14
16
|
const execution_steps_1 = require("./execution-steps");
|
|
15
17
|
const docker_task_1 = require("./docker-task");
|
|
18
|
+
const container_secrets_1 = require("./container-secrets");
|
|
16
19
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
17
20
|
const get_service_containers_1 = require("./get-service-containers");
|
|
18
21
|
const work_instance_id_1 = require("../planner/work-instance-id");
|
|
19
22
|
const work_healthcheck_1 = require("../planner/work-healthcheck");
|
|
20
23
|
const HEALTHCHECK_INTERVAL_MS = 1000;
|
|
24
|
+
function buildServiceCreateOptions(item, options, network, secrets) {
|
|
25
|
+
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
|
|
26
|
+
const command = (0, work_service_1.getServiceCommand)(item.data);
|
|
27
|
+
return {
|
|
28
|
+
Image: item.data.image,
|
|
29
|
+
Hostname: (0, service_hostname_1.getServiceHostname)(item.name),
|
|
30
|
+
Env: Object.entries({ ...envs, ...secrets.env }).map(([key, value]) => `${key}=${value}`),
|
|
31
|
+
Labels: {
|
|
32
|
+
app: 'hammerkit',
|
|
33
|
+
'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
|
|
34
|
+
...(0, run_labels_1.getRunLabels)(),
|
|
35
|
+
'hammerkit-type': 'service',
|
|
36
|
+
'hammerkit-state': options.stateKey,
|
|
37
|
+
'hammerkit-daemon': options.daemon ? 'true' : 'false',
|
|
38
|
+
'hammerkit-definition': (0, service_definition_1.getServiceDefinitionHash)(item),
|
|
39
|
+
},
|
|
40
|
+
ExposedPorts: item.data.ports.reduce((map, port) => {
|
|
41
|
+
map[`${port.containerPort}/tcp`] = {};
|
|
42
|
+
return map;
|
|
43
|
+
}, {}),
|
|
44
|
+
Entrypoint: command.entrypoint ?? undefined,
|
|
45
|
+
Cmd: command.cmd ?? undefined,
|
|
46
|
+
WorkingDir: (0, execute_docker_1.convertToPosixPath)((0, work_service_1.getServiceWorkingDir)(item.data)),
|
|
47
|
+
HostConfig: {
|
|
48
|
+
ExtraHosts: network.hosts,
|
|
49
|
+
Links: network.links,
|
|
50
|
+
Binds: [
|
|
51
|
+
...item.data.src.map((s) => `${s.absolutePath}:${s.absolutePath}`),
|
|
52
|
+
...item.data.mounts.map((v) => `${v.localPath}:${(0, execute_docker_1.convertToPosixPath)(v.containerPath)}${v.readOnly ? ':ro' : ''}`),
|
|
53
|
+
...item.data.volumes.map((v) => `${v.name}:${(0, execute_docker_1.convertToPosixPath)(v.containerPath)}${v.readOnly ? ':ro' : ''}`),
|
|
54
|
+
...secrets.binds,
|
|
55
|
+
],
|
|
56
|
+
// host ports are for `hammerkit up` and local tasks; container tasks
|
|
57
|
+
// reach the service over its link, and a run publishing them would
|
|
58
|
+
// collide with another run on the same host
|
|
59
|
+
PortBindings: options.publishPorts
|
|
60
|
+
? item.data.ports
|
|
61
|
+
.filter((p) => !!p.hostPort)
|
|
62
|
+
.reduce((map, port) => {
|
|
63
|
+
map[`${port.containerPort}/tcp`] = [{ HostPort: `${port.hostPort}` }];
|
|
64
|
+
return map;
|
|
65
|
+
}, {})
|
|
66
|
+
: {},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
exports.buildServiceCreateOptions = buildServiceCreateOptions;
|
|
21
71
|
async function dockerService(docker, item, options, environment) {
|
|
22
72
|
let container = null;
|
|
23
73
|
try {
|
|
@@ -30,46 +80,12 @@ async function dockerService(docker, item, options, environment) {
|
|
|
30
80
|
const serviceContainers = (0, get_service_containers_1.getServiceContainers)(item.needs);
|
|
31
81
|
const network = (0, docker_task_1.getNeedsNetwork)(serviceContainers, item.needs);
|
|
32
82
|
item.status.write('debug', `create container with image ${item.data.image}`);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
container = await docker.createContainer({
|
|
36
|
-
Image: item.data.image,
|
|
37
|
-
Env: Object.keys(envs).map((k) => `${k}=${envs[k]}`),
|
|
38
|
-
Labels: {
|
|
39
|
-
app: 'hammerkit',
|
|
40
|
-
'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
|
|
41
|
-
...(0, run_labels_1.getRunLabels)(),
|
|
42
|
-
'hammerkit-type': 'service',
|
|
43
|
-
'hammerkit-state': options.stateKey,
|
|
44
|
-
'hammerkit-daemon': options.daemon ? 'true' : 'false',
|
|
45
|
-
},
|
|
46
|
-
ExposedPorts: item.data.ports.reduce((map, port) => {
|
|
47
|
-
map[`${port.containerPort}/tcp`] = {};
|
|
48
|
-
return map;
|
|
49
|
-
}, {}),
|
|
50
|
-
Entrypoint: command.entrypoint ?? undefined,
|
|
51
|
-
Cmd: command.cmd ?? undefined,
|
|
52
|
-
WorkingDir: (0, execute_docker_1.convertToPosixPath)((0, work_service_1.getServiceWorkingDir)(item.data)),
|
|
53
|
-
HostConfig: {
|
|
54
|
-
ExtraHosts: network.hosts,
|
|
55
|
-
Links: network.links,
|
|
56
|
-
Binds: [
|
|
57
|
-
...item.data.src.map((s) => `${s.absolutePath}:${s.absolutePath}`),
|
|
58
|
-
...item.data.mounts.map((v) => `${v.localPath}:${(0, execute_docker_1.convertToPosixPath)(v.containerPath)}`),
|
|
59
|
-
...item.data.volumes.map((v) => `${v.name}:${(0, execute_docker_1.convertToPosixPath)(v.containerPath)}`),
|
|
60
|
-
],
|
|
61
|
-
PortBindings: item.data.ports
|
|
62
|
-
.filter((p) => !!p.hostPort)
|
|
63
|
-
.reduce((map, port) => {
|
|
64
|
-
map[`${port.containerPort}/tcp`] = [{ HostPort: `${port.hostPort}` }];
|
|
65
|
-
return map;
|
|
66
|
-
}, {}),
|
|
67
|
-
},
|
|
68
|
-
});
|
|
83
|
+
const secrets = await (0, container_secrets_1.prepareContainerSecrets)(item.data.secrets, (0, work_instance_id_1.getWorkInstanceId)(item), environment);
|
|
84
|
+
container = await docker.createContainer(buildServiceCreateOptions(item, options, network, secrets));
|
|
69
85
|
const stream = await container.attach({ stream: true, stdout: true, stderr: true });
|
|
70
86
|
(0, stream_1.logStream)(item.status, stream);
|
|
71
87
|
await container.start();
|
|
72
|
-
if (!item.data.healthcheck) {
|
|
88
|
+
if (!item.data.healthcheck || !options.waitForReady) {
|
|
73
89
|
options.state.set({
|
|
74
90
|
type: 'running',
|
|
75
91
|
dns: { containerId: container.id },
|
|
@@ -149,6 +165,10 @@ async function dockerService(docker, item, options, environment) {
|
|
|
149
165
|
item.status.write('error', `remove of container failed ${(0, log_1.getErrorMessage)(e)}`);
|
|
150
166
|
}
|
|
151
167
|
}
|
|
168
|
+
// a daemon keeps its secret files until `hammerkit down` stops it
|
|
169
|
+
if (!options.daemon) {
|
|
170
|
+
await (0, container_secrets_1.removeContainerSecrets)((0, work_instance_id_1.getWorkInstanceId)(item));
|
|
171
|
+
}
|
|
152
172
|
}
|
|
153
173
|
}
|
|
154
174
|
exports.dockerService = dockerService;
|
|
@@ -15,6 +15,7 @@ const get_container_binds_1 = require("./get-container-binds");
|
|
|
15
15
|
const get_service_containers_1 = require("./get-service-containers");
|
|
16
16
|
const work_instance_id_1 = require("../planner/work-instance-id");
|
|
17
17
|
const get_outputs_to_reset_1 = require("../planner/utils/get-outputs-to-reset");
|
|
18
|
+
const container_secrets_1 = require("./container-secrets");
|
|
18
19
|
const container_task_state_1 = require("./container-task-state");
|
|
19
20
|
function getNeedsNetwork(serviceContainers, needs) {
|
|
20
21
|
const links = [];
|
|
@@ -34,7 +35,7 @@ function getNeedsNetwork(serviceContainers, needs) {
|
|
|
34
35
|
return { links, hosts };
|
|
35
36
|
}
|
|
36
37
|
exports.getNeedsNetwork = getNeedsNetwork;
|
|
37
|
-
function buildCreateOptions(item, stateKey, serviceContainers, environment) {
|
|
38
|
+
function buildCreateOptions(item, stateKey, serviceContainers, environment, secrets) {
|
|
38
39
|
const network = getNeedsNetwork(serviceContainers, item.needs);
|
|
39
40
|
const binds = (0, get_container_binds_1.getContainerBinds)(item);
|
|
40
41
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
|
|
@@ -48,7 +49,7 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment) {
|
|
|
48
49
|
Tty: true,
|
|
49
50
|
Entrypoint: [item.data.shell],
|
|
50
51
|
Cmd: ['-c', 'sleep 3600'],
|
|
51
|
-
Env: Object.entries({ ...envs, ...home }).map(([key, value]) => `${key}=${value}`),
|
|
52
|
+
Env: Object.entries({ ...envs, ...home, ...secrets.env }).map(([key, value]) => `${key}=${value}`),
|
|
52
53
|
WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
|
|
53
54
|
Labels: {
|
|
54
55
|
app: 'hammerkit',
|
|
@@ -58,7 +59,10 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment) {
|
|
|
58
59
|
'hammerkit-state': stateKey,
|
|
59
60
|
},
|
|
60
61
|
HostConfig: {
|
|
61
|
-
Binds:
|
|
62
|
+
Binds: [
|
|
63
|
+
...binds.map((b) => `${b.localPath}:${(0, execute_docker_1.convertToPosixPath)(b.containerPath)}${b.readOnly ? ':ro' : ''}`),
|
|
64
|
+
...secrets.binds,
|
|
65
|
+
],
|
|
62
66
|
ExtraHosts: network.hosts,
|
|
63
67
|
Links: network.links,
|
|
64
68
|
AutoRemove: true,
|
|
@@ -84,7 +88,8 @@ async function dockerTask(docker, item, environment, options) {
|
|
|
84
88
|
await (0, execution_steps_1.prepareVolume)(item, docker);
|
|
85
89
|
(0, abort_1.checkForAbort)(options.abort);
|
|
86
90
|
const serviceContainers = (0, get_service_containers_1.getServiceContainers)(item.needs);
|
|
87
|
-
const
|
|
91
|
+
const secrets = await (0, container_secrets_1.prepareContainerSecrets)(item.data.secrets, (0, work_instance_id_1.getWorkInstanceId)(item), environment);
|
|
92
|
+
const containerOptions = buildCreateOptions(item, options.stateKey, serviceContainers, environment, secrets);
|
|
88
93
|
(0, print_container_options_1.printContainerOptions)(item.status, containerOptions);
|
|
89
94
|
const succeeded = await (0, using_container_1.usingContainer)(docker, item, containerOptions, async (container) => {
|
|
90
95
|
await (0, execution_steps_1.setUserPermissions)(item, container, environment);
|
|
@@ -102,6 +107,7 @@ async function dockerTask(docker, item, environment, options) {
|
|
|
102
107
|
throw new abort_1.AbortError();
|
|
103
108
|
}
|
|
104
109
|
if (result.result.ExitCode !== 0) {
|
|
110
|
+
await exportGenerates(environment, container, item.data.generates.filter((g) => g.exportAlways));
|
|
105
111
|
options.state.set({
|
|
106
112
|
stateKey: options.stateKey,
|
|
107
113
|
type: 'crash',
|
|
@@ -110,25 +116,7 @@ async function dockerTask(docker, item, environment, options) {
|
|
|
110
116
|
return false;
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
|
-
|
|
114
|
-
if (!generate.export || generate.inherited || generate.isFile) {
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
const readable = await container.getArchive({
|
|
118
|
-
path: generate.path,
|
|
119
|
-
});
|
|
120
|
-
await environment.file.createDirectory(generate.path);
|
|
121
|
-
await new Promise((resolve, reject) => {
|
|
122
|
-
readable
|
|
123
|
-
.pipe((0, tar_1.extract)({
|
|
124
|
-
cwd: generate.path,
|
|
125
|
-
newer: true,
|
|
126
|
-
stripComponents: 1,
|
|
127
|
-
}))
|
|
128
|
-
.on('close', () => resolve())
|
|
129
|
-
.on('error', (err) => reject(err));
|
|
130
|
-
});
|
|
131
|
-
}
|
|
119
|
+
await exportGenerates(environment, container, item.data.generates.filter((g) => g.export));
|
|
132
120
|
return true;
|
|
133
121
|
});
|
|
134
122
|
if (succeeded) {
|
|
@@ -150,6 +138,32 @@ async function dockerTask(docker, item, environment, options) {
|
|
|
150
138
|
});
|
|
151
139
|
}
|
|
152
140
|
}
|
|
141
|
+
finally {
|
|
142
|
+
await (0, container_secrets_1.removeContainerSecrets)((0, work_instance_id_1.getWorkInstanceId)(item));
|
|
143
|
+
}
|
|
153
144
|
}
|
|
154
145
|
exports.dockerTask = dockerTask;
|
|
146
|
+
// Copy exported directory outputs out of the container to the host. File
|
|
147
|
+
// outputs are bind mounts and inherited outputs belong to their task.
|
|
148
|
+
async function exportGenerates(environment, container, generates) {
|
|
149
|
+
for (const generate of generates) {
|
|
150
|
+
if (generate.inherited || generate.isFile) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const readable = await container.getArchive({
|
|
154
|
+
path: generate.path,
|
|
155
|
+
});
|
|
156
|
+
await environment.file.createDirectory(generate.path);
|
|
157
|
+
await new Promise((resolve, reject) => {
|
|
158
|
+
readable
|
|
159
|
+
.pipe((0, tar_1.extract)({
|
|
160
|
+
cwd: generate.path,
|
|
161
|
+
newer: true,
|
|
162
|
+
stripComponents: 1,
|
|
163
|
+
}))
|
|
164
|
+
.on('close', () => resolve())
|
|
165
|
+
.on('error', (err) => reject(err));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
155
169
|
//# sourceMappingURL=docker-task.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findInitOf = exports.executeInitTask = exports.isTerminalTask = void 0;
|
|
4
|
+
const state_resolver_1 = require("./state-resolver");
|
|
5
|
+
const execute_work_task_1 = require("./execute-work-task");
|
|
6
|
+
const abort_1 = require("./abort");
|
|
7
|
+
const abort_event_1 = require("../utils/abort-event");
|
|
8
|
+
const isTerminalService = (state) => state.type === 'end' || state.type === 'error' || state.type === 'canceled';
|
|
9
|
+
const isTerminalTask = (state) => state.type === 'completed' || state.type === 'error' || state.type === 'crash' || state.type === 'canceled';
|
|
10
|
+
exports.isTerminalTask = isTerminalTask;
|
|
11
|
+
// The init task of a service runs each time the service passed its healthcheck
|
|
12
|
+
// in this run, never on its own: a service left running by an earlier `up` ran
|
|
13
|
+
// its init when it started, so the init is skipped unless it was asked for by
|
|
14
|
+
// name. A failing init fails the run like any failing task.
|
|
15
|
+
async function executeInitTask(work, init, service, requested, environment, options) {
|
|
16
|
+
if (!service || service.state.current.type === 'running') {
|
|
17
|
+
if (service && requested) {
|
|
18
|
+
await (0, execute_work_task_1.executeWorkTask)(work, environment, options);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
skip(work, `${work.name} skipped, the service it initializes does not start in this run`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (requested) {
|
|
25
|
+
// asked for by name, the init is what the service starts for
|
|
26
|
+
work.state.set({ type: 'ready', stateKey: '', started: new Date() });
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
do {
|
|
30
|
+
const view = await (0, state_resolver_1.isState)('await-init-' + work.name, init.state, (state) => state.type === 'running' || isTerminalService(state), environment.abortCtrl.signal);
|
|
31
|
+
if (view.type !== 'running') {
|
|
32
|
+
if (!(0, exports.isTerminalTask)(work.state.current)) {
|
|
33
|
+
skip(work, `${work.name} skipped, ${service.name} did not start`);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await runOnce(work, environment, options);
|
|
38
|
+
settleService(service, work, view);
|
|
39
|
+
if (!options.watch) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await (0, state_resolver_1.awaitState)('await-init-restart-' + work.name, init.state, (state) => state.type !== 'running', environment.abortCtrl.signal);
|
|
43
|
+
} while (!environment.abortCtrl.signal.aborted);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (!(e instanceof abort_1.AbortError)) {
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
if (!(0, exports.isTerminalTask)(work.state.current)) {
|
|
50
|
+
work.state.set({ type: 'canceled', stateKey: null });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.executeInitTask = executeInitTask;
|
|
55
|
+
// Only now does the service run for everything else needing it.
|
|
56
|
+
function settleService(service, work, view) {
|
|
57
|
+
const result = work.state.current;
|
|
58
|
+
if (result.type === 'completed') {
|
|
59
|
+
service.state.set(view);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
service.state.set({
|
|
63
|
+
type: 'error',
|
|
64
|
+
stateKey: null,
|
|
65
|
+
errorMessage: `init ${work.name} did not succeed (${result.type})`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// One run of the init as a plain task. It aborts its own run when it fails;
|
|
69
|
+
// the whole run stops only outside watch mode, as for a failing service.
|
|
70
|
+
async function runOnce(work, environment, options) {
|
|
71
|
+
const abortCtrl = new AbortController();
|
|
72
|
+
const abortListener = (0, abort_event_1.listenOnAbort)(environment.abortCtrl.signal, () => abortCtrl.abort());
|
|
73
|
+
try {
|
|
74
|
+
await (0, execute_work_task_1.executeWorkTask)(work, { ...environment, abortCtrl }, { ...options, watch: false });
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
abortListener.close();
|
|
78
|
+
}
|
|
79
|
+
const result = work.state.current.type;
|
|
80
|
+
if ((result === 'error' || result === 'crash') && !options.watch) {
|
|
81
|
+
environment.abortCtrl.abort();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function skip(work, message) {
|
|
85
|
+
work.status.write('debug', message);
|
|
86
|
+
work.state.set({ type: 'completed', cached: false, skipped: true, duration: 0, stateKey: '' });
|
|
87
|
+
}
|
|
88
|
+
// The service a task is the init of: the task needs it through the view the
|
|
89
|
+
// planner gave it, which shares the service's data.
|
|
90
|
+
function findInitOf(task) {
|
|
91
|
+
for (const need of task.needs) {
|
|
92
|
+
const data = need.service.data;
|
|
93
|
+
if (data.type === 'container-service' && data.init && data.init.task === task) {
|
|
94
|
+
return { serviceName: need.service.name, init: data.init };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
exports.findInitOf = findInitOf;
|
|
100
|
+
//# sourceMappingURL=execute-init-task.js.map
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.executeWorkService = exports.stopService = void 0;
|
|
4
|
+
const work_item_1 = require("../planner/work-item");
|
|
4
5
|
const await_completed_dependencies_1 = require("./await-completed-dependencies");
|
|
5
6
|
const abort_1 = require("./abort");
|
|
6
7
|
const log_1 = require("../log");
|
|
7
8
|
const watch_loop_1 = require("./watch-loop");
|
|
9
|
+
const state_1 = require("./state");
|
|
8
10
|
async function stopService(work) {
|
|
9
11
|
if (work.state.current.type === 'running') {
|
|
10
12
|
await work.runtime.stop();
|
|
@@ -47,12 +49,16 @@ async function executeWorkService(work, environment, options) {
|
|
|
47
49
|
// aborted: the service is stopping anyway
|
|
48
50
|
});
|
|
49
51
|
}
|
|
52
|
+
const init = getServiceInit(work);
|
|
50
53
|
await work.runtime.execute(environment, {
|
|
51
54
|
cache: cacheState,
|
|
52
55
|
abort,
|
|
53
|
-
state: work.state,
|
|
56
|
+
state: init ? gateOnInit(work, init) : work.state,
|
|
54
57
|
stateKey: cacheState.stateKey,
|
|
55
58
|
daemon: options.daemon,
|
|
59
|
+
publishPorts: options.type === 'up' || work.requiredBy.some(isLocalTaskItem) || (!!init && isLocalTaskItem(init.task)),
|
|
60
|
+
// an init needs a healthy service, so a service with one is always awaited
|
|
61
|
+
waitForReady: options.wait === 'ready' || work.requiredBy.length > 0 || !!init,
|
|
56
62
|
});
|
|
57
63
|
});
|
|
58
64
|
}
|
|
@@ -78,4 +84,27 @@ async function executeWorkService(work, environment, options) {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
exports.executeWorkService = executeWorkService;
|
|
87
|
+
function getServiceInit(work) {
|
|
88
|
+
return work.data.type === 'container-service' ? work.data.init : null;
|
|
89
|
+
}
|
|
90
|
+
// With an init, the runtime's `running` (healthcheck passed) first reaches only
|
|
91
|
+
// the init task's view of the service; executeInitTask runs the init and then
|
|
92
|
+
// sets the service running, or failed.
|
|
93
|
+
function gateOnInit(work, init) {
|
|
94
|
+
const gate = new state_1.State(work.state.current);
|
|
95
|
+
gate.on('init-gate', (state) => {
|
|
96
|
+
if (state.type !== 'running') {
|
|
97
|
+
if (work.state.current.type !== 'error') {
|
|
98
|
+
work.state.set(state);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
work.status.write('info', `${work.name} is healthy, run its init ${init.task.name}`);
|
|
103
|
+
init.state.set(state);
|
|
104
|
+
});
|
|
105
|
+
return gate;
|
|
106
|
+
}
|
|
107
|
+
function isLocalTaskItem(item) {
|
|
108
|
+
return (0, work_item_1.isWorkTaskItem)(item) && (0, work_item_1.isLocalWorkTaskItem)(item);
|
|
109
|
+
}
|
|
81
110
|
//# sourceMappingURL=execute-work-service.js.map
|
|
@@ -6,10 +6,10 @@ const abort_1 = require("./abort");
|
|
|
6
6
|
const log_1 = require("../log");
|
|
7
7
|
const await_completed_dependencies_1 = require("./await-completed-dependencies");
|
|
8
8
|
const watch_loop_1 = require("./watch-loop");
|
|
9
|
+
const deadline_1 = require("./deadline");
|
|
9
10
|
const explain_1 = require("../cache/explain");
|
|
10
11
|
const archive_task_entry_1 = require("./archive-task-entry");
|
|
11
12
|
const units_1 = require("../utils/units");
|
|
12
|
-
const abort_event_1 = require("../utils/abort-event");
|
|
13
13
|
async function pushToBackend(work, environment, cacheState, options) {
|
|
14
14
|
const { resolved, stateKey, provable } = cacheState;
|
|
15
15
|
if (resolved.method === 'none' || !provable) {
|
|
@@ -28,29 +28,6 @@ async function pushToBackend(work, environment, cacheState, options) {
|
|
|
28
28
|
work.status.write('warn', `${work.name} failed to push to cache "${resolved.name}": ${(0, log_1.getErrorMessage)(e)}`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
// An abort signal that follows `parent` and additionally fires after `timeout`
|
|
32
|
-
// ms, so a task deadline reuses the runtimes' existing abort/cleanup path.
|
|
33
|
-
function withDeadline(parent, timeout) {
|
|
34
|
-
const controller = new AbortController();
|
|
35
|
-
let expired = false;
|
|
36
|
-
const parentListener = (0, abort_event_1.listenOnAbort)(parent, () => controller.abort());
|
|
37
|
-
const timer = timeout === null
|
|
38
|
-
? null
|
|
39
|
-
: setTimeout(() => {
|
|
40
|
-
expired = true;
|
|
41
|
-
controller.abort();
|
|
42
|
-
}, timeout);
|
|
43
|
-
return {
|
|
44
|
-
signal: controller.signal,
|
|
45
|
-
expired: () => expired,
|
|
46
|
-
clear() {
|
|
47
|
-
if (timer) {
|
|
48
|
-
clearTimeout(timer);
|
|
49
|
-
}
|
|
50
|
-
parentListener.close();
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
31
|
// A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
|
|
55
32
|
// Having no src of its own it is never a cache hit, so its dependencies are
|
|
56
33
|
// always checked and their outputs restored. When every one of them was a hit,
|
|
@@ -118,7 +95,7 @@ dependents = null) {
|
|
|
118
95
|
started,
|
|
119
96
|
});
|
|
120
97
|
const timeout = work.data.timeout ?? options.timeout;
|
|
121
|
-
const deadline = withDeadline(abort, timeout);
|
|
98
|
+
const deadline = (0, deadline_1.withDeadline)(abort, timeout);
|
|
122
99
|
try {
|
|
123
100
|
await work.runtime.execute(environment, {
|
|
124
101
|
cache: cacheState,
|
|
@@ -126,6 +103,8 @@ dependents = null) {
|
|
|
126
103
|
stateKey: cacheState.stateKey,
|
|
127
104
|
state: work.state,
|
|
128
105
|
daemon: options.daemon,
|
|
106
|
+
publishPorts: false,
|
|
107
|
+
waitForReady: true,
|
|
129
108
|
});
|
|
130
109
|
}
|
|
131
110
|
finally {
|