hammerkit 1.10.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.schema.json +52 -1
- package/dist/cli.js +1 -0
- package/dist/executer/container-secrets.js +59 -0
- package/dist/executer/deadline.js +29 -0
- package/dist/executer/docker-service.js +58 -38
- package/dist/executer/docker-task.js +37 -23
- package/dist/executer/execute-init-task.js +100 -0
- package/dist/executer/execute-work-service.js +30 -1
- package/dist/executer/execute-work-task.js +4 -25
- package/dist/executer/execute-work-tree.js +28 -2
- package/dist/executer/get-container-binds.js +8 -4
- package/dist/executer/local-task.js +6 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +13 -7
- package/dist/kubernetes/secrets.js +83 -0
- package/dist/kubernetes/volumes.js +1 -1
- package/dist/optimizer/work-task-cache-description.js +6 -0
- package/dist/parse-set-arguments.js +38 -0
- package/dist/planner/service-definition.js +40 -0
- package/dist/planner/utils/append-work-dependencies.js +2 -0
- package/dist/planner/utils/append-work-needs.js +7 -0
- package/dist/planner/utils/append-work-service.js +67 -1
- package/dist/planner/utils/append-work-task.js +6 -0
- package/dist/planner/utils/parse-work-generate.js +4 -2
- package/dist/planner/utils/parse-work-mount.js +7 -3
- package/dist/planner/utils/parse-work-volume.js +13 -5
- package/dist/planner/utils/plan-work-volume.js +1 -0
- package/dist/planner/utils/service-hostname.js +15 -0
- package/dist/planner/work-item-status.js +3 -1
- package/dist/planner/work-runtime-docker.js +8 -0
- package/dist/planner/work-runtime-kubernetes.js +40 -6
- package/dist/planner/work-secret.js +83 -0
- package/dist/program.js +9 -1
- package/dist/schema/build-file-base-task-schema.js +2 -0
- package/dist/schema/build-file-container-service-schema.js +5 -0
- package/dist/schema/build-file-secret-schema.js +22 -0
- package/dist/schema/build-file-volume-schema.js +3 -1
- package/dist/schema/reference-parser.js +14 -0
- package/dist/schema/work-scope-parser.js +17 -5
- package/dist/utils/redact.js +29 -0
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
-
|
|
88
|
-
|
|
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:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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,
|
|
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