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.
Files changed (44) hide show
  1. package/build.schema.json +52 -1
  2. package/dist/cache/backends/registry/registry-client.js +19 -4
  3. package/dist/cli.js +1 -0
  4. package/dist/docker/pull.js +38 -1
  5. package/dist/environment/replace-env-variables.js +53 -22
  6. package/dist/executer/container-secrets.js +59 -0
  7. package/dist/executer/deadline.js +29 -0
  8. package/dist/executer/docker-service.js +58 -38
  9. package/dist/executer/docker-task.js +37 -23
  10. package/dist/executer/execute-init-task.js +100 -0
  11. package/dist/executer/execute-work-service.js +30 -1
  12. package/dist/executer/execute-work-task.js +4 -25
  13. package/dist/executer/execute-work-tree.js +28 -2
  14. package/dist/executer/get-container-binds.js +8 -4
  15. package/dist/executer/local-task.js +6 -1
  16. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +13 -7
  17. package/dist/kubernetes/secrets.js +83 -0
  18. package/dist/kubernetes/volumes.js +1 -1
  19. package/dist/optimizer/work-task-cache-description.js +6 -0
  20. package/dist/parse-set-arguments.js +38 -0
  21. package/dist/planner/service-definition.js +40 -0
  22. package/dist/planner/utils/append-work-dependencies.js +2 -0
  23. package/dist/planner/utils/append-work-needs.js +7 -0
  24. package/dist/planner/utils/append-work-service.js +67 -1
  25. package/dist/planner/utils/append-work-task.js +6 -0
  26. package/dist/planner/utils/parse-work-generate.js +4 -2
  27. package/dist/planner/utils/parse-work-mount.js +7 -3
  28. package/dist/planner/utils/parse-work-volume.js +8 -3
  29. package/dist/planner/utils/plan-work-volume.js +1 -0
  30. package/dist/planner/utils/service-hostname.js +15 -0
  31. package/dist/planner/utils/template-value.js +1 -1
  32. package/dist/planner/work-item-status.js +3 -1
  33. package/dist/planner/work-runtime-docker.js +8 -0
  34. package/dist/planner/work-runtime-kubernetes.js +40 -6
  35. package/dist/planner/work-secret.js +83 -0
  36. package/dist/program.js +9 -1
  37. package/dist/schema/build-file-base-task-schema.js +2 -0
  38. package/dist/schema/build-file-container-service-schema.js +5 -0
  39. package/dist/schema/build-file-secret-schema.js +22 -0
  40. package/dist/schema/build-file-volume-schema.js +3 -1
  41. package/dist/schema/reference-parser.js +14 -0
  42. package/dist/schema/work-scope-parser.js +30 -5
  43. package/dist/utils/redact.js +29 -0
  44. 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
- "type": "boolean"
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
- // always ask for push too: one token then serves the whole backend; a
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);
@@ -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
- let nameValue = environment.processEnvs[name] ?? null;
11
- if (!nameValue) {
12
- for (const envFile of Object.values(context.envFiles)) {
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
- if (nameValue) {
20
- replacements.push({
21
- key,
22
- name,
23
- available: true,
24
- value: nameValue,
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
- replacements.push({
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 envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
34
- const command = (0, work_service_1.getServiceCommand)(item.data);
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: binds.map((b) => `${b.localPath}:${(0, execute_docker_1.convertToPosixPath)(b.containerPath)}`),
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 containerOptions = buildCreateOptions(item, options.stateKey, serviceContainers, environment);
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
- for (const generate of item.data.generates) {
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