hammerkit 1.9.2 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.schema.json +52 -1
- package/dist/cache/backends/registry/registry-client.js +19 -4
- package/dist/cli.js +1 -0
- package/dist/docker/pull.js +38 -1
- package/dist/environment/replace-env-variables.js +53 -22
- 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 +8 -3
- package/dist/planner/utils/plan-work-volume.js +1 -0
- package/dist/planner/utils/service-hostname.js +15 -0
- package/dist/planner/utils/template-value.js +1 -1
- 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 +30 -5
- package/dist/utils/redact.js +29 -0
- package/package.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findInitOf = exports.executeInitTask = exports.isTerminalTask = void 0;
|
|
4
|
+
const state_resolver_1 = require("./state-resolver");
|
|
5
|
+
const execute_work_task_1 = require("./execute-work-task");
|
|
6
|
+
const abort_1 = require("./abort");
|
|
7
|
+
const abort_event_1 = require("../utils/abort-event");
|
|
8
|
+
const isTerminalService = (state) => state.type === 'end' || state.type === 'error' || state.type === 'canceled';
|
|
9
|
+
const isTerminalTask = (state) => state.type === 'completed' || state.type === 'error' || state.type === 'crash' || state.type === 'canceled';
|
|
10
|
+
exports.isTerminalTask = isTerminalTask;
|
|
11
|
+
// The init task of a service runs each time the service passed its healthcheck
|
|
12
|
+
// in this run, never on its own: a service left running by an earlier `up` ran
|
|
13
|
+
// its init when it started, so the init is skipped unless it was asked for by
|
|
14
|
+
// name. A failing init fails the run like any failing task.
|
|
15
|
+
async function executeInitTask(work, init, service, requested, environment, options) {
|
|
16
|
+
if (!service || service.state.current.type === 'running') {
|
|
17
|
+
if (service && requested) {
|
|
18
|
+
await (0, execute_work_task_1.executeWorkTask)(work, environment, options);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
skip(work, `${work.name} skipped, the service it initializes does not start in this run`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (requested) {
|
|
25
|
+
// asked for by name, the init is what the service starts for
|
|
26
|
+
work.state.set({ type: 'ready', stateKey: '', started: new Date() });
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
do {
|
|
30
|
+
const view = await (0, state_resolver_1.isState)('await-init-' + work.name, init.state, (state) => state.type === 'running' || isTerminalService(state), environment.abortCtrl.signal);
|
|
31
|
+
if (view.type !== 'running') {
|
|
32
|
+
if (!(0, exports.isTerminalTask)(work.state.current)) {
|
|
33
|
+
skip(work, `${work.name} skipped, ${service.name} did not start`);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await runOnce(work, environment, options);
|
|
38
|
+
settleService(service, work, view);
|
|
39
|
+
if (!options.watch) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await (0, state_resolver_1.awaitState)('await-init-restart-' + work.name, init.state, (state) => state.type !== 'running', environment.abortCtrl.signal);
|
|
43
|
+
} while (!environment.abortCtrl.signal.aborted);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (!(e instanceof abort_1.AbortError)) {
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
if (!(0, exports.isTerminalTask)(work.state.current)) {
|
|
50
|
+
work.state.set({ type: 'canceled', stateKey: null });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.executeInitTask = executeInitTask;
|
|
55
|
+
// Only now does the service run for everything else needing it.
|
|
56
|
+
function settleService(service, work, view) {
|
|
57
|
+
const result = work.state.current;
|
|
58
|
+
if (result.type === 'completed') {
|
|
59
|
+
service.state.set(view);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
service.state.set({
|
|
63
|
+
type: 'error',
|
|
64
|
+
stateKey: null,
|
|
65
|
+
errorMessage: `init ${work.name} did not succeed (${result.type})`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// One run of the init as a plain task. It aborts its own run when it fails;
|
|
69
|
+
// the whole run stops only outside watch mode, as for a failing service.
|
|
70
|
+
async function runOnce(work, environment, options) {
|
|
71
|
+
const abortCtrl = new AbortController();
|
|
72
|
+
const abortListener = (0, abort_event_1.listenOnAbort)(environment.abortCtrl.signal, () => abortCtrl.abort());
|
|
73
|
+
try {
|
|
74
|
+
await (0, execute_work_task_1.executeWorkTask)(work, { ...environment, abortCtrl }, { ...options, watch: false });
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
abortListener.close();
|
|
78
|
+
}
|
|
79
|
+
const result = work.state.current.type;
|
|
80
|
+
if ((result === 'error' || result === 'crash') && !options.watch) {
|
|
81
|
+
environment.abortCtrl.abort();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function skip(work, message) {
|
|
85
|
+
work.status.write('debug', message);
|
|
86
|
+
work.state.set({ type: 'completed', cached: false, skipped: true, duration: 0, stateKey: '' });
|
|
87
|
+
}
|
|
88
|
+
// The service a task is the init of: the task needs it through the view the
|
|
89
|
+
// planner gave it, which shares the service's data.
|
|
90
|
+
function findInitOf(task) {
|
|
91
|
+
for (const need of task.needs) {
|
|
92
|
+
const data = need.service.data;
|
|
93
|
+
if (data.type === 'container-service' && data.init && data.init.task === task) {
|
|
94
|
+
return { serviceName: need.service.name, init: data.init };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
exports.findInitOf = findInitOf;
|
|
100
|
+
//# sourceMappingURL=execute-init-task.js.map
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.executeWorkService = exports.stopService = void 0;
|
|
4
|
+
const work_item_1 = require("../planner/work-item");
|
|
4
5
|
const await_completed_dependencies_1 = require("./await-completed-dependencies");
|
|
5
6
|
const abort_1 = require("./abort");
|
|
6
7
|
const log_1 = require("../log");
|
|
7
8
|
const watch_loop_1 = require("./watch-loop");
|
|
9
|
+
const state_1 = require("./state");
|
|
8
10
|
async function stopService(work) {
|
|
9
11
|
if (work.state.current.type === 'running') {
|
|
10
12
|
await work.runtime.stop();
|
|
@@ -47,12 +49,16 @@ async function executeWorkService(work, environment, options) {
|
|
|
47
49
|
// aborted: the service is stopping anyway
|
|
48
50
|
});
|
|
49
51
|
}
|
|
52
|
+
const init = getServiceInit(work);
|
|
50
53
|
await work.runtime.execute(environment, {
|
|
51
54
|
cache: cacheState,
|
|
52
55
|
abort,
|
|
53
|
-
state: work.state,
|
|
56
|
+
state: init ? gateOnInit(work, init) : work.state,
|
|
54
57
|
stateKey: cacheState.stateKey,
|
|
55
58
|
daemon: options.daemon,
|
|
59
|
+
publishPorts: options.type === 'up' || work.requiredBy.some(isLocalTaskItem) || (!!init && isLocalTaskItem(init.task)),
|
|
60
|
+
// an init needs a healthy service, so a service with one is always awaited
|
|
61
|
+
waitForReady: options.wait === 'ready' || work.requiredBy.length > 0 || !!init,
|
|
56
62
|
});
|
|
57
63
|
});
|
|
58
64
|
}
|
|
@@ -78,4 +84,27 @@ async function executeWorkService(work, environment, options) {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
exports.executeWorkService = executeWorkService;
|
|
87
|
+
function getServiceInit(work) {
|
|
88
|
+
return work.data.type === 'container-service' ? work.data.init : null;
|
|
89
|
+
}
|
|
90
|
+
// With an init, the runtime's `running` (healthcheck passed) first reaches only
|
|
91
|
+
// the init task's view of the service; executeInitTask runs the init and then
|
|
92
|
+
// sets the service running, or failed.
|
|
93
|
+
function gateOnInit(work, init) {
|
|
94
|
+
const gate = new state_1.State(work.state.current);
|
|
95
|
+
gate.on('init-gate', (state) => {
|
|
96
|
+
if (state.type !== 'running') {
|
|
97
|
+
if (work.state.current.type !== 'error') {
|
|
98
|
+
work.state.set(state);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
work.status.write('info', `${work.name} is healthy, run its init ${init.task.name}`);
|
|
103
|
+
init.state.set(state);
|
|
104
|
+
});
|
|
105
|
+
return gate;
|
|
106
|
+
}
|
|
107
|
+
function isLocalTaskItem(item) {
|
|
108
|
+
return (0, work_item_1.isWorkTaskItem)(item) && (0, work_item_1.isLocalWorkTaskItem)(item);
|
|
109
|
+
}
|
|
81
110
|
//# sourceMappingURL=execute-work-service.js.map
|
|
@@ -6,10 +6,10 @@ const abort_1 = require("./abort");
|
|
|
6
6
|
const log_1 = require("../log");
|
|
7
7
|
const await_completed_dependencies_1 = require("./await-completed-dependencies");
|
|
8
8
|
const watch_loop_1 = require("./watch-loop");
|
|
9
|
+
const deadline_1 = require("./deadline");
|
|
9
10
|
const explain_1 = require("../cache/explain");
|
|
10
11
|
const archive_task_entry_1 = require("./archive-task-entry");
|
|
11
12
|
const units_1 = require("../utils/units");
|
|
12
|
-
const abort_event_1 = require("../utils/abort-event");
|
|
13
13
|
async function pushToBackend(work, environment, cacheState, options) {
|
|
14
14
|
const { resolved, stateKey, provable } = cacheState;
|
|
15
15
|
if (resolved.method === 'none' || !provable) {
|
|
@@ -28,29 +28,6 @@ async function pushToBackend(work, environment, cacheState, options) {
|
|
|
28
28
|
work.status.write('warn', `${work.name} failed to push to cache "${resolved.name}": ${(0, log_1.getErrorMessage)(e)}`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
// An abort signal that follows `parent` and additionally fires after `timeout`
|
|
32
|
-
// ms, so a task deadline reuses the runtimes' existing abort/cleanup path.
|
|
33
|
-
function withDeadline(parent, timeout) {
|
|
34
|
-
const controller = new AbortController();
|
|
35
|
-
let expired = false;
|
|
36
|
-
const parentListener = (0, abort_event_1.listenOnAbort)(parent, () => controller.abort());
|
|
37
|
-
const timer = timeout === null
|
|
38
|
-
? null
|
|
39
|
-
: setTimeout(() => {
|
|
40
|
-
expired = true;
|
|
41
|
-
controller.abort();
|
|
42
|
-
}, timeout);
|
|
43
|
-
return {
|
|
44
|
-
signal: controller.signal,
|
|
45
|
-
expired: () => expired,
|
|
46
|
-
clear() {
|
|
47
|
-
if (timer) {
|
|
48
|
-
clearTimeout(timer);
|
|
49
|
-
}
|
|
50
|
-
parentListener.close();
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
31
|
// A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
|
|
55
32
|
// Having no src of its own it is never a cache hit, so its dependencies are
|
|
56
33
|
// always checked and their outputs restored. When every one of them was a hit,
|
|
@@ -118,7 +95,7 @@ dependents = null) {
|
|
|
118
95
|
started,
|
|
119
96
|
});
|
|
120
97
|
const timeout = work.data.timeout ?? options.timeout;
|
|
121
|
-
const deadline = withDeadline(abort, timeout);
|
|
98
|
+
const deadline = (0, deadline_1.withDeadline)(abort, timeout);
|
|
122
99
|
try {
|
|
123
100
|
await work.runtime.execute(environment, {
|
|
124
101
|
cache: cacheState,
|
|
@@ -126,6 +103,8 @@ dependents = null) {
|
|
|
126
103
|
stateKey: cacheState.stateKey,
|
|
127
104
|
state: work.state,
|
|
128
105
|
daemon: options.daemon,
|
|
106
|
+
publishPorts: false,
|
|
107
|
+
waitForReady: true,
|
|
129
108
|
});
|
|
130
109
|
}
|
|
131
110
|
finally {
|
|
@@ -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
|