hammerkit 1.24.0 → 1.26.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/dist/executer/container-task-state.js +8 -6
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +1 -1
- package/dist/optimizer/get-cache-directory.js +5 -0
- package/dist/planner/service-definition.js +3 -2
- package/dist/planner/utils/task-arguments.js +155 -0
- package/dist/planner/work-cache-id.js +15 -1
- package/dist/planner/work-runtime-docker.js +2 -0
- package/dist/planner/work-runtime-kubernetes.js +2 -1
- package/dist/planner/work-runtime-local.js +16 -8
- package/dist/planner/work-secret.js +6 -2
- package/dist/planner/work-service.js +7 -3
- package/dist/planner/work-state-record.js +30 -0
- package/dist/program.js +61 -7
- package/dist/remote/remote-commands.js +16 -12
- package/dist/remote/remote-work-commands.js +75 -3
- package/dist/schema/reference-parser.js +8 -2
- package/dist/schema/work-scope-parser.js +8 -0
- package/dist/secrets/provider-values.js +4 -0
- package/dist/server/accounts-api.js +42 -0
- package/dist/server/approvals-api.js +11 -6
- package/dist/server/definitions-api.js +65 -21
- package/dist/server/definitions.js +54 -142
- package/dist/server/executor.js +180 -181
- package/dist/server/git-checkout.js +55 -3
- package/dist/server/plan.js +296 -0
- package/dist/server/policy.js +98 -50
- package/dist/server/proposals-api.js +30 -8
- package/dist/server/proposals.js +50 -3
- package/dist/server/repo-access.js +127 -0
- package/dist/server/repo.js +94 -0
- package/dist/server/run-environment.js +20 -0
- package/dist/server/run-plan.js +44 -0
- package/dist/server/run-request.js +23 -5
- package/dist/server/run-store.js +61 -0
- package/dist/server/run-worker-command.js +17 -0
- package/dist/server/run-worker-main.js +7 -0
- package/dist/server/run-worker.js +179 -0
- package/dist/server/runs.js +189 -36
- package/dist/server/server-command.js +18 -0
- package/dist/server/server-config.js +203 -42
- package/dist/server/server.js +148 -52
- package/dist/server/sources-api.js +1 -1
- package/package.json +1 -1
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.removeContainerTaskState = exports.writeContainerTaskState = exports.readContainerTaskState = exports.getContainerTaskStateFile = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
6
|
-
const
|
|
6
|
+
const work_state_record_1 = require("../planner/work-state-record");
|
|
7
7
|
// The record says "the outputs of this container task belong to this state
|
|
8
|
-
// key", like the state file of a local task. It lives
|
|
9
|
-
// directory, not the project, so writing it never touches a
|
|
8
|
+
// key", like the state file of a local task (see work-state-record.ts). It lives
|
|
9
|
+
// in the hammerkit directory, not the project, so writing it never touches a
|
|
10
|
+
// task's src; the record name folds in the project root, so two checkouts keep
|
|
11
|
+
// their own.
|
|
10
12
|
function getContainerTaskStateFile(task) {
|
|
11
|
-
return (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'state', (0,
|
|
13
|
+
return (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'state', (0, work_state_record_1.getStateRecordName)(task));
|
|
12
14
|
}
|
|
13
15
|
exports.getContainerTaskStateFile = getContainerTaskStateFile;
|
|
14
16
|
async function readContainerTaskState(environment, task) {
|
|
@@ -16,12 +18,12 @@ async function readContainerTaskState(environment, task) {
|
|
|
16
18
|
if (!(await environment.file.exists(stateFile))) {
|
|
17
19
|
return null;
|
|
18
20
|
}
|
|
19
|
-
return (await environment.file.read(stateFile))
|
|
21
|
+
return (0, work_state_record_1.parseStateRecord)(task, await environment.file.read(stateFile));
|
|
20
22
|
}
|
|
21
23
|
exports.readContainerTaskState = readContainerTaskState;
|
|
22
24
|
async function writeContainerTaskState(environment, task, stateKey) {
|
|
23
25
|
await environment.file.createDirectory((0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'state'));
|
|
24
|
-
await environment.file.writeFile(getContainerTaskStateFile(task), stateKey);
|
|
26
|
+
await environment.file.writeFile(getContainerTaskStateFile(task), (0, work_state_record_1.formatStateRecord)(task, stateKey));
|
|
25
27
|
}
|
|
26
28
|
exports.writeContainerTaskState = writeContainerTaskState;
|
|
27
29
|
async function removeContainerTaskState(environment, task) {
|
|
@@ -30,7 +30,7 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
30
30
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
|
|
31
31
|
const name = (0, resources_1.getResourceName)(service);
|
|
32
32
|
const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
|
|
33
|
-
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data);
|
|
33
|
+
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data, { kubernetes: true });
|
|
34
34
|
const probe = healthcheckCommand
|
|
35
35
|
? {
|
|
36
36
|
exec: {
|
|
@@ -14,7 +14,12 @@ function getCacheStatsFile(cacheId) {
|
|
|
14
14
|
return (0, path_1.join)(getCacheDirectory(cacheId), 'stats.json');
|
|
15
15
|
}
|
|
16
16
|
exports.getCacheStatsFile = getCacheStatsFile;
|
|
17
|
+
// `HAMMERKIT_DIR` puts hammerkit's directory (the cache) elsewhere than below the home directory;
|
|
18
|
+
// a server uses it to keep the caches of runs apart.
|
|
17
19
|
function getHammerkitDirectory() {
|
|
20
|
+
if (process.env.HAMMERKIT_DIR) {
|
|
21
|
+
return process.env.HAMMERKIT_DIR;
|
|
22
|
+
}
|
|
18
23
|
if (process.platform === 'win32') {
|
|
19
24
|
return (0, path_1.join)(process.env.APPDATA, 'hammerkit');
|
|
20
25
|
}
|
|
@@ -4,6 +4,7 @@ exports.getServiceDefinitionHash = void 0;
|
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const work_secret_1 = require("./work-secret");
|
|
6
6
|
const work_service_1 = require("./work-service");
|
|
7
|
+
const work_cache_id_1 = require("./work-cache-id");
|
|
7
8
|
// Everything a running container of the service was created from, plus the
|
|
8
9
|
// definitions of the services it needs: a dependent links to the container it
|
|
9
10
|
// started with, so a recreated need means a recreated dependent too. Stored as
|
|
@@ -30,8 +31,8 @@ function getServiceDefinitionHash(item) {
|
|
|
30
31
|
? `provider:${(0, work_secret_1.getSecretName)(s)}:${(0, work_secret_1.describeSecretSource)(s)}`
|
|
31
32
|
: `${s.source.type}:${(0, work_secret_1.getSecretName)(s)}`),
|
|
32
33
|
// the init task's id covers its image, commands, envs and mounts: a changed
|
|
33
|
-
// init recreates the service, so it runs again
|
|
34
|
-
init: service.init ? service.init.task
|
|
34
|
+
// init recreates the service, so it runs again (arguments after -- don't)
|
|
35
|
+
init: service.init ? (0, work_cache_id_1.getDeclaredWorkTaskId)(service.init.task) : null,
|
|
35
36
|
needs: item.needs
|
|
36
37
|
.filter((need) => (0, work_service_1.isContainerWorkService)(need.service.data))
|
|
37
38
|
.map((need) => `${need.name}=${getServiceDefinitionHash(need.service)}`),
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRefusedByWindowsPowerShell = exports.applyArguments = exports.getArgumentsTarget = exports.getKubernetesCommandArgs = exports.getKubernetesCommandText = exports.escapeKubernetesText = exports.appendArguments = exports.quoteArgument = exports.getShellQuoting = void 0;
|
|
4
|
+
const os_1 = require("os");
|
|
5
|
+
const POSIX_SHELLS = ['sh', 'bash', 'zsh', 'dash', 'ash', 'ksh', 'mksh'];
|
|
6
|
+
const POWERSHELLS = ['powershell', 'pwsh'];
|
|
7
|
+
// The quoting of a shell, by the name of its program; null for a shell whose
|
|
8
|
+
// quoting isn't known (fish, cmd.exe, …). Guessing would let shell syntax in an
|
|
9
|
+
// argument run as a second command, so such a shell takes no arguments.
|
|
10
|
+
function getShellQuoting(shell) {
|
|
11
|
+
const program = shell.trim().split(/\s+/)[0] ?? '';
|
|
12
|
+
const name = (program.split(/[\\/]/).pop() ?? '').toLowerCase().replace(/\.exe$/, '');
|
|
13
|
+
if (POSIX_SHELLS.includes(name)) {
|
|
14
|
+
return 'posix';
|
|
15
|
+
}
|
|
16
|
+
if (POWERSHELLS.includes(name)) {
|
|
17
|
+
return 'powershell';
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
exports.getShellQuoting = getShellQuoting;
|
|
22
|
+
// Single quotes take everything literally in both shells. In sh a quote ends the
|
|
23
|
+
// string, so it is closed, escaped and reopened; PowerShell doubles it, and reads
|
|
24
|
+
// the typographic single quotes as quotes too.
|
|
25
|
+
function quoteArgument(arg, quoting) {
|
|
26
|
+
if (quoting === 'powershell') {
|
|
27
|
+
return `'${arg.replace(/['‘’‚‛]/g, (quote) => quote + quote)}'`;
|
|
28
|
+
}
|
|
29
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
30
|
+
}
|
|
31
|
+
exports.quoteArgument = quoteArgument;
|
|
32
|
+
function appendArguments(command, args, quoting) {
|
|
33
|
+
// an exec-form command is shown as sh would read it; it runs from `parsed`
|
|
34
|
+
const display = quoting === 'exec' ? 'posix' : quoting;
|
|
35
|
+
return {
|
|
36
|
+
cwd: command.cwd,
|
|
37
|
+
cmd: [command.cmd, ...args.map((arg) => quoteArgument(arg, display))].join(' '),
|
|
38
|
+
parsed: {
|
|
39
|
+
command: command.parsed.command,
|
|
40
|
+
args: [...command.parsed.args, ...args],
|
|
41
|
+
},
|
|
42
|
+
arguments: { cmd: command.cmd.length, args: command.parsed.args.length },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
exports.appendArguments = appendArguments;
|
|
46
|
+
// Kubernetes expands `$(NAME)` with the container's env and turns `$$` into `$`
|
|
47
|
+
// in a container's command and args, before any shell sees them, so a quoted
|
|
48
|
+
// argument isn't text there unless its `$` is doubled. The part of the command
|
|
49
|
+
// the build file declares keeps its meaning.
|
|
50
|
+
function escapeKubernetesText(text) {
|
|
51
|
+
return text.replace(/\$/g, '$$$$');
|
|
52
|
+
}
|
|
53
|
+
exports.escapeKubernetesText = escapeKubernetesText;
|
|
54
|
+
function getKubernetesCommandText(command) {
|
|
55
|
+
if (!command.arguments) {
|
|
56
|
+
return command.cmd;
|
|
57
|
+
}
|
|
58
|
+
const from = command.arguments.cmd;
|
|
59
|
+
return command.cmd.slice(0, from) + escapeKubernetesText(command.cmd.slice(from));
|
|
60
|
+
}
|
|
61
|
+
exports.getKubernetesCommandText = getKubernetesCommandText;
|
|
62
|
+
function getKubernetesCommandArgs(command) {
|
|
63
|
+
const from = command.arguments?.args ?? command.parsed.args.length;
|
|
64
|
+
return command.parsed.args.map((arg, index) => (index >= from ? escapeKubernetesText(arg) : arg));
|
|
65
|
+
}
|
|
66
|
+
exports.getKubernetesCommandArgs = getKubernetesCommandArgs;
|
|
67
|
+
// The command arguments after `--` land in: the last command of a task, the
|
|
68
|
+
// `cmd` of a container service. Null when there is none.
|
|
69
|
+
function getArgumentsTarget(item) {
|
|
70
|
+
if (item.type === 'local-task' || item.type === 'container-task') {
|
|
71
|
+
return item.cmds[item.cmds.length - 1] ?? null;
|
|
72
|
+
}
|
|
73
|
+
if (item.type === 'container-service') {
|
|
74
|
+
return item.cmd;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
exports.getArgumentsTarget = getArgumentsTarget;
|
|
79
|
+
// Appends the arguments to the one task or service a run selects, before
|
|
80
|
+
// anything asks for its id: the last command is part of the task id, so the
|
|
81
|
+
// arguments are part of its cache key. A run with arguments is a one-off (one
|
|
82
|
+
// test, one flag), so the task skips the cache unless `keepCache`; even then the
|
|
83
|
+
// key keeps runs with other arguments apart. Dependencies and needed services
|
|
84
|
+
// are planned items of their own and keep their commands and caching. No
|
|
85
|
+
// arguments change nothing, so a task run without them keeps its cache entries.
|
|
86
|
+
function applyArguments(selection, args, keepCache = false) {
|
|
87
|
+
if (args.length === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const selected = [...selection.tasks, ...selection.services];
|
|
91
|
+
if (selected.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (selected.length > 1) {
|
|
95
|
+
throw new Error(`arguments after -- go to one task or service, but this run selects ${selected.map((s) => s.name).join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
if (args.some((arg) => arg.includes('\0'))) {
|
|
98
|
+
throw new Error('an argument after -- contains a NUL character');
|
|
99
|
+
}
|
|
100
|
+
const item = selected[0];
|
|
101
|
+
if (item.type === 'kubernetes-service') {
|
|
102
|
+
throw new Error(`service ${item.name} is a kubernetes service and has no cmd for the arguments after --`);
|
|
103
|
+
}
|
|
104
|
+
const target = getArgumentsTarget(item);
|
|
105
|
+
if (!target) {
|
|
106
|
+
throw new Error(item.type === 'container-service'
|
|
107
|
+
? `service ${item.name} has no cmd for the arguments after --`
|
|
108
|
+
: `task ${item.name} has no command for the arguments after --`);
|
|
109
|
+
}
|
|
110
|
+
const quoting = getItemQuoting(item);
|
|
111
|
+
if (item.type === 'local-task' && quoting === 'powershell') {
|
|
112
|
+
const refused = args.find(isRefusedByWindowsPowerShell);
|
|
113
|
+
if (refused !== undefined) {
|
|
114
|
+
throw new Error(`task ${item.name} runs in Windows PowerShell, which would split or run the argument ${JSON.stringify(refused)}, so it is refused: an empty argument, a ", the characters & | < > ^ % !, line breaks, and a space with a trailing \\`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (item.type === 'container-service') {
|
|
118
|
+
item.cmd = appendArguments(target, args, quoting);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
item.cmds = [...item.cmds.slice(0, -1), appendArguments(target, args, quoting)];
|
|
122
|
+
if (!keepCache) {
|
|
123
|
+
// not implicit, so the run's --cache default doesn't bring the method back
|
|
124
|
+
item.caching = { ...item.caching, method: 'none', implicit: false };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.applyArguments = applyArguments;
|
|
129
|
+
// Windows PowerShell 5.1 builds a new command line for a native program, and
|
|
130
|
+
// starts a .cmd or .bat shim (yarn.cmd, most of node_modules/.bin) through
|
|
131
|
+
// cmd.exe. There an empty argument is dropped, a " splits or joins arguments,
|
|
132
|
+
// an argument with a space that ends in \ is quoted as "a b\" and so escapes
|
|
133
|
+
// its own closing quote, and cmd reads & | < > ^ % ! and a line break as
|
|
134
|
+
// syntax, so such an argument can't be passed as text.
|
|
135
|
+
function isRefusedByWindowsPowerShell(arg) {
|
|
136
|
+
return arg === '' || /["&|<>^%!\r\n]/.test(arg) || (/\s/.test(arg) && arg.endsWith('\\'));
|
|
137
|
+
}
|
|
138
|
+
exports.isRefusedByWindowsPowerShell = isRefusedByWindowsPowerShell;
|
|
139
|
+
function getItemQuoting(item) {
|
|
140
|
+
if (item.type === 'local-task') {
|
|
141
|
+
// what executeCommand runs a local command in
|
|
142
|
+
return (0, os_1.platform)() === 'win32' ? 'powershell' : 'posix';
|
|
143
|
+
}
|
|
144
|
+
const shell = item.type === 'kubernetes-service' ? null : item.shell;
|
|
145
|
+
if (shell === null) {
|
|
146
|
+
return 'exec';
|
|
147
|
+
}
|
|
148
|
+
const quoting = getShellQuoting(shell);
|
|
149
|
+
if (!quoting) {
|
|
150
|
+
throw new Error(`${item.type === 'container-task' ? 'task' : 'service'} ${item.name} runs in ${shell}, whose quoting hammerkit ` +
|
|
151
|
+
`doesn't know, so it takes no arguments after --; use sh, bash, zsh, dash, ash, ksh, mksh or PowerShell`);
|
|
152
|
+
}
|
|
153
|
+
return quoting;
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=task-arguments.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getWorkTaskId = void 0;
|
|
3
|
+
exports.getDeclaredWorkTaskId = exports.getWorkTaskId = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
|
|
6
6
|
function getWorkTaskId(item) {
|
|
@@ -9,4 +9,18 @@ function getWorkTaskId(item) {
|
|
|
9
9
|
return (0, crypto_1.createHash)('sha1').update(jsonData).digest('hex');
|
|
10
10
|
}
|
|
11
11
|
exports.getWorkTaskId = getWorkTaskId;
|
|
12
|
+
// The id of the task as the build file declares it: without the arguments
|
|
13
|
+
// after -- a run appended to its last command. A service's definition takes its
|
|
14
|
+
// init task by this id, so `run <init> -- ...` leaves a running service as it is.
|
|
15
|
+
function getDeclaredWorkTaskId(item) {
|
|
16
|
+
const description = (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item);
|
|
17
|
+
const cmds = description.cmds?.map((cmd, index) => {
|
|
18
|
+
const declared = item.data.cmds[index];
|
|
19
|
+
return declared.arguments ? { ...cmd, cmd: declared.cmd.slice(0, declared.arguments.cmd) } : cmd;
|
|
20
|
+
});
|
|
21
|
+
return (0, crypto_1.createHash)('sha1')
|
|
22
|
+
.update(JSON.stringify({ ...description, cmds }))
|
|
23
|
+
.digest('hex');
|
|
24
|
+
}
|
|
25
|
+
exports.getDeclaredWorkTaskId = getDeclaredWorkTaskId;
|
|
12
26
|
//# sourceMappingURL=work-cache-id.js.map
|
|
@@ -39,6 +39,8 @@ function dockerTaskRuntime(task, workEnvironment) {
|
|
|
39
39
|
});
|
|
40
40
|
},
|
|
41
41
|
async restore(environment, path) {
|
|
42
|
+
// the restored outputs aren't the ones the record names any more
|
|
43
|
+
await (0, container_task_state_1.removeContainerTaskState)(environment, task);
|
|
42
44
|
await restoreContainer(docker, environment, task, path);
|
|
43
45
|
},
|
|
44
46
|
async archive(environment, path) {
|
|
@@ -22,6 +22,7 @@ const ensure_http_route_1 = require("../kubernetes/ensure-http-route");
|
|
|
22
22
|
const find_process_1 = __importDefault(require("find-process"));
|
|
23
23
|
const log_1 = require("../log");
|
|
24
24
|
const version_1 = require("../version");
|
|
25
|
+
const task_arguments_1 = require("./utils/task-arguments");
|
|
25
26
|
const restore_kubernetes_data_1 = require("../kubernetes/restore-kubernetes-data");
|
|
26
27
|
const store_kubernetes_data_1 = require("../kubernetes/store-kubernetes-data");
|
|
27
28
|
const remove_persistent_data_1 = require("../kubernetes/remove-persistent-data");
|
|
@@ -110,7 +111,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
110
111
|
image: task.data.image,
|
|
111
112
|
// through the task's shell, as on Docker
|
|
112
113
|
command: [task.data.shell, '-c'],
|
|
113
|
-
args: [
|
|
114
|
+
args: [(0, task_arguments_1.getKubernetesCommandText)(cmd)],
|
|
114
115
|
workingDir: cmd.cwd,
|
|
115
116
|
env: [
|
|
116
117
|
...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getLocalWorkRuntime = void 0;
|
|
6
|
+
exports.getLocalWorkRuntime = exports.getStateFilename = void 0;
|
|
7
7
|
const tar_1 = require("tar");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const local_task_1 = require("../executer/local-task");
|
|
@@ -11,9 +11,11 @@ const event_cache_1 = require("../executer/event-cache");
|
|
|
11
11
|
const get_outputs_to_reset_1 = require("./utils/get-outputs-to-reset");
|
|
12
12
|
const find_process_1 = __importDefault(require("find-process"));
|
|
13
13
|
const stream_1 = require("stream");
|
|
14
|
+
const work_state_record_1 = require("./work-state-record");
|
|
14
15
|
function getStateFilename(task) {
|
|
15
|
-
return (0, path_1.join)(task.data.cwd, '.hammerkit',
|
|
16
|
+
return (0, path_1.join)(task.data.cwd, '.hammerkit', 'state', (0, work_state_record_1.getStateRecordName)(task));
|
|
16
17
|
}
|
|
18
|
+
exports.getStateFilename = getStateFilename;
|
|
17
19
|
function getPidFilename(task) {
|
|
18
20
|
return (0, path_1.join)(task.data.cwd, '.hammerkit', `${task.id()}.pid`);
|
|
19
21
|
}
|
|
@@ -39,6 +41,8 @@ function getLocalWorkRuntime(task) {
|
|
|
39
41
|
// pidfile check happens in execute (initialize has no Environment in scope)
|
|
40
42
|
},
|
|
41
43
|
async restore(environment, path) {
|
|
44
|
+
// the restored outputs aren't the ones the record names any more
|
|
45
|
+
await removeStateFile(environment, task);
|
|
42
46
|
await restoreLocal(environment, task.data, path);
|
|
43
47
|
},
|
|
44
48
|
async archive(environment, path) {
|
|
@@ -54,7 +58,7 @@ function getLocalWorkRuntime(task) {
|
|
|
54
58
|
});
|
|
55
59
|
return;
|
|
56
60
|
}
|
|
57
|
-
await environment.file.createDirectory((0, path_1.join)(task.data.cwd, '.hammerkit'));
|
|
61
|
+
await environment.file.createDirectory((0, path_1.join)(task.data.cwd, '.hammerkit', 'state'));
|
|
58
62
|
const pidFile = getPidFilename(task);
|
|
59
63
|
await environment.file.writeFile(pidFile, `${process.pid}`);
|
|
60
64
|
try {
|
|
@@ -62,16 +66,14 @@ function getLocalWorkRuntime(task) {
|
|
|
62
66
|
// fails overwrites outputs, so the previous record goes first, and only
|
|
63
67
|
// a run that succeeded writes a new one.
|
|
64
68
|
const stateFile = getStateFilename(task);
|
|
65
|
-
|
|
66
|
-
await environment.file.remove(stateFile);
|
|
67
|
-
}
|
|
69
|
+
await removeStateFile(environment, task);
|
|
68
70
|
for (const generate of (0, get_outputs_to_reset_1.getOutputsToReset)(task.data)) {
|
|
69
71
|
await environment.file.remove(generate.path);
|
|
70
72
|
}
|
|
71
73
|
await (0, local_task_1.localTask)(task, environment, options);
|
|
72
74
|
const failed = ['crash', 'error', 'canceled'].includes(options.state.current.type);
|
|
73
75
|
if (!failed) {
|
|
74
|
-
await environment.file.writeFile(stateFile, options.stateKey);
|
|
76
|
+
await environment.file.writeFile(stateFile, (0, work_state_record_1.formatStateRecord)(task, options.stateKey));
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
finally {
|
|
@@ -95,7 +97,7 @@ function getLocalWorkRuntime(task) {
|
|
|
95
97
|
return null;
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
return await environment.file.read(stateFileName);
|
|
100
|
+
return (0, work_state_record_1.parseStateRecord)(task, await environment.file.read(stateFileName));
|
|
99
101
|
},
|
|
100
102
|
async remove(environment) {
|
|
101
103
|
for (const generate of task.data.generates) {
|
|
@@ -109,6 +111,12 @@ function getLocalWorkRuntime(task) {
|
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
113
|
exports.getLocalWorkRuntime = getLocalWorkRuntime;
|
|
114
|
+
async function removeStateFile(environment, task) {
|
|
115
|
+
const stateFile = getStateFilename(task);
|
|
116
|
+
if (await environment.file.exists(stateFile)) {
|
|
117
|
+
await environment.file.remove(stateFile);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
112
120
|
async function archiveLocal(environment, task, path) {
|
|
113
121
|
for (const generatedArchive of (0, event_cache_1.getArchivePaths)(task.data, path)) {
|
|
114
122
|
await environment.file.writeStream(generatedArchive.filename,
|
|
@@ -66,9 +66,13 @@ function parseSecretSource(secret, ref, cwd, envs, catalog, taskAccount, label)
|
|
|
66
66
|
if (providerRef.length === 0 || providerRef.startsWith('-')) {
|
|
67
67
|
throw new Error(`${label}: a reference for ${kind} must not be empty or start with -`);
|
|
68
68
|
}
|
|
69
|
-
const accountName =
|
|
69
|
+
const accountName = catalog.explicitAccounts
|
|
70
|
+
? secret.account
|
|
71
|
+
: secret.account ?? taskAccount ?? Object.values(catalog.accounts).find((a) => a.default)?.name;
|
|
70
72
|
if (!accountName) {
|
|
71
|
-
throw new Error(
|
|
73
|
+
throw new Error(catalog.explicitAccounts
|
|
74
|
+
? `${label}: ${kind}:${providerRef} needs account: on the secret, a server uses neither a task's account nor a default`
|
|
75
|
+
: `${label}: ${kind}:${providerRef} needs an account, and none is named or marked default: true`);
|
|
72
76
|
}
|
|
73
77
|
const account = catalog.accounts[accountName];
|
|
74
78
|
if (!account) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getHealthcheckCommand = exports.getServiceCommand = exports.getServiceWorkingDir = exports.isContainerWorkService = void 0;
|
|
4
|
+
const task_arguments_1 = require("./utils/task-arguments");
|
|
4
5
|
const isContainerWorkService = (svc) => 'image' in svc && svc.type === 'container-service';
|
|
5
6
|
exports.isContainerWorkService = isContainerWorkService;
|
|
6
7
|
// The container's working directory: the declared `workdir`, else the build
|
|
@@ -10,14 +11,17 @@ exports.getServiceWorkingDir = getServiceWorkingDir;
|
|
|
10
11
|
// The container's entrypoint and command for a service: with a `shell`, its
|
|
11
12
|
// `cmd` runs as `<shell> -c "<cmd>"` in place of the image entrypoint; without
|
|
12
13
|
// one the tokenized `cmd` becomes the command and the image entrypoint stays.
|
|
13
|
-
|
|
14
|
+
// For Kubernetes, the arguments after -- are escaped so they stay text there.
|
|
15
|
+
function getServiceCommand(service, options = {}) {
|
|
14
16
|
if (!service.cmd) {
|
|
15
17
|
return { entrypoint: null, cmd: null };
|
|
16
18
|
}
|
|
17
19
|
if (service.shell) {
|
|
18
|
-
|
|
20
|
+
const cmd = options.kubernetes ? (0, task_arguments_1.getKubernetesCommandText)(service.cmd) : service.cmd.cmd;
|
|
21
|
+
return { entrypoint: [service.shell, '-c'], cmd: [cmd] };
|
|
19
22
|
}
|
|
20
|
-
|
|
23
|
+
const args = options.kubernetes ? (0, task_arguments_1.getKubernetesCommandArgs)(service.cmd) : service.cmd.parsed.args;
|
|
24
|
+
return { entrypoint: null, cmd: [service.cmd.parsed.command, ...args] };
|
|
21
25
|
}
|
|
22
26
|
exports.getServiceCommand = getServiceCommand;
|
|
23
27
|
// The healthcheck as an exec command, through the service's `shell` if it has one.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStateRecord = exports.formatStateRecord = exports.getStateRecordName = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
// The state record says "the outputs at this task's paths were made by this
|
|
6
|
+
// variant of the task (its id), from this state key". It is kept per task
|
|
7
|
+
// *name*, because every variant of a task (other arguments after --, another
|
|
8
|
+
// --set value) writes the same output paths: a record per id would let a run of
|
|
9
|
+
// one variant overwrite the outputs while the record of another one still
|
|
10
|
+
// claims them. The id stored with the key makes the next run of that other
|
|
11
|
+
// variant a miss.
|
|
12
|
+
function getStateRecordName(task) {
|
|
13
|
+
return (0, crypto_1.createHash)('sha1').update(`${task.name}\0${task.data.projectRoot}`).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
exports.getStateRecordName = getStateRecordName;
|
|
16
|
+
function formatStateRecord(task, stateKey) {
|
|
17
|
+
return `${task.id()}:${stateKey}`;
|
|
18
|
+
}
|
|
19
|
+
exports.formatStateRecord = formatStateRecord;
|
|
20
|
+
// The state key of the record when this variant made the outputs, else null.
|
|
21
|
+
function parseStateRecord(task, content) {
|
|
22
|
+
const record = content.trim();
|
|
23
|
+
const separator = record.indexOf(':');
|
|
24
|
+
if (separator < 0 || record.slice(0, separator) !== task.id()) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return record.slice(separator + 1) || null;
|
|
28
|
+
}
|
|
29
|
+
exports.parseStateRecord = parseStateRecord;
|
|
30
|
+
//# sourceMappingURL=work-state-record.js.map
|
package/dist/program.js
CHANGED
|
@@ -27,6 +27,8 @@ const units_1 = require("./utils/units");
|
|
|
27
27
|
const cache_inventory_1 = require("./cache/cache-inventory");
|
|
28
28
|
const remote_commands_1 = require("./remote/remote-commands");
|
|
29
29
|
const server_command_1 = require("./server/server-command");
|
|
30
|
+
const run_worker_command_1 = require("./server/run-worker-command");
|
|
31
|
+
const task_arguments_1 = require("./planner/utils/task-arguments");
|
|
30
32
|
async function createCli(fileName, environment, workScope) {
|
|
31
33
|
const { ctx, scope } = await (0, schema_parser_1.createParseContext)(fileName, environment);
|
|
32
34
|
const referencedScope = await (0, reference_parser_1.parseReferences)(ctx, scope, environment);
|
|
@@ -79,6 +81,31 @@ function printTaskDefinition(environment, definition) {
|
|
|
79
81
|
(0, log_1.printProperty)(environment, `env ${reference.name}`, `from $${reference.from} ${colors_1.default.grey(reference.available ? '(set)' : '(not set)')}`);
|
|
80
82
|
}
|
|
81
83
|
}
|
|
84
|
+
// `ls --json`: what a caller (an agent picking a task) needs to know, with the
|
|
85
|
+
// command that arguments after `--` are appended to (null: it takes none).
|
|
86
|
+
function getListJson(tasks, services) {
|
|
87
|
+
return {
|
|
88
|
+
tasks: tasks.map(({ item }) => ({
|
|
89
|
+
name: item.name,
|
|
90
|
+
description: item.data.description,
|
|
91
|
+
type: item.data.type,
|
|
92
|
+
...(item.data.type === 'container-task' ? { image: item.data.image } : {}),
|
|
93
|
+
deps: item.deps.map((d) => d.name),
|
|
94
|
+
needs: item.needs.map((n) => n.name),
|
|
95
|
+
labels: item.data.labels,
|
|
96
|
+
command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
|
|
97
|
+
})),
|
|
98
|
+
services: services.map(({ item }) => ({
|
|
99
|
+
name: item.name,
|
|
100
|
+
description: item.data.description,
|
|
101
|
+
type: item.data.type,
|
|
102
|
+
...(item.data.type === 'container-service' ? { image: item.data.image } : {}),
|
|
103
|
+
ports: item.data.ports.map((p) => ({ host: p.hostPort, container: p.containerPort })),
|
|
104
|
+
labels: item.data.labels,
|
|
105
|
+
command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
82
109
|
function parseWorkLabelScope(options) {
|
|
83
110
|
const scope = {
|
|
84
111
|
environmentName: null,
|
|
@@ -102,10 +129,20 @@ function parseWorkLabelScope(options) {
|
|
|
102
129
|
}
|
|
103
130
|
return scope;
|
|
104
131
|
}
|
|
132
|
+
// A task given arguments after -- skips the cache, unless the caller asks for a
|
|
133
|
+
// cache method on the command line.
|
|
134
|
+
function keepsCache(command) {
|
|
135
|
+
return command.getOptionValueSource('cache') === 'cli';
|
|
136
|
+
}
|
|
105
137
|
async function getProgram(environment, argv) {
|
|
106
138
|
const program = new commander_1.Command();
|
|
107
139
|
(0, server_command_1.registerServerCommand)(program, environment);
|
|
108
|
-
|
|
140
|
+
(0, run_worker_command_1.registerRunWorkerCommand)(program, environment);
|
|
141
|
+
// What follows `--` belongs to the one task or service of a run or up, not to
|
|
142
|
+
// hammerkit: a `--set` or `--file` there is an argument like any other.
|
|
143
|
+
const separator = argv.indexOf('--');
|
|
144
|
+
const passthrough = separator >= 0 ? argv.slice(separator + 1) : [];
|
|
145
|
+
const parsed = (0, parse_set_arguments_1.parseSetArguments)(separator >= 0 ? argv.slice(0, separator) : argv);
|
|
109
146
|
const args = parsed.args;
|
|
110
147
|
environment.processEnvs = { ...environment.processEnvs, ...parsed.values };
|
|
111
148
|
const fileIndex = args.indexOf('--file');
|
|
@@ -114,17 +151,28 @@ async function getProgram(environment, argv) {
|
|
|
114
151
|
args.splice(fileIndex, 2);
|
|
115
152
|
}
|
|
116
153
|
(0, remote_commands_1.registerRemoteCommands)(program, environment, fileName);
|
|
154
|
+
program.hook('preAction', (_, actionCommand) => {
|
|
155
|
+
const takesArguments = actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name());
|
|
156
|
+
if (passthrough.length > 0 && !takesArguments) {
|
|
157
|
+
program.error(`${actionCommand.name()} takes no arguments after --, only run and up do`, { exitCode: 1 });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
117
160
|
if (await environment.file.exists(fileName)) {
|
|
118
161
|
program
|
|
119
162
|
.command('ls')
|
|
120
163
|
.description('list all tasks')
|
|
121
164
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
122
165
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
166
|
+
.addOption(new commander_1.Option('--json', 'emit the tasks and services as JSON').default(false))
|
|
123
167
|
.action(async (options) => {
|
|
124
168
|
const cli = await createCli(fileName, environment, parseWorkLabelScope(options));
|
|
125
169
|
const items = cli.ls();
|
|
126
170
|
const tasks = items.filter(cli_1.isCliTask);
|
|
127
171
|
const services = items.filter(cli_1.isCliService);
|
|
172
|
+
if (options.json) {
|
|
173
|
+
environment.stdout.write(`${JSON.stringify(getListJson(tasks, services), null, 2)}\n`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
128
176
|
if (services.length > 0) {
|
|
129
177
|
(0, log_1.printTitle)(environment, 'Services');
|
|
130
178
|
for (const service of services) {
|
|
@@ -525,7 +573,8 @@ async function getProgram(environment, argv) {
|
|
|
525
573
|
});
|
|
526
574
|
program
|
|
527
575
|
.command('up')
|
|
528
|
-
.description(
|
|
576
|
+
.description("start services(s), with the arguments after -- appended to the selected service's cmd")
|
|
577
|
+
.usage('[options] [-- args...]')
|
|
529
578
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
530
579
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
531
580
|
.addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count (default 4, unbounded when every task and service requests cpus and memory)').argParser(parseInt))
|
|
@@ -544,9 +593,12 @@ async function getProgram(environment, argv) {
|
|
|
544
593
|
.choices(['checksum', 'modify-date', 'none']))
|
|
545
594
|
.addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
|
|
546
595
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
547
|
-
.action(async (options) => {
|
|
596
|
+
.action(async (options, command) => {
|
|
548
597
|
try {
|
|
549
|
-
const scope =
|
|
598
|
+
const scope = {
|
|
599
|
+
...parseWorkLabelScope(options),
|
|
600
|
+
args: { values: passthrough, services: true, cache: keepsCache(command) },
|
|
601
|
+
};
|
|
550
602
|
const cli = await createCli(fileName, environment, scope);
|
|
551
603
|
const result = await cli.runUp({
|
|
552
604
|
cacheDefault: options.cache,
|
|
@@ -599,7 +651,8 @@ async function getProgram(environment, argv) {
|
|
|
599
651
|
});
|
|
600
652
|
program
|
|
601
653
|
.command('run [task]', { isDefault: true })
|
|
602
|
-
.description('execute task')
|
|
654
|
+
.description('execute task, with the arguments after -- appended to its last command')
|
|
655
|
+
.usage('[options] [task] [-- args...]')
|
|
603
656
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
604
657
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
605
658
|
.addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count (default 4, unbounded when every task and service requests cpus and memory)').argParser(parseInt))
|
|
@@ -619,14 +672,15 @@ async function getProgram(environment, argv) {
|
|
|
619
672
|
.addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
|
|
620
673
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
621
674
|
.addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when nothing needing them has to run'))
|
|
622
|
-
.action(async (task, options) => {
|
|
675
|
+
.action(async (task, options, command) => {
|
|
623
676
|
try {
|
|
624
677
|
// For machine-readable JSON, suppress the human progress logger (which
|
|
625
678
|
// also writes to stdout) so the only thing on stdout is the JSON.
|
|
626
679
|
const runEnvironment = options.summaryJson
|
|
627
680
|
? { ...environment, stdout: (0, empty_writable_1.emptyWritable)(), console: (0, log_1.consoleContext)((0, empty_writable_1.emptyWritable)()) }
|
|
628
681
|
: environment;
|
|
629
|
-
const
|
|
682
|
+
const args = { values: passthrough, services: false, cache: keepsCache(command) };
|
|
683
|
+
const cli = await createCli(fileName, runEnvironment, task ? { taskName: task, environmentName: options.env, args } : { ...parseWorkLabelScope(options), args });
|
|
630
684
|
if (cli.tasks().length === 0) {
|
|
631
685
|
program.error('No tasks found', { exitCode: 127 });
|
|
632
686
|
return;
|
|
@@ -108,7 +108,7 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
108
108
|
const me = await (0, server_client_1.getMe)(server, result.accessToken);
|
|
109
109
|
environment.stdout.write(`signed in to ${name} as ${me.sub}\n`);
|
|
110
110
|
environment.stdout.write(me.accounts.length > 0
|
|
111
|
-
? `service accounts: ${me.accounts.join(', ')}\n`
|
|
111
|
+
? `service accounts: ${me.accounts.map((a) => `${(0, server_client_1.printable)(a.name)} (${a.level})`).join(', ')}\n`
|
|
112
112
|
: 'you have no service accounts on this server\n');
|
|
113
113
|
}
|
|
114
114
|
catch (e) {
|
|
@@ -131,21 +131,25 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
131
131
|
const chosen = await getServer(name);
|
|
132
132
|
const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
|
|
133
133
|
const me = await (0, server_client_1.getMe)(chosen.server, token);
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
const { body } = await (0, server_client_1.callServer)(chosen.server, token, 'GET', '/v1/accounts');
|
|
135
|
+
environment.stdout.write(`${chosen.name}: ${(0, server_client_1.printable)(me.sub)} (${(0, server_client_1.printable)(me.iss)})\n`);
|
|
136
|
+
if (body.accounts.length === 0) {
|
|
136
137
|
environment.stdout.write('no service accounts\n');
|
|
137
138
|
}
|
|
138
|
-
for (const account of
|
|
139
|
-
|
|
139
|
+
for (const account of body.accounts) {
|
|
140
|
+
const level = account.level === 'use' ? 'use' : 'request, somebody who uses it approves';
|
|
141
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(account.name))} ${level}${account.description ? ` ${(0, server_client_1.printable)(account.description)}` : ''} from ${account.sources.map(server_client_1.printable).join(', ')}\n`);
|
|
140
142
|
}
|
|
141
|
-
if (me.
|
|
142
|
-
|
|
143
|
+
if (me.builds) {
|
|
144
|
+
const may = {
|
|
145
|
+
run: 'run published build files',
|
|
146
|
+
propose: 'propose build files, which somebody who publishes approves',
|
|
147
|
+
publish: 'publish build files',
|
|
148
|
+
}[me.builds];
|
|
149
|
+
environment.stdout.write(`you may ${may}\n`);
|
|
143
150
|
}
|
|
144
|
-
if (me.
|
|
145
|
-
environment.stdout.write(`
|
|
146
|
-
}
|
|
147
|
-
if (me.request && me.request.length > 0) {
|
|
148
|
-
environment.stdout.write(`may request runs, which an admin approves: ${me.request.join(', ')}\n`);
|
|
151
|
+
if (me.repos.length > 0) {
|
|
152
|
+
environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
|
|
149
153
|
}
|
|
150
154
|
}));
|
|
151
155
|
(0, remote_work_commands_1.registerWorkCommands)(remote, { environment, buildFile, getServer, guarded });
|