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
|
@@ -4,13 +4,24 @@ exports.executeWorkTree = void 0;
|
|
|
4
4
|
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
5
5
|
const execute_work_service_1 = require("./execute-work-service");
|
|
6
6
|
const execute_work_task_1 = require("./execute-work-task");
|
|
7
|
+
const execute_init_task_1 = require("./execute-init-task");
|
|
7
8
|
async function executeWorkTree(work, environment, options) {
|
|
8
9
|
const itemPromises = [];
|
|
9
10
|
const dependents = skippableDependents(work, options);
|
|
11
|
+
mirrorServicesToInitViews(work);
|
|
10
12
|
for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(work)) {
|
|
11
|
-
if (task.state.current.type
|
|
12
|
-
|
|
13
|
+
if (task.state.current.type !== 'pending') {
|
|
14
|
+
continue;
|
|
13
15
|
}
|
|
16
|
+
const initOf = (0, execute_init_task_1.findInitOf)(task);
|
|
17
|
+
if (initOf) {
|
|
18
|
+
const service = work.services[initOf.serviceName] ?? null;
|
|
19
|
+
// asked for alone (by name), it runs against a service already running too
|
|
20
|
+
const requested = work.requested?.length === 1 && work.requested[0] === task.name;
|
|
21
|
+
itemPromises.push((0, execute_init_task_1.executeInitTask)(task, initOf.init, service, requested, environment, options));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
itemPromises.push((0, execute_work_task_1.executeWorkTask)(task, environment, options, dependents.get(task.name) ?? null));
|
|
14
25
|
}
|
|
15
26
|
for (const service of (0, plan_work_tasks_1.iterateWorkServices)(work)) {
|
|
16
27
|
if (options.type === 'down') {
|
|
@@ -25,6 +36,21 @@ async function executeWorkTree(work, environment, options) {
|
|
|
25
36
|
await Promise.all(itemPromises);
|
|
26
37
|
}
|
|
27
38
|
exports.executeWorkTree = executeWorkTree;
|
|
39
|
+
// An init task sees its service through a view that follows the service, so a
|
|
40
|
+
// service left running by an earlier `up` is running for its init task too. The
|
|
41
|
+
// view runs early only while the init itself runs, see executeWorkService.
|
|
42
|
+
function mirrorServicesToInitViews(work) {
|
|
43
|
+
for (const service of (0, plan_work_tasks_1.iterateWorkServices)(work)) {
|
|
44
|
+
if (service.data.type !== 'container-service' || !service.data.init) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const view = service.data.init.state;
|
|
48
|
+
view.set(service.state.current);
|
|
49
|
+
service.state.on('init-view', (state) => {
|
|
50
|
+
view.set(state);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
28
54
|
// For every task that may be skipped when nothing needs it, the tasks in this
|
|
29
55
|
// run that depend on it. Requested tasks always run; so does anything a service
|
|
30
56
|
// depends on, and everything in watch mode or with skipping turned off.
|
|
@@ -9,16 +9,20 @@ function isInside(path, parent) {
|
|
|
9
9
|
function getContainerBinds(item) {
|
|
10
10
|
const sources = item.data.src.map((s) => s.absolutePath);
|
|
11
11
|
const items = [
|
|
12
|
-
...item.data.mounts,
|
|
12
|
+
...item.data.mounts.map((m) => ({ localPath: m.localPath, containerPath: m.containerPath, readOnly: m.readOnly })),
|
|
13
13
|
// A source inside another source (a dependency's `app/app.csproj` under the
|
|
14
14
|
// task's own `app`) is already visible through the outer bind. Binding it
|
|
15
15
|
// again would nest bind mounts, which Docker Desktop then refuses to let the
|
|
16
16
|
// host delete while the container exists.
|
|
17
17
|
...sources
|
|
18
18
|
.filter((path) => !sources.some((other) => isInside(path, other)))
|
|
19
|
-
.map((path) => ({ localPath: path, containerPath: path })),
|
|
20
|
-
...item.data.generates
|
|
21
|
-
|
|
19
|
+
.map((path) => ({ localPath: path, containerPath: path, readOnly: false })),
|
|
20
|
+
...item.data.generates
|
|
21
|
+
.filter((g) => !g.isFile)
|
|
22
|
+
.map((v) => ({ localPath: v.volumeName, containerPath: v.path, readOnly: false })),
|
|
23
|
+
...item.data.generates
|
|
24
|
+
.filter((g) => g.isFile)
|
|
25
|
+
.map((v) => ({ localPath: v.path, containerPath: v.path, readOnly: false })),
|
|
22
26
|
];
|
|
23
27
|
return items.reduce((array, item) => {
|
|
24
28
|
if (array.findIndex((i) => i.containerPath === item.containerPath) === -1) {
|
|
@@ -6,10 +6,15 @@ const execute_command_1 = require("./execute-command");
|
|
|
6
6
|
const log_1 = require("../log");
|
|
7
7
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
8
8
|
const get_service_env_hints_1 = require("./get-service-env-hints");
|
|
9
|
+
const container_secrets_1 = require("./container-secrets");
|
|
9
10
|
async function localTask(item, environment, options) {
|
|
10
11
|
item.status.write('info', `execute ${item.name} locally`);
|
|
11
|
-
const envs = { ...(0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs), ...(0, get_service_env_hints_1.getServiceEnvHints)(item.needs) };
|
|
12
12
|
try {
|
|
13
|
+
const envs = {
|
|
14
|
+
...(0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs),
|
|
15
|
+
...(0, get_service_env_hints_1.getServiceEnvHints)(item.needs),
|
|
16
|
+
...(await (0, container_secrets_1.resolveSecretEnvs)(item.data.secrets, environment)),
|
|
17
|
+
};
|
|
13
18
|
for (const cmd of item.data.cmds) {
|
|
14
19
|
(0, abort_1.checkForAbort)(options.abort);
|
|
15
20
|
item.status.write('info', `execute cmd "${cmd.cmd}" locally`);
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ensureKubernetesDeploymentExists = void 0;
|
|
4
4
|
const work_item_1 = require("../planner/work-item");
|
|
5
5
|
const work_service_1 = require("../planner/work-service");
|
|
6
|
+
const service_hostname_1 = require("../planner/utils/service-hostname");
|
|
6
7
|
const apply_1 = require("./apply");
|
|
7
8
|
const get_service_ip_1 = require("./get-service-ip");
|
|
8
9
|
const version_1 = require("../version");
|
|
@@ -10,7 +11,7 @@ const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
|
10
11
|
const await_running_state_1 = require("./await-running-state");
|
|
11
12
|
const resources_1 = require("./resources");
|
|
12
13
|
const work_healthcheck_1 = require("../planner/work-healthcheck");
|
|
13
|
-
async function ensureKubernetesDeploymentExists(instance, env, service, persistence, stateKey) {
|
|
14
|
+
async function ensureKubernetesDeploymentExists(instance, env, service, persistence, secrets, stateKey) {
|
|
14
15
|
const hostAliases = [];
|
|
15
16
|
for (const need of service.needs) {
|
|
16
17
|
if (!(0, work_item_1.isContainerWorkServiceItem)(need.service)) {
|
|
@@ -67,23 +68,28 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
67
68
|
},
|
|
68
69
|
},
|
|
69
70
|
spec: {
|
|
71
|
+
// the pod resolves its own service name, as with Docker's Hostname
|
|
72
|
+
hostname: (0, service_hostname_1.getServiceHostname)(service.name),
|
|
70
73
|
hostAliases: hostAliases,
|
|
71
74
|
containers: [
|
|
72
75
|
{
|
|
73
76
|
name: service.name.replace(/:/, '-'),
|
|
74
77
|
image: service.data.image,
|
|
75
78
|
workingDir: (0, work_service_1.getServiceWorkingDir)(service.data),
|
|
76
|
-
env:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
env: [
|
|
80
|
+
...Object.entries(envs).map(([key, value]) => ({
|
|
81
|
+
name: key,
|
|
82
|
+
value: value,
|
|
83
|
+
})),
|
|
84
|
+
...secrets.env,
|
|
85
|
+
],
|
|
80
86
|
...getKubernetesCommand(serviceCommand),
|
|
81
87
|
ports: service.data.ports.map((p) => ({
|
|
82
88
|
containerPort: p.containerPort,
|
|
83
89
|
})),
|
|
84
90
|
readinessProbe: probe,
|
|
85
91
|
livenessProbe: probe,
|
|
86
|
-
volumeMounts: persistence.mounts.map((m) => m.mount),
|
|
92
|
+
volumeMounts: [...persistence.mounts.map((m) => m.mount), ...secrets.mounts],
|
|
87
93
|
},
|
|
88
94
|
{
|
|
89
95
|
name: 'debug',
|
|
@@ -93,7 +99,7 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
93
99
|
volumeMounts: persistence.mounts.map((m) => m.mount),
|
|
94
100
|
},
|
|
95
101
|
],
|
|
96
|
-
volumes: persistence.volumes,
|
|
102
|
+
volumes: [...persistence.volumes, ...secrets.volumes],
|
|
97
103
|
},
|
|
98
104
|
},
|
|
99
105
|
},
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeKubernetesSecret = exports.ensureKubernetesSecret = exports.getKubernetesSecretRefs = exports.getSecretResourceName = void 0;
|
|
4
|
+
const work_secret_1 = require("../planner/work-secret");
|
|
5
|
+
const apply_1 = require("./apply");
|
|
6
|
+
const resources_1 = require("./resources");
|
|
7
|
+
const version_1 = require("../version");
|
|
8
|
+
const SECRET_VOLUME_NAME = 'hammerkit-secrets';
|
|
9
|
+
function getSecretResourceName(item) {
|
|
10
|
+
return (0, resources_1.getResourceName)(item, '-secrets');
|
|
11
|
+
}
|
|
12
|
+
exports.getSecretResourceName = getSecretResourceName;
|
|
13
|
+
// The values go into a Secret of the item; the pod only references it, so no
|
|
14
|
+
// value is part of a Job or Deployment spec.
|
|
15
|
+
function getKubernetesSecretRefs(item, secrets) {
|
|
16
|
+
if (secrets.length === 0) {
|
|
17
|
+
return { env: [], mounts: [], volumes: [] };
|
|
18
|
+
}
|
|
19
|
+
const secretName = getSecretResourceName(item);
|
|
20
|
+
const refs = {
|
|
21
|
+
env: [],
|
|
22
|
+
mounts: [],
|
|
23
|
+
volumes: [{ name: SECRET_VOLUME_NAME, secret: { secretName } }],
|
|
24
|
+
};
|
|
25
|
+
secrets.forEach((secret, index) => {
|
|
26
|
+
const key = getSecretKey(index);
|
|
27
|
+
if (secret.target.type === 'env') {
|
|
28
|
+
refs.env.push({ name: secret.target.name, valueFrom: { secretKeyRef: { name: secretName, key } } });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
refs.mounts.push({ name: SECRET_VOLUME_NAME, mountPath: secret.target.path, subPath: key, readOnly: true });
|
|
32
|
+
});
|
|
33
|
+
if (refs.mounts.length === 0) {
|
|
34
|
+
refs.volumes = [];
|
|
35
|
+
}
|
|
36
|
+
return refs;
|
|
37
|
+
}
|
|
38
|
+
exports.getKubernetesSecretRefs = getKubernetesSecretRefs;
|
|
39
|
+
async function ensureKubernetesSecret(instance, kubernetes, item, secrets, environment) {
|
|
40
|
+
if (secrets.length === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const stringData = {};
|
|
44
|
+
for (const [index, secret] of secrets.entries()) {
|
|
45
|
+
stringData[getSecretKey(index)] = await (0, work_secret_1.resolveSecretValue)(secret, environment);
|
|
46
|
+
}
|
|
47
|
+
const spec = {
|
|
48
|
+
kind: 'Secret',
|
|
49
|
+
apiVersion: 'v1',
|
|
50
|
+
metadata: {
|
|
51
|
+
namespace: kubernetes.namespace,
|
|
52
|
+
name: getSecretResourceName(item),
|
|
53
|
+
annotations: {
|
|
54
|
+
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
55
|
+
},
|
|
56
|
+
labels: {
|
|
57
|
+
'hammerkit.dev/id': item.id(),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
type: 'Opaque',
|
|
61
|
+
stringData,
|
|
62
|
+
};
|
|
63
|
+
await (0, apply_1.apply)(instance, spec);
|
|
64
|
+
}
|
|
65
|
+
exports.ensureKubernetesSecret = ensureKubernetesSecret;
|
|
66
|
+
async function removeKubernetesSecret(instance, kubernetes, item) {
|
|
67
|
+
try {
|
|
68
|
+
await instance.coreApi.deleteNamespacedSecret({
|
|
69
|
+
name: getSecretResourceName(item),
|
|
70
|
+
namespace: kubernetes.namespace,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
if ((0, apply_1.statusCodeOf)(e) !== 404) {
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.removeKubernetesSecret = removeKubernetesSecret;
|
|
80
|
+
function getSecretKey(index) {
|
|
81
|
+
return `secret-${index}`;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -24,7 +24,7 @@ async function getKubernetesPersistence(work) {
|
|
|
24
24
|
}
|
|
25
25
|
else {
|
|
26
26
|
for (const volume of work.data.volumes) {
|
|
27
|
-
appendVolume(volume.inherited ?? work, persistence, volume.containerPath, null, false, !!volume.inherited);
|
|
27
|
+
appendVolume(volume.inherited ?? work, persistence, volume.containerPath, null, false, !!volume.inherited || volume.readOnly);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
return persistence;
|
|
@@ -5,6 +5,7 @@ const os_1 = require("os");
|
|
|
5
5
|
const crypto_1 = require("crypto");
|
|
6
6
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
7
7
|
const portable_path_1 = require("../planner/utils/portable-path");
|
|
8
|
+
const work_secret_1 = require("../planner/work-secret");
|
|
8
9
|
// Every path is made project-relative (portablePath) so the description, and the
|
|
9
10
|
// task id hashed from it, is identical wherever the project is checked out.
|
|
10
11
|
//
|
|
@@ -15,6 +16,10 @@ function getWorkTaskCacheDescription(item) {
|
|
|
15
16
|
const task = item.data;
|
|
16
17
|
const portable = (path) => (0, portable_path_1.portablePath)(task.projectRoot, path);
|
|
17
18
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.envs);
|
|
19
|
+
const cachedSecrets = task.secrets
|
|
20
|
+
.filter((secret) => secret.cache)
|
|
21
|
+
.map((secret) => `${(0, work_secret_1.getPortableSecretName)(task.projectRoot, secret)}=${secret.digest()}`)
|
|
22
|
+
.sort();
|
|
18
23
|
return {
|
|
19
24
|
shell: task.shell ?? undefined,
|
|
20
25
|
platform: task.type === 'container-task' ? task.image : (0, os_1.platform)(),
|
|
@@ -33,6 +38,7 @@ function getWorkTaskCacheDescription(item) {
|
|
|
33
38
|
mounts: task.type === 'container-task' ? task.mounts.map((m) => m.mount).sort() : undefined,
|
|
34
39
|
cwd: portable(task.cwd),
|
|
35
40
|
deps: item.deps.length > 0 ? item.deps.map((dep) => dep.id()).sort() : undefined,
|
|
41
|
+
secrets: cachedSecrets.length > 0 ? cachedSecrets : undefined,
|
|
36
42
|
};
|
|
37
43
|
}
|
|
38
44
|
exports.getWorkTaskCacheDescription = getWorkTaskCacheDescription;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSetArguments = void 0;
|
|
4
|
+
// `--set NAME=value` / `--set=NAME=value` on any command, as often as needed:
|
|
5
|
+
// the value is what `$NAME` and `${NAME}` resolve to, ahead of the shell and
|
|
6
|
+
// `.env` files. Taken out of argv before commander parses it, like `--file`.
|
|
7
|
+
function parseSetArguments(argv) {
|
|
8
|
+
const args = [];
|
|
9
|
+
const values = {};
|
|
10
|
+
for (let index = 0; index < argv.length; index++) {
|
|
11
|
+
const arg = argv[index];
|
|
12
|
+
if (arg === '--set') {
|
|
13
|
+
const assignment = argv[index + 1];
|
|
14
|
+
if (assignment === undefined) {
|
|
15
|
+
throw new Error('--set expects NAME=value');
|
|
16
|
+
}
|
|
17
|
+
addAssignment(values, assignment);
|
|
18
|
+
index++;
|
|
19
|
+
}
|
|
20
|
+
else if (arg.startsWith('--set=')) {
|
|
21
|
+
addAssignment(values, arg.substring('--set='.length));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
args.push(arg);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { args, values };
|
|
28
|
+
}
|
|
29
|
+
exports.parseSetArguments = parseSetArguments;
|
|
30
|
+
function addAssignment(values, assignment) {
|
|
31
|
+
const separator = assignment.indexOf('=');
|
|
32
|
+
const name = separator > 0 ? assignment.substring(0, separator) : '';
|
|
33
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
34
|
+
throw new Error(`--set expects NAME=value, got "${assignment}"`);
|
|
35
|
+
}
|
|
36
|
+
values[name] = assignment.substring(separator + 1);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=parse-set-arguments.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getServiceDefinitionHash = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const work_secret_1 = require("./work-secret");
|
|
6
|
+
const work_service_1 = require("./work-service");
|
|
7
|
+
// Everything a running container of the service was created from, plus the
|
|
8
|
+
// definitions of the services it needs: a dependent links to the container it
|
|
9
|
+
// started with, so a recreated need means a recreated dependent too. Stored as
|
|
10
|
+
// a container label (a hash, never values) so `up` and `run` recreate a running
|
|
11
|
+
// service whose definition changed instead of silently reusing it.
|
|
12
|
+
function getServiceDefinitionHash(item) {
|
|
13
|
+
const service = item.data;
|
|
14
|
+
const definition = {
|
|
15
|
+
image: service.image,
|
|
16
|
+
cmd: service.cmd?.cmd ?? null,
|
|
17
|
+
shell: service.shell,
|
|
18
|
+
workdir: service.workdir,
|
|
19
|
+
envs: {
|
|
20
|
+
variables: service.envs.variables,
|
|
21
|
+
replacements: service.envs.replacements.map((r) => [r.key, r.name, r.value]),
|
|
22
|
+
},
|
|
23
|
+
ports: service.ports.map((p) => `${p.hostPort ?? ''}:${p.containerPort}`),
|
|
24
|
+
mounts: service.mounts.map((m) => `${m.localPath}:${m.containerPath}:${m.readOnly ? 'ro' : 'rw'}`),
|
|
25
|
+
volumes: service.volumes.map((v) => `${v.name}:${v.containerPath}:${v.readOnly ? 'ro' : 'rw'}`),
|
|
26
|
+
src: service.src.map((s) => s.absolutePath),
|
|
27
|
+
healthcheck: service.healthcheck?.cmd.cmd ?? null,
|
|
28
|
+
// where secrets go, never their values: a rotated value needs a `down`
|
|
29
|
+
secrets: service.secrets.map((s) => `${s.source.type}:${(0, work_secret_1.getSecretName)(s)}`),
|
|
30
|
+
// the init task's id covers its image, commands, envs and mounts: a changed
|
|
31
|
+
// init recreates the service, so it runs again
|
|
32
|
+
init: service.init ? service.init.task.id() : null,
|
|
33
|
+
needs: item.needs
|
|
34
|
+
.filter((need) => (0, work_service_1.isContainerWorkService)(need.service.data))
|
|
35
|
+
.map((need) => `${need.name}=${getServiceDefinitionHash(need.service)}`),
|
|
36
|
+
};
|
|
37
|
+
return (0, crypto_1.createHash)('sha1').update(JSON.stringify(definition)).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
exports.getServiceDefinitionHash = getServiceDefinitionHash;
|
|
40
|
+
//# sourceMappingURL=service-definition.js.map
|
|
@@ -32,6 +32,7 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
32
32
|
resetOnChange: false,
|
|
33
33
|
containerPath: generate.path,
|
|
34
34
|
inherited: depNode,
|
|
35
|
+
readOnly: false,
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -40,6 +41,7 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
40
41
|
item.data.generates.push({
|
|
41
42
|
volumeName: generate.volumeName,
|
|
42
43
|
export: false,
|
|
44
|
+
exportAlways: false,
|
|
43
45
|
resetOnChange: false,
|
|
44
46
|
path: generate.path,
|
|
45
47
|
inherited: depNode,
|
|
@@ -5,6 +5,10 @@ const append_work_service_1 = require("./append-work-service");
|
|
|
5
5
|
function appendWorkNeeds(workTree, referenced, item, environment, context) {
|
|
6
6
|
for (const need of referenced.needs) {
|
|
7
7
|
const serviceNeed = (0, append_work_service_1.appendWorkService)(workTree, need.service, environment, context);
|
|
8
|
+
if (isInitOf(serviceNeed, item)) {
|
|
9
|
+
// planning the service planned this task as its init, with the view
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
8
12
|
item.needs.push({
|
|
9
13
|
service: serviceNeed,
|
|
10
14
|
name: need.relativeName,
|
|
@@ -15,4 +19,7 @@ function appendWorkNeeds(workTree, referenced, item, environment, context) {
|
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
exports.appendWorkNeeds = appendWorkNeeds;
|
|
22
|
+
function isInitOf(service, item) {
|
|
23
|
+
return service.data.type === 'container-service' && service.data.init?.task === item;
|
|
24
|
+
}
|
|
18
25
|
//# sourceMappingURL=append-work-needs.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getHealthcheckTimeout = exports.DEFAULT_HEALTHCHECK_TIMEOUT = exports.appendWorkService = void 0;
|
|
3
|
+
exports.getHealthcheckTimeout = exports.DEFAULT_HEALTHCHECK_TIMEOUT = exports.getInitTimeout = exports.DEFAULT_INIT_TIMEOUT = exports.getPlannedService = exports.appendWorkService = void 0;
|
|
4
|
+
const work_secret_1 = require("../work-secret");
|
|
4
5
|
const work_service_id_1 = require("../work-service-id");
|
|
5
6
|
const parse_work_ports_1 = require("./parse-work-ports");
|
|
6
7
|
const template_value_1 = require("./template-value");
|
|
@@ -12,6 +13,8 @@ const parse_work_mounts_1 = require("./parse-work-mounts");
|
|
|
12
13
|
const parse_work_source_1 = require("./parse-work-source");
|
|
13
14
|
const append_work_dependencies_1 = require("./append-work-dependencies");
|
|
14
15
|
const append_work_needs_1 = require("./append-work-needs");
|
|
16
|
+
const work_item_1 = require("../work-item");
|
|
17
|
+
const append_work_task_1 = require("./append-work-task");
|
|
15
18
|
const replace_env_variables_1 = require("../../environment/replace-env-variables");
|
|
16
19
|
const state_1 = require("../../executer/state");
|
|
17
20
|
const lazy_resolver_1 = require("../../executer/lazy-resolver");
|
|
@@ -42,6 +45,7 @@ function appendWorkService(workTree, service, environment, context) {
|
|
|
42
45
|
workTree.services[workItem.name] = workStateItem;
|
|
43
46
|
(0, append_work_dependencies_1.appendWorkDependencies)(workTree, service, workStateItem, environment, context);
|
|
44
47
|
(0, append_work_needs_1.appendWorkNeeds)(workTree, service, workStateItem, environment, context);
|
|
48
|
+
appendServiceInit(workTree, service, workStateItem, environment, context);
|
|
45
49
|
return workStateItem;
|
|
46
50
|
}
|
|
47
51
|
else {
|
|
@@ -107,9 +111,71 @@ function parseService(workTree, service, environment, context) {
|
|
|
107
111
|
mounts: (0, parse_work_mounts_1.parseWorkMounts)(service.cwd, service.schema, envs),
|
|
108
112
|
src: (0, parse_work_source_1.parseWorkSource)(service.cwd, service.schema.src, envs),
|
|
109
113
|
caching,
|
|
114
|
+
// set once the service is planned, see appendServiceInit
|
|
115
|
+
init: null,
|
|
116
|
+
secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment),
|
|
110
117
|
};
|
|
111
118
|
}
|
|
112
119
|
}
|
|
120
|
+
// The task named by `init` is planned like any other task. It needs the
|
|
121
|
+
// service under the service's name, but through a view of it that turns running
|
|
122
|
+
// once the healthcheck passed; the service itself only runs for everything else
|
|
123
|
+
// once the init succeeded. The init never makes the service start (it is not
|
|
124
|
+
// among the service's requirers), never comes from the cache, and gets the init
|
|
125
|
+
// timeout unless it declares its own.
|
|
126
|
+
function appendServiceInit(workTree, service, item, environment, context) {
|
|
127
|
+
if (!service.init || !(0, work_item_1.isContainerWorkServiceItem)(item)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const task = (0, append_work_task_1.appendWorkTask)(workTree, service.init.cwd, service.init.task, environment, context);
|
|
131
|
+
const state = new state_1.State({ type: 'pending', stateKey: null });
|
|
132
|
+
const view = { ...item, state };
|
|
133
|
+
initViews.set(view, item);
|
|
134
|
+
const selfNeed = task.needs.findIndex((need) => need.service.name === item.name);
|
|
135
|
+
if (selfNeed >= 0) {
|
|
136
|
+
task.needs[selfNeed] = { name: task.needs[selfNeed].name, service: view };
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
task.needs.push({ name: item.name, service: view });
|
|
140
|
+
}
|
|
141
|
+
removeRequirer(item, task.name);
|
|
142
|
+
task.data.caching = { ...task.data.caching, method: 'none' };
|
|
143
|
+
if (task.data.timeout === null) {
|
|
144
|
+
task.data.timeout = getInitTimeout(undefined, environment);
|
|
145
|
+
}
|
|
146
|
+
item.data.init = { task, state };
|
|
147
|
+
}
|
|
148
|
+
const initViews = new WeakMap();
|
|
149
|
+
// The service itself for the view its init task needs, else the item.
|
|
150
|
+
function getPlannedService(item) {
|
|
151
|
+
return initViews.get(item) ?? item;
|
|
152
|
+
}
|
|
153
|
+
exports.getPlannedService = getPlannedService;
|
|
154
|
+
function removeRequirer(item, name) {
|
|
155
|
+
const index = item.requiredBy.findIndex((requirer) => requirer.name === name);
|
|
156
|
+
if (index >= 0) {
|
|
157
|
+
item.requiredBy.splice(index, 1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.DEFAULT_INIT_TIMEOUT = '5m';
|
|
161
|
+
// The init task's own `timeout`, else HAMMERKIT_INIT_TIMEOUT, else 5 minutes:
|
|
162
|
+
// an init that never ends would otherwise keep the run waiting.
|
|
163
|
+
function getInitTimeout(timeout, environment) {
|
|
164
|
+
if (timeout) {
|
|
165
|
+
return (0, units_1.parseDuration)(timeout);
|
|
166
|
+
}
|
|
167
|
+
const fromEnv = environment.processEnvs.HAMMERKIT_INIT_TIMEOUT;
|
|
168
|
+
if (fromEnv) {
|
|
169
|
+
try {
|
|
170
|
+
return (0, units_1.parseDuration)(fromEnv);
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
throw new Error(`HAMMERKIT_INIT_TIMEOUT: ${(0, log_1.getErrorMessage)(e)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return (0, units_1.parseDuration)(exports.DEFAULT_INIT_TIMEOUT);
|
|
177
|
+
}
|
|
178
|
+
exports.getInitTimeout = getInitTimeout;
|
|
113
179
|
exports.DEFAULT_HEALTHCHECK_TIMEOUT = '20s';
|
|
114
180
|
// A service's own `healthcheck.timeout`, else HAMMERKIT_HEALTHCHECK_TIMEOUT,
|
|
115
181
|
// else 20s: a check that never passes fails the run instead of hanging it.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.appendWorkTask = void 0;
|
|
4
|
+
const work_secret_1 = require("../work-secret");
|
|
4
5
|
const parse_work_generate_1 = require("./parse-work-generate");
|
|
5
6
|
const template_value_1 = require("./template-value");
|
|
6
7
|
const parse_work_mounts_1 = require("./parse-work-mounts");
|
|
@@ -67,6 +68,7 @@ function parseTask(cwd, task, environment, context) {
|
|
|
67
68
|
shell: task.schema.shell ? (0, template_value_1.templateValue)(task.schema.shell, envs) : '/bin/sh',
|
|
68
69
|
continuous: task.schema.continuous ?? false,
|
|
69
70
|
timeout: task.schema.timeout ? (0, units_1.parseDuration)(task.schema.timeout) : null,
|
|
71
|
+
secrets: (0, work_secret_1.parseWorkSecrets)(cwd, context.projectRoot, task.schema.secrets, envs, environment),
|
|
70
72
|
};
|
|
71
73
|
if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
|
|
72
74
|
return {
|
|
@@ -78,6 +80,10 @@ function parseTask(cwd, task, environment, context) {
|
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
82
|
else {
|
|
83
|
+
const fileSecret = baseWorkNode.secrets.find((secret) => secret.target.type === 'file');
|
|
84
|
+
if (fileSecret) {
|
|
85
|
+
throw new Error(`task ${task.relativeName}: a local task takes secrets as env only (${(0, work_secret_1.getSecretName)(fileSecret)})`);
|
|
86
|
+
}
|
|
81
87
|
return {
|
|
82
88
|
...baseWorkNode,
|
|
83
89
|
type: 'local-task',
|
|
@@ -16,6 +16,7 @@ function parseWorkGenerate(cwd, schema, envs) {
|
|
|
16
16
|
volumeName: (0, plan_work_volume_1.getVolumeName)(filePath),
|
|
17
17
|
resetOnChange: generate.resetOnChange,
|
|
18
18
|
export: generate.export,
|
|
19
|
+
exportAlways: generate.exportAlways,
|
|
19
20
|
isFile: (0, path_1.extname)(generate.path).length > 1,
|
|
20
21
|
inherited: null,
|
|
21
22
|
};
|
|
@@ -24,13 +25,14 @@ function parseWorkGenerate(cwd, schema, envs) {
|
|
|
24
25
|
exports.parseWorkGenerate = parseWorkGenerate;
|
|
25
26
|
function mapGenerate(generate) {
|
|
26
27
|
if (typeof generate === 'string') {
|
|
27
|
-
return { path: generate, resetOnChange: true, export: false };
|
|
28
|
+
return { path: generate, resetOnChange: true, export: false, exportAlways: false };
|
|
28
29
|
}
|
|
29
30
|
else {
|
|
30
31
|
return {
|
|
31
32
|
path: generate.path,
|
|
32
33
|
resetOnChange: generate.resetOnChange ?? true,
|
|
33
|
-
export: generate.export
|
|
34
|
+
export: !!generate.export,
|
|
35
|
+
exportAlways: generate.export === 'always',
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -7,22 +7,26 @@ const path_1 = require("path");
|
|
|
7
7
|
function parseWorkMount(cwd, dir) {
|
|
8
8
|
const parts = dir.split(':');
|
|
9
9
|
if (parts.length === 1) {
|
|
10
|
-
return parseLocalMount(cwd, dir, dir, dir);
|
|
10
|
+
return parseLocalMount(cwd, dir, dir, dir, false);
|
|
11
11
|
}
|
|
12
12
|
else if (parts.length === 2) {
|
|
13
|
-
return parseLocalMount(cwd, parts[0], parts[1], dir);
|
|
13
|
+
return parseLocalMount(cwd, parts[0], parts[1], dir, false);
|
|
14
|
+
}
|
|
15
|
+
else if (parts.length === 3 && (parts[2] === 'ro' || parts[2] === 'rw')) {
|
|
16
|
+
return parseLocalMount(cwd, parts[0], parts[1], dir, parts[2] === 'ro');
|
|
14
17
|
}
|
|
15
18
|
else {
|
|
16
19
|
throw new Error(`invalid mount ${dir}`);
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
exports.parseWorkMount = parseWorkMount;
|
|
20
|
-
function parseLocalMount(cwd, localPath, containerPath, mount) {
|
|
23
|
+
function parseLocalMount(cwd, localPath, containerPath, mount, readOnly) {
|
|
21
24
|
return {
|
|
22
25
|
localPath: (0, normalize_path_1.normalizePath)(cwd, (0, os_1.homedir)(), localPath),
|
|
23
26
|
containerPath: (0, normalize_path_1.normalizePath)(cwd, cwd, containerPath),
|
|
24
27
|
isFile: (0, path_1.extname)(localPath).length > 1,
|
|
25
28
|
mount,
|
|
29
|
+
readOnly,
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
32
|
//# sourceMappingURL=parse-work-mount.js.map
|
|
@@ -6,22 +6,29 @@ const plan_work_volume_1 = require("./plan-work-volume");
|
|
|
6
6
|
const template_value_1 = require("./template-value");
|
|
7
7
|
function parseWorkVolume(cwd, volume, envs) {
|
|
8
8
|
if (typeof volume === 'string') {
|
|
9
|
-
|
|
9
|
+
// `$NAME` references resolve first, as in mounts, so a value holding a
|
|
10
|
+
// colon can't break the name:path split
|
|
11
|
+
const value = (0, template_value_1.templateValue)(volume, envs);
|
|
12
|
+
const parts = value.split(':');
|
|
10
13
|
if (parts.length === 2) {
|
|
11
|
-
return parseVolume(cwd, parts[0], parts[1]);
|
|
14
|
+
return parseVolume(cwd, parts[0], parts[1], false);
|
|
15
|
+
}
|
|
16
|
+
else if (parts.length === 3 && (parts[2] === 'ro' || parts[2] === 'rw')) {
|
|
17
|
+
return parseVolume(cwd, parts[0], parts[1], parts[2] === 'ro');
|
|
12
18
|
}
|
|
13
19
|
else {
|
|
14
|
-
throw new Error(`invalid volume ${
|
|
20
|
+
throw new Error(`invalid volume ${value}`);
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
23
|
else {
|
|
18
24
|
const path = (0, template_value_1.templateValue)(volume.path, envs);
|
|
19
25
|
return {
|
|
20
26
|
containerPath: path,
|
|
21
|
-
export: volume.export
|
|
27
|
+
export: !!volume.export,
|
|
22
28
|
name: volume.name ? (0, template_value_1.templateValue)(volume.name, envs) : (0, plan_work_volume_1.getVolumeName)(path),
|
|
23
29
|
inherited: null,
|
|
24
30
|
resetOnChange: volume.resetOnChange ?? false,
|
|
31
|
+
readOnly: volume.readOnly ?? false,
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
}
|
|
@@ -33,13 +40,14 @@ function parseWorkVolumes(cwd, volumes, envs) {
|
|
|
33
40
|
return volumes.map((v) => parseWorkVolume(cwd, v, envs));
|
|
34
41
|
}
|
|
35
42
|
exports.parseWorkVolumes = parseWorkVolumes;
|
|
36
|
-
function parseVolume(cwd, name, containerPath) {
|
|
43
|
+
function parseVolume(cwd, name, containerPath, readOnly) {
|
|
37
44
|
return {
|
|
38
45
|
name,
|
|
39
46
|
resetOnChange: false,
|
|
40
47
|
containerPath: (0, normalize_path_1.normalizePath)(cwd, cwd, containerPath),
|
|
41
48
|
inherited: null,
|
|
42
49
|
export: false,
|
|
50
|
+
readOnly,
|
|
43
51
|
};
|
|
44
52
|
}
|
|
45
53
|
//# sourceMappingURL=parse-work-volume.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getServiceHostname = void 0;
|
|
4
|
+
// A DNS label from the service name: Docker writes the container's hostname
|
|
5
|
+
// into its own /etc/hosts, so the service reaches itself by name (a database
|
|
6
|
+
// that advertises its own address, such as a MongoDB replica set member).
|
|
7
|
+
function getServiceHostname(name) {
|
|
8
|
+
return (name
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
11
|
+
.replace(/^-+|-+$/g, '')
|
|
12
|
+
.substring(0, 63) || 'service');
|
|
13
|
+
}
|
|
14
|
+
exports.getServiceHostname = getServiceHostname;
|
|
15
|
+
//# sourceMappingURL=service-hostname.js.map
|