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.
Files changed (40) hide show
  1. package/build.schema.json +52 -1
  2. package/dist/cli.js +1 -0
  3. package/dist/executer/container-secrets.js +59 -0
  4. package/dist/executer/deadline.js +29 -0
  5. package/dist/executer/docker-service.js +58 -38
  6. package/dist/executer/docker-task.js +37 -23
  7. package/dist/executer/execute-init-task.js +100 -0
  8. package/dist/executer/execute-work-service.js +30 -1
  9. package/dist/executer/execute-work-task.js +4 -25
  10. package/dist/executer/execute-work-tree.js +28 -2
  11. package/dist/executer/get-container-binds.js +8 -4
  12. package/dist/executer/local-task.js +6 -1
  13. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +13 -7
  14. package/dist/kubernetes/secrets.js +83 -0
  15. package/dist/kubernetes/volumes.js +1 -1
  16. package/dist/optimizer/work-task-cache-description.js +6 -0
  17. package/dist/parse-set-arguments.js +38 -0
  18. package/dist/planner/service-definition.js +40 -0
  19. package/dist/planner/utils/append-work-dependencies.js +2 -0
  20. package/dist/planner/utils/append-work-needs.js +7 -0
  21. package/dist/planner/utils/append-work-service.js +67 -1
  22. package/dist/planner/utils/append-work-task.js +6 -0
  23. package/dist/planner/utils/parse-work-generate.js +4 -2
  24. package/dist/planner/utils/parse-work-mount.js +7 -3
  25. package/dist/planner/utils/parse-work-volume.js +13 -5
  26. package/dist/planner/utils/plan-work-volume.js +1 -0
  27. package/dist/planner/utils/service-hostname.js +15 -0
  28. package/dist/planner/work-item-status.js +3 -1
  29. package/dist/planner/work-runtime-docker.js +8 -0
  30. package/dist/planner/work-runtime-kubernetes.js +40 -6
  31. package/dist/planner/work-secret.js +83 -0
  32. package/dist/program.js +9 -1
  33. package/dist/schema/build-file-base-task-schema.js +2 -0
  34. package/dist/schema/build-file-container-service-schema.js +5 -0
  35. package/dist/schema/build-file-secret-schema.js +22 -0
  36. package/dist/schema/build-file-volume-schema.js +3 -1
  37. package/dist/schema/reference-parser.js +14 -0
  38. package/dist/schema/work-scope-parser.js +17 -5
  39. package/dist/utils/redact.js +29 -0
  40. 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": [
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 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
@@ -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 {