hammerkit 1.9.2 → 1.11.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/build.schema.json +52 -1
- package/dist/cache/backends/registry/registry-client.js +19 -4
- package/dist/cli.js +1 -0
- package/dist/docker/pull.js +38 -1
- package/dist/environment/replace-env-variables.js +53 -22
- 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 +8 -3
- package/dist/planner/utils/plan-work-volume.js +1 -0
- package/dist/planner/utils/service-hostname.js +15 -0
- package/dist/planner/utils/template-value.js +1 -1
- 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 +30 -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": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.digestBuffer = exports.digestFile = exports.RegistryClient = exports.parseChallenge = exports.RegistryError = exports.OCI_LAYER_TAR = exports.OCI_CONFIG = exports.OCI_MANIFEST = void 0;
|
|
3
|
+
exports.digestBuffer = exports.digestFile = exports.RegistryClient = exports.getTokenScope = exports.parseChallenge = exports.RegistryError = exports.OCI_LAYER_TAR = exports.OCI_CONFIG = exports.OCI_MANIFEST = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const promises_1 = require("fs/promises");
|
|
@@ -32,6 +32,23 @@ function parseChallenge(header) {
|
|
|
32
32
|
return { scheme, params };
|
|
33
33
|
}
|
|
34
34
|
exports.parseChallenge = parseChallenge;
|
|
35
|
+
// Always ask for pull and push, so one token serves the whole backend, plus
|
|
36
|
+
// what the challenge names for this repository: a DELETE is challenged for
|
|
37
|
+
// `delete`, which a pull/push token lacks. A registry grants the subset the
|
|
38
|
+
// credentials allow.
|
|
39
|
+
function getTokenScope(repository, challengeScope) {
|
|
40
|
+
const actions = ['pull', 'push'];
|
|
41
|
+
const prefix = `repository:${repository}:`;
|
|
42
|
+
if (challengeScope?.startsWith(prefix)) {
|
|
43
|
+
for (const action of challengeScope.substring(prefix.length).split(',')) {
|
|
44
|
+
if (action && !actions.includes(action)) {
|
|
45
|
+
actions.push(action);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return `${prefix}${actions.join(',')}`;
|
|
50
|
+
}
|
|
51
|
+
exports.getTokenScope = getTokenScope;
|
|
35
52
|
function basicHeader(credentials) {
|
|
36
53
|
return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}`;
|
|
37
54
|
}
|
|
@@ -76,9 +93,7 @@ class RegistryClient {
|
|
|
76
93
|
if (challenge.params.service) {
|
|
77
94
|
tokenUrl.searchParams.set('service', challenge.params.service);
|
|
78
95
|
}
|
|
79
|
-
|
|
80
|
-
// registry grants the subset the credentials allow
|
|
81
|
-
tokenUrl.searchParams.set('scope', `repository:${this.reference.repository}:pull,push`);
|
|
96
|
+
tokenUrl.searchParams.set('scope', getTokenScope(this.reference.repository, challenge.params.scope ?? null));
|
|
82
97
|
const response = await fetch(tokenUrl, {
|
|
83
98
|
headers: credentials ? { authorization: basicHeader(credentials) } : {},
|
|
84
99
|
});
|
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);
|
package/dist/docker/pull.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.splitImageName = exports.isImagePresent = exports.pull = void 0;
|
|
3
|
+
exports.normalizeArchitecture = exports.splitImageName = exports.isImagePresent = exports.pull = void 0;
|
|
4
4
|
const registry_credentials_1 = require("../cache/backends/registry/registry-credentials");
|
|
5
5
|
const registry_reference_1 = require("../cache/backends/registry/registry-reference");
|
|
6
6
|
async function pull(status, docker, imageName, environment) {
|
|
7
7
|
const images = await docker.listImages({});
|
|
8
8
|
if (isImagePresent(images, imageName)) {
|
|
9
|
+
await warnOnForeignPlatform(status, docker, imageName);
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
12
|
status.write('debug', `pull image ${imageName}`);
|
|
@@ -49,4 +50,40 @@ async function getPullAuth(imageName, environment) {
|
|
|
49
50
|
}
|
|
50
51
|
return { ...credentials, serveraddress: credentialHost };
|
|
51
52
|
}
|
|
53
|
+
const daemonArchitectures = new WeakMap();
|
|
54
|
+
const warnedImages = new Set();
|
|
55
|
+
// A digest pins a multi-arch index; a copy pulled earlier for another
|
|
56
|
+
// architecture (an amd64 one from a Compose file with `platform:`) carries the
|
|
57
|
+
// same digest, so docker uses it and the image runs emulated instead of native.
|
|
58
|
+
async function warnOnForeignPlatform(status, docker, imageName) {
|
|
59
|
+
const { repository, digest } = splitImageName(imageName);
|
|
60
|
+
if (!digest || warnedImages.has(imageName)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
if (!daemonArchitectures.has(docker)) {
|
|
65
|
+
daemonArchitectures.set(docker, docker.info().then((info) => normalizeArchitecture(info.Architecture ?? '')));
|
|
66
|
+
}
|
|
67
|
+
const [image, daemonArchitecture] = await Promise.all([
|
|
68
|
+
docker.getImage(imageName).inspect(),
|
|
69
|
+
daemonArchitectures.get(docker),
|
|
70
|
+
]);
|
|
71
|
+
const imageArchitecture = normalizeArchitecture(image.Architecture ?? '');
|
|
72
|
+
if (!imageArchitecture || !daemonArchitecture || imageArchitecture === daemonArchitecture) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
warnedImages.add(imageName);
|
|
76
|
+
status.write('warn', `image ${imageName} is present for ${imageArchitecture} on this ${daemonArchitecture} docker host and runs ` +
|
|
77
|
+
`emulated; if the digest is a multi-arch index, remove the copy to pull the native one: ` +
|
|
78
|
+
`docker image rm ${repository}@${digest}`);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// only a hint: never fail a pull over it
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function normalizeArchitecture(architecture) {
|
|
85
|
+
const aliases = { x86_64: 'amd64', aarch64: 'arm64' };
|
|
86
|
+
return aliases[architecture] ?? architecture;
|
|
87
|
+
}
|
|
88
|
+
exports.normalizeArchitecture = normalizeArchitecture;
|
|
52
89
|
//# sourceMappingURL=pull.js.map
|
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getEnvironmentVariables = exports.buildEnvironmentVariables = void 0;
|
|
4
|
+
// `${NAME}` or `${NAME:-default}` anywhere in a value
|
|
5
|
+
const INTERPOLATION = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
6
|
+
// The shell / process environment first, then the .env files. A variable set to
|
|
7
|
+
// an empty string is set, as in a shell: CI providers leave many empty (a merge
|
|
8
|
+
// request id on a branch pipeline).
|
|
9
|
+
function resolveReference(name, environment, context) {
|
|
10
|
+
const processValue = environment.processEnvs[name];
|
|
11
|
+
if (processValue !== undefined && processValue !== null) {
|
|
12
|
+
return processValue;
|
|
13
|
+
}
|
|
14
|
+
for (const envFile of Object.values(context.envFiles)) {
|
|
15
|
+
const fileValue = envFile[name];
|
|
16
|
+
if (fileValue !== undefined && fileValue !== null) {
|
|
17
|
+
return fileValue;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
4
22
|
function buildEnvironmentVariables(envs, environment, context) {
|
|
5
23
|
const replacements = [];
|
|
6
24
|
const variables = {};
|
|
7
25
|
for (const [key, value] of Object.entries(envs)) {
|
|
8
|
-
if (value.startsWith('$')) {
|
|
26
|
+
if (value.startsWith('$') && !value.startsWith('${')) {
|
|
9
27
|
const name = value.substring(1);
|
|
10
|
-
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
nameValue = envFile[name] ?? null;
|
|
14
|
-
if (nameValue) {
|
|
15
|
-
break;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
28
|
+
const nameValue = resolveReference(name, environment, context);
|
|
29
|
+
if (nameValue !== null) {
|
|
30
|
+
replacements.push({ key, name, available: true, value: nameValue });
|
|
18
31
|
}
|
|
19
|
-
|
|
20
|
-
replacements.push({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
else {
|
|
33
|
+
replacements.push({ key, name, available: false, value: null });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (value.includes('${')) {
|
|
37
|
+
const interpolated = interpolate(value, envs, environment, context);
|
|
38
|
+
if (interpolated.missing) {
|
|
39
|
+
replacements.push({ key, name: interpolated.missing, available: false, value: null });
|
|
26
40
|
}
|
|
27
41
|
else {
|
|
28
|
-
|
|
29
|
-
key,
|
|
30
|
-
name,
|
|
31
|
-
available: false,
|
|
32
|
-
value: null,
|
|
33
|
-
});
|
|
42
|
+
variables[key] = interpolated.value;
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
45
|
else {
|
|
@@ -40,6 +49,28 @@ function buildEnvironmentVariables(envs, environment, context) {
|
|
|
40
49
|
return { variables, replacements };
|
|
41
50
|
}
|
|
42
51
|
exports.buildEnvironmentVariables = buildEnvironmentVariables;
|
|
52
|
+
// Each `${NAME}` comes from the process environment or a .env file, else from a
|
|
53
|
+
// literal value of the same build file / task `envs`, else from its
|
|
54
|
+
// `:-default`. A reference without any of them is missing.
|
|
55
|
+
function interpolate(value, envs, environment, context) {
|
|
56
|
+
let missing = null;
|
|
57
|
+
const result = value.replace(INTERPOLATION, (match, name, fallback) => {
|
|
58
|
+
const resolved = resolveReference(name, environment, context) ?? getLiteral(envs, name) ?? fallback ?? null;
|
|
59
|
+
if (resolved === null) {
|
|
60
|
+
missing = missing ?? name;
|
|
61
|
+
return match;
|
|
62
|
+
}
|
|
63
|
+
return resolved;
|
|
64
|
+
});
|
|
65
|
+
return { value: result, missing };
|
|
66
|
+
}
|
|
67
|
+
function getLiteral(envs, name) {
|
|
68
|
+
const literal = envs[name];
|
|
69
|
+
if (literal === undefined || literal.includes('$')) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return literal;
|
|
73
|
+
}
|
|
43
74
|
function getEnvironmentVariables(envs) {
|
|
44
75
|
const result = { ...envs.variables };
|
|
45
76
|
for (const replacement of envs.replacements) {
|
|
@@ -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
|