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
@@ -5,6 +5,7 @@ const emitter_1 = require("../utils/emitter");
5
5
  const environment_config_1 = require("../utils/environment-config");
6
6
  const log_1 = require("../log");
7
7
  const buffer_context_1 = require("../utils/buffer-context");
8
+ const redact_1 = require("../utils/redact");
8
9
  const StatusBufferMax = (0, environment_config_1.getEnvironmentConfig)('STATUS_BUFFER_LIMIT', 300);
9
10
  const ConsoleBufferMax = (0, environment_config_1.getEnvironmentConfig)('CONSOLE_BUFFER_LIMIT', 30);
10
11
  function logContext(type, item) {
@@ -18,7 +19,8 @@ function statusConsole(writable) {
18
19
  const statusBuffer = new buffer_context_1.BufferContext(StatusBufferMax);
19
20
  const consoleBuffer = new buffer_context_1.BufferContext(ConsoleBufferMax);
20
21
  const emit = (0, emitter_1.emitter)();
21
- function addMessage(context, message) {
22
+ function addMessage(context, unredacted) {
23
+ const message = { ...unredacted, message: (0, redact_1.redact)(unredacted.message) };
22
24
  emit.emit(message);
23
25
  writable.write(JSON.stringify({ context, message }) + '\n');
24
26
  if (message.type === 'console') {
@@ -6,6 +6,7 @@ const execute_docker_1 = require("../executer/execute-docker");
6
6
  const remove_container_1 = require("../docker/remove-container");
7
7
  const docker_task_1 = require("../executer/docker-task");
8
8
  const docker_service_1 = require("../executer/docker-service");
9
+ const container_secrets_1 = require("../executer/container-secrets");
9
10
  const using_container_1 = require("../docker/using-container");
10
11
  const event_cache_1 = require("../executer/event-cache");
11
12
  const get_docker_executor_1 = require("../executer/get-docker-executor");
@@ -14,6 +15,7 @@ const tar_1 = require("tar");
14
15
  const plan_work_volume_1 = require("./utils/plan-work-volume");
15
16
  const work_instance_id_1 = require("./work-instance-id");
16
17
  const stream_1 = require("stream");
18
+ const service_definition_1 = require("./service-definition");
17
19
  const container_task_state_1 = require("../executer/container-task-state");
18
20
  function dockerTaskRuntime(task, workEnvironment) {
19
21
  const docker = (0, execute_docker_1.getContainerCli)(workEnvironment);
@@ -160,6 +162,11 @@ function dockerServiceRuntime(service, workEnvironment) {
160
162
  if (!currentService) {
161
163
  return;
162
164
  }
165
+ if (currentService.Labels['hammerkit-definition'] !== (0, service_definition_1.getServiceDefinitionHash)(service)) {
166
+ service.status.write('info', `${service.name} changed since it was started, recreating it`);
167
+ await (0, remove_container_1.removeContainer)(docker.getContainer(currentService.Id));
168
+ return;
169
+ }
163
170
  const servicePid = 'hammerkit-pid' in currentService.Labels ? parseInt(currentService.Labels['hammerkit-pid']) : undefined;
164
171
  const serviceState = 'hammerkit-state' in currentService.Labels ? currentService.Labels['hammerkit-state'] : '';
165
172
  state.set({
@@ -181,6 +188,7 @@ function dockerServiceRuntime(service, workEnvironment) {
181
188
  for (const container of containers) {
182
189
  await (0, remove_container_1.removeContainer)(docker.getContainer(container.Id));
183
190
  }
191
+ await (0, container_secrets_1.removeContainerSecrets)((0, work_instance_id_1.getWorkInstanceId)(service));
184
192
  },
185
193
  async remove() {
186
194
  await this.stop();
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = void 0;
7
7
  const abort_1 = require("../executer/abort");
8
+ const get_service_ip_1 = require("../kubernetes/get-service-ip");
9
+ const work_item_1 = require("./work-item");
8
10
  const kubernetes_service_1 = require("../executer/kubernetes-service");
9
11
  const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
10
12
  const replace_env_variables_1 = require("../environment/replace-env-variables");
@@ -23,6 +25,7 @@ const version_1 = require("../version");
23
25
  const restore_kubernetes_data_1 = require("../kubernetes/restore-kubernetes-data");
24
26
  const store_kubernetes_data_1 = require("../kubernetes/store-kubernetes-data");
25
27
  const remove_persistent_data_1 = require("../kubernetes/remove-persistent-data");
28
+ const secrets_1 = require("../kubernetes/secrets");
26
29
  async function listJobNames(instance, kubernetes, task) {
27
30
  const jobs = await instance.batchApi.listNamespacedJob({
28
31
  namespace: kubernetes.namespace,
@@ -47,11 +50,31 @@ function kubernetesTaskRuntime(task, kubernetes) {
47
50
  const persistence = await (0, volumes_1.getKubernetesPersistence)(task);
48
51
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
49
52
  await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence);
53
+ const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, task.data.secrets);
54
+ try {
55
+ await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, task, task.data.secrets, environment);
56
+ }
57
+ catch (e) {
58
+ options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
59
+ return;
60
+ }
50
61
  // The last job of a run stays: its state label is what currentStateKey
51
62
  // reads. Jobs from an earlier run of this task go before this one starts.
52
63
  for (const name of await listJobNames(instance, kubernetes, task)) {
53
64
  await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, name, options.abort);
54
65
  }
66
+ // the services the task needs, by the names it lists them under
67
+ const hostAliases = [];
68
+ for (const need of task.needs) {
69
+ if (!(0, work_item_1.isContainerWorkServiceItem)(need.service)) {
70
+ continue;
71
+ }
72
+ const ip = await (0, get_service_ip_1.getServiceIp)(instance, kubernetes, need.service);
73
+ if (!ip) {
74
+ throw new Error(`unable to get service ip for ${need.name}`);
75
+ }
76
+ hostAliases.push({ ip, hostnames: [need.name] });
77
+ }
55
78
  const podName = `${task.name}-${options.stateKey}`;
56
79
  let i = 0;
57
80
  for (const cmd of task.data.cmds) {
@@ -84,15 +107,20 @@ function kubernetesTaskRuntime(task, kubernetes) {
84
107
  containers: [
85
108
  {
86
109
  image: task.data.image,
87
- command: [cmd.parsed.command],
88
- args: cmd.parsed.args,
110
+ // through the task's shell, as on Docker
111
+ command: [task.data.shell, '-c'],
112
+ args: [cmd.cmd],
89
113
  workingDir: cmd.cwd,
90
- env: Object.entries(envs).map(([key, value]) => ({ name: key, value })),
114
+ env: [
115
+ ...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
116
+ ...secrets.env,
117
+ ],
91
118
  name: `cmd-${++i}`,
92
- volumeMounts: persistence.mounts.map((m) => m.mount),
119
+ volumeMounts: [...persistence.mounts.map((m) => m.mount), ...secrets.mounts],
93
120
  },
94
121
  ],
95
- volumes: persistence.volumes,
122
+ volumes: [...persistence.volumes, ...secrets.volumes],
123
+ hostAliases,
96
124
  restartPolicy: 'Never',
97
125
  },
98
126
  },
@@ -121,9 +149,12 @@ function kubernetesTaskRuntime(task, kubernetes) {
121
149
  type: 'error',
122
150
  errorMessage: (0, log_1.getErrorMessage)(e),
123
151
  });
152
+ await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task).catch(() => undefined);
124
153
  return;
125
154
  }
126
155
  }
156
+ // the finished job stays for its state, the values it read need not
157
+ await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task);
127
158
  },
128
159
  async stop() {
129
160
  const jobs = await instance.batchApi.listNamespacedJob({
@@ -160,6 +191,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
160
191
  async remove() {
161
192
  await this.stop();
162
193
  await (0, remove_persistent_data_1.removePersistentData)(instance, kubernetes, task);
194
+ await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task);
163
195
  },
164
196
  async currentStateKey() {
165
197
  const jobs = await instance.batchApi.listNamespacedJob({
@@ -217,13 +249,15 @@ function kubernetesServiceRuntime(service, kubernetes) {
217
249
  });
218
250
  }
219
251
  }
252
+ await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, service);
220
253
  },
221
254
  async execute(environment, options) {
222
255
  const persistence = await (0, volumes_1.getKubernetesPersistence)(service);
223
256
  await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
224
257
  await (0, ensure_kubernetes_service_exists_1.ensureKubernetesServiceExists)(instance, kubernetes, service);
225
258
  await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence);
226
- await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, options.stateKey);
259
+ await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, service, service.data.secrets, environment);
260
+ await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, service.data.secrets), options.stateKey);
227
261
  const name = (0, resources_1.getResourceName)(service);
228
262
  options.state.set({
229
263
  type: 'running',
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSecretDigest = exports.resolveSecretValue = exports.parseWorkSecrets = exports.getPortableSecretName = exports.getSecretName = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const os_1 = require("os");
6
+ const fs_1 = require("fs");
7
+ const normalize_path_1 = require("./utils/normalize-path");
8
+ const template_value_1 = require("./utils/template-value");
9
+ const portable_path_1 = require("./utils/portable-path");
10
+ const redact_1 = require("../utils/redact");
11
+ function getSecretName(secret) {
12
+ return secret.target.type === 'env' ? secret.target.name : secret.target.path;
13
+ }
14
+ exports.getSecretName = getSecretName;
15
+ // The name as it reads on every checkout, for cache identity: a file target
16
+ // lives under the task's cwd, which is a different absolute path per machine.
17
+ function getPortableSecretName(projectRoot, secret) {
18
+ return secret.target.type === 'env' ? secret.target.name : (0, portable_path_1.portablePath)(projectRoot, secret.target.path);
19
+ }
20
+ exports.getPortableSecretName = getPortableSecretName;
21
+ function parseWorkSecrets(cwd, projectRoot, secrets, envs, environment) {
22
+ return (secrets ?? []).map((secret) => {
23
+ const ref = secret.from.substring(secret.from.indexOf(':') + 1);
24
+ const source = secret.from.startsWith('env:')
25
+ ? { type: 'env', name: ref }
26
+ : { type: 'file', path: (0, normalize_path_1.normalizePath)(cwd, (0, os_1.homedir)(), (0, template_value_1.templateValue)(ref, envs)) };
27
+ const target = secret.env
28
+ ? { type: 'env', name: secret.env }
29
+ : { type: 'file', path: (0, normalize_path_1.normalizePath)(cwd, cwd, (0, template_value_1.templateValue)(secret.path ?? '', envs)) };
30
+ const workSecret = {
31
+ source,
32
+ target,
33
+ cache: secret.cache ?? false,
34
+ digest: () => getSecretDigest(projectRoot, cwd, workSecret, readSecretValueSync(workSecret, environment)),
35
+ };
36
+ return workSecret;
37
+ });
38
+ }
39
+ exports.parseWorkSecrets = parseWorkSecrets;
40
+ function readSecretValueSync(secret, environment) {
41
+ if (secret.source.type === 'env') {
42
+ const value = environment.processEnvs[secret.source.name];
43
+ if (value === undefined || value === null) {
44
+ throw new Error(`secret ${getSecretName(secret)}: environment variable ${secret.source.name} is not set`);
45
+ }
46
+ (0, redact_1.registerSecretValue)(value);
47
+ return value;
48
+ }
49
+ if (!(0, fs_1.existsSync)(secret.source.path)) {
50
+ throw new Error(`secret ${getSecretName(secret)}: file ${secret.source.path} does not exist`);
51
+ }
52
+ const value = (0, fs_1.readFileSync)(secret.source.path, 'utf8');
53
+ (0, redact_1.registerSecretValue)(value);
54
+ return value;
55
+ }
56
+ // The value, read when the task or service starts (or when a cache-affecting
57
+ // secret is hashed); a missing source fails naming the secret.
58
+ async function resolveSecretValue(secret, environment) {
59
+ if (secret.source.type === 'env') {
60
+ const value = environment.processEnvs[secret.source.name];
61
+ if (value === undefined || value === null) {
62
+ throw new Error(`secret ${getSecretName(secret)}: environment variable ${secret.source.name} is not set`);
63
+ }
64
+ (0, redact_1.registerSecretValue)(value);
65
+ return value;
66
+ }
67
+ if (!(await environment.file.exists(secret.source.path))) {
68
+ throw new Error(`secret ${getSecretName(secret)}: file ${secret.source.path} does not exist`);
69
+ }
70
+ const value = await environment.file.read(secret.source.path);
71
+ (0, redact_1.registerSecretValue)(value);
72
+ return value;
73
+ }
74
+ exports.resolveSecretValue = resolveSecretValue;
75
+ // Salted with the project-relative location and the secret's name, so the
76
+ // digest is identical on every checkout (remote cache ids match) but can't be
77
+ // matched against digests of the same value elsewhere.
78
+ function getSecretDigest(projectRoot, cwd, secret, value) {
79
+ const salt = `hammerkit-secret\0${(0, portable_path_1.portablePath)(projectRoot, cwd)}\0${getPortableSecretName(projectRoot, secret)}`;
80
+ return `sha256:${(0, crypto_1.createHash)('sha256').update(`${salt}\0${value}`).digest('hex')}`;
81
+ }
82
+ exports.getSecretDigest = getSecretDigest;
83
+ //# sourceMappingURL=work-secret.js.map
package/dist/program.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getProgram = exports.createCli = void 0;
7
7
  const commander_1 = require("commander");
8
+ const parse_set_arguments_1 = require("./parse-set-arguments");
8
9
  const path_1 = require("path");
9
10
  const ci_1 = require("./utils/ci");
10
11
  const parse_label_arguments_1 = require("./parser/parse-label-arguments");
@@ -56,7 +57,9 @@ function parseWorkLabelScope(options) {
56
57
  }
57
58
  async function getProgram(environment, argv) {
58
59
  const program = new commander_1.Command();
59
- const args = [...argv];
60
+ const parsed = (0, parse_set_arguments_1.parseSetArguments)(argv);
61
+ const args = parsed.args;
62
+ environment.processEnvs = { ...environment.processEnvs, ...parsed.values };
60
63
  const fileIndex = args.indexOf('--file');
61
64
  const fileName = fileIndex >= 0 ? (0, path_1.join)(environment.cwd, args[fileIndex + 1]) : await (0, default_build_file_1.getBuildFilename)(environment.cwd, environment);
62
65
  if (fileIndex >= 0) {
@@ -421,6 +424,9 @@ async function getProgram(environment, argv) {
421
424
  .addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count').argParser(parseInt).default(4))
422
425
  .addOption(new commander_1.Option('-w, --watch', 'watch tasks').default(false))
423
426
  .addOption(new commander_1.Option('-d, --daemon', 'run services in background').default(false))
427
+ .addOption(new commander_1.Option('--wait <mode>', 'with --daemon, return once services are ready or once they started')
428
+ .default('ready')
429
+ .choices(['ready', 'start']))
424
430
  .addOption(new commander_1.Option('--env <name>', 'environment'))
425
431
  .addOption(new commander_1.Option('-l, --log <mode>', 'log mode')
426
432
  .default(ci_1.isCI ? 'live' : 'interactive')
@@ -440,6 +446,7 @@ async function getProgram(environment, argv) {
440
446
  workers: options.concurrency,
441
447
  logMode: options.log,
442
448
  daemon: options.daemon,
449
+ wait: options.wait,
443
450
  cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
444
451
  timeout: options.timeout ?? null,
445
452
  });
@@ -593,6 +600,7 @@ tasks:
593
600
  program.version((0, version_1.getVersion)());
594
601
  program.option('--verbose', 'log debugging information', false);
595
602
  program.option('--file', 'set build file', '.hammerkit.yaml');
603
+ program.option('--set <NAME=value>', 'value for $NAME / ${NAME} references, ahead of the shell and .env (repeatable)');
596
604
  program.configureOutput({
597
605
  writeOut: (str) => environment.console.info(str),
598
606
  writeErr: (str) => {
@@ -9,6 +9,7 @@ const cache_schema_1 = require("./cache-schema");
9
9
  const build_file_task_command_schema_1 = require("./build-file-task-command-schema");
10
10
  const build_file_volume_schema_1 = require("./build-file-volume-schema");
11
11
  const duration_schema_1 = require("./duration-schema");
12
+ const build_file_secret_schema_1 = require("./build-file-secret-schema");
12
13
  exports.buildFileBaseTaskSchema = (0, zod_1.object)({
13
14
  deps: (0, zod_1.array)((0, zod_1.string)()).optional(),
14
15
  needs: (0, zod_1.array)(build_file_need_schema_1.buildFileNeedSchema).optional(),
@@ -27,5 +28,6 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
27
28
  // Maximum execution time, e.g. `30s`, `10m`, `1h30m`. The task fails when it
28
29
  // runs longer. Does not affect the cache key (ADR-0002).
29
30
  timeout: duration_schema_1.durationSchema.optional(),
31
+ secrets: (0, zod_1.array)(build_file_secret_schema_1.buildFileSecretSchema).optional(),
30
32
  });
31
33
  //# sourceMappingURL=build-file-base-task-schema.js.map
@@ -6,6 +6,7 @@ const envs_schema_1 = require("./envs-schema");
6
6
  const build_file_need_schema_1 = require("./build-file-need-schema");
7
7
  const labels_schema_1 = require("./labels-schema");
8
8
  const build_file_service_container_healthcheck_1 = require("./build-file-service-container-healthcheck");
9
+ const build_file_secret_schema_1 = require("./build-file-secret-schema");
9
10
  exports.buildFileContainerServiceSchema = (0, zod_1.object)({
10
11
  image: (0, zod_1.string)(),
11
12
  description: (0, zod_1.string)().optional(),
@@ -30,6 +31,10 @@ exports.buildFileContainerServiceSchema = (0, zod_1.object)({
30
31
  labels: labels_schema_1.labelsSchema.optional(),
31
32
  continuous: (0, zod_1.boolean)().optional(),
32
33
  healthcheck: build_file_service_container_healthcheck_1.buildFileServiceContainerHealthcheck.optional(),
34
+ // a task (name or prefix:name, as in deps) that runs once the healthcheck
35
+ // passed; the service counts as ready for dependents only after it succeeded
36
+ init: (0, zod_1.string)().describe('task run once the service is healthy, before dependents start').optional(),
37
+ secrets: (0, zod_1.array)(build_file_secret_schema_1.buildFileSecretSchema).optional(),
33
38
  })
34
39
  .strict()
35
40
  .describe('container service');
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildFileSecretSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ // A credential handed to a task or service without putting it in the build
6
+ // file, the cache or the logs: `from: env:NAME` or `from: file:path`, injected
7
+ // as an env variable (`env`) or a read-only file (`path`). `cache: true` makes
8
+ // a salted digest of the value part of the task's cache key (ADR 0002).
9
+ exports.buildFileSecretSchema = (0, zod_1.object)({
10
+ from: (0, zod_1.string)().regex(/^(env|file):.+$/, 'from must be env:NAME or file:path'),
11
+ env: (0, zod_1.string)()
12
+ .regex(/^[A-Za-z_][A-Za-z0-9_]*$/)
13
+ .optional(),
14
+ path: (0, zod_1.string)().optional(),
15
+ cache: (0, zod_1.boolean)().optional(),
16
+ })
17
+ .strict()
18
+ .refine((secret) => (secret.env === undefined) !== (secret.path === undefined), {
19
+ message: 'a secret needs exactly one of env or path',
20
+ })
21
+ .describe('secret from env:NAME or file:path, injected as env or a read-only file');
22
+ //# sourceMappingURL=build-file-secret-schema.js.map
@@ -5,10 +5,12 @@ const zod_1 = require("zod");
5
5
  exports.buildFileVolumeSchema = (0, zod_1.union)([
6
6
  (0, zod_1.string)(),
7
7
  (0, zod_1.object)({
8
- export: (0, zod_1.boolean)().optional(),
8
+ // `always`: copied to the host even when the task fails (test reports)
9
+ export: (0, zod_1.union)([(0, zod_1.boolean)(), (0, zod_1.literal)('always')]).optional(),
9
10
  resetOnChange: (0, zod_1.boolean)().optional(),
10
11
  path: (0, zod_1.string)(),
11
12
  name: (0, zod_1.string)().optional(),
13
+ readOnly: (0, zod_1.boolean)().optional(),
12
14
  }),
13
15
  ]);
14
16
  //# sourceMappingURL=build-file-volume-schema.js.map
@@ -73,6 +73,7 @@ async function parseReferences(ctx, mainScope, environment) {
73
73
  scope: file,
74
74
  needs: [],
75
75
  deps: [],
76
+ init: null,
76
77
  envs: serviceEnvs,
77
78
  };
78
79
  }
@@ -86,6 +87,7 @@ async function parseReferences(ctx, mainScope, environment) {
86
87
  for (const service of Object.values(reference.services)) {
87
88
  resolveDeps(reference, service, service.scope, service.schema);
88
89
  resolveNeeds(reference, service, service.scope, service.schema);
90
+ resolveInit(reference, service);
89
91
  }
90
92
  reference.caches = (0, resolve_cache_1.withBuiltinCaches)(declaredCaches);
91
93
  return reference;
@@ -142,6 +144,18 @@ function resolveDeps(reference, taskOrService, scope, schema) {
142
144
  });
143
145
  }
144
146
  }
147
+ function resolveInit(reference, service) {
148
+ if ((0, build_file_service_schema_1.isBuildFileKubernetesServiceSchema)(service.schema) || !service.schema.init) {
149
+ return;
150
+ }
151
+ const initTask = findTask(reference, service.scope, service.schema.init, null);
152
+ service.init = {
153
+ task: initTask.task,
154
+ type: initTask.type,
155
+ cwd: initTask.type === 'include' ? service.cwd : initTask.task.cwd,
156
+ relativeName: service.schema.init,
157
+ };
158
+ }
145
159
  function resolveNeeds(reference, taskOrService, scope, schema) {
146
160
  if ((0, build_file_service_schema_1.isBuildFileKubernetesServiceSchema)(schema)) {
147
161
  return;
@@ -37,15 +37,17 @@ function getWorkContext(context, scope, environment) {
37
37
  workTree.environment = filteredWorkTree.environment;
38
38
  }
39
39
  }
40
- for (const task of Object.values(context.tasks)) {
41
- const item = (0, append_work_task_1.appendWorkTask)(workTree, task.cwd, task, environment, context);
40
+ // everything is planned before the run is narrowed: planning a service adds
41
+ // its init task's need on it, which narrowing follows
42
+ const tasks = Object.values(context.tasks).map((task) => (0, append_work_task_1.appendWorkTask)(workTree, task.cwd, task, environment, context));
43
+ const services = Object.values(context.services).map((service) => (0, append_work_service_1.appendWorkService)(workTree, service, environment, context));
44
+ for (const item of tasks) {
42
45
  if (matchesTaskWorkScope(item, scope)) {
43
46
  requested.push(item.name);
44
47
  applyTask(filteredWorkTree, item);
45
48
  }
46
49
  }
47
- for (const service of Object.values(context.services)) {
48
- const item = (0, append_work_service_1.appendWorkService)(workTree, service, environment, context);
50
+ for (const item of services) {
49
51
  if (matchesServiceWorkScope(item.data, scope)) {
50
52
  applyService(filteredWorkTree, item);
51
53
  }
@@ -64,6 +66,11 @@ function keepRequirersInScope(workTree) {
64
66
  ]);
65
67
  for (const service of Object.values(workTree.services)) {
66
68
  service.requiredBy = service.requiredBy.filter((required) => inScope.has(required));
69
+ // an init task asked for alone is what its service starts for
70
+ const init = service.data.type === 'container-service' ? service.data.init : null;
71
+ if (init && workTree.requested?.length === 1 && workTree.requested[0] === init.task.name) {
72
+ service.requiredBy.push(init.task);
73
+ }
67
74
  }
68
75
  }
69
76
  function applyTask(workTree, item) {
@@ -73,12 +80,17 @@ function applyTask(workTree, item) {
73
80
  workTree.tasks[item.name] = item;
74
81
  applyNeedsAndDeps(workTree, item);
75
82
  }
76
- function applyService(workTree, item) {
83
+ function applyService(workTree, need) {
84
+ const item = (0, append_work_service_1.getPlannedService)(need);
77
85
  if (workTree.services[item.name]) {
78
86
  return;
79
87
  }
80
88
  workTree.services[item.name] = item;
81
89
  applyNeedsAndDeps(workTree, item);
90
+ // a service in the run brings its init task, which runs once it is healthy
91
+ if (item.data.type === 'container-service' && item.data.init) {
92
+ applyTask(workTree, item.data.init.task);
93
+ }
82
94
  }
83
95
  function applyNeedsAndDeps(workTree, item) {
84
96
  for (const dep of item.deps) {
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redact = exports.registerSecretValue = void 0;
4
+ // Secret values hammerkit has read, masked in everything it logs, including the
5
+ // output it streams from a task or service. Logs are per line, so each line of
6
+ // a multi-line value (a key file) is masked on its own. Values shorter than
7
+ // MIN_LENGTH are left alone: masking them would mangle unrelated output.
8
+ const MIN_LENGTH = 4;
9
+ const MASK = '***';
10
+ const values = new Set();
11
+ function registerSecretValue(value) {
12
+ for (const candidate of [value, ...value.split(/\r?\n/)]) {
13
+ const trimmed = candidate.trim();
14
+ if (trimmed.length >= MIN_LENGTH) {
15
+ values.add(trimmed);
16
+ }
17
+ }
18
+ }
19
+ exports.registerSecretValue = registerSecretValue;
20
+ function redact(message) {
21
+ if (values.size === 0) {
22
+ return message;
23
+ }
24
+ // longest first, so a value containing another is masked as a whole
25
+ const sorted = [...values].sort((a, b) => b.length - a.length);
26
+ return sorted.reduce((result, value) => result.split(value).join(MASK), message);
27
+ }
28
+ exports.redact = redact;
29
+ //# sourceMappingURL=redact.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "1.10.0",
4
+ "version": "1.11.1",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",