hammerkit 1.27.0-next.17 → 1.27.0-next.18
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/execution-steps.js +5 -2
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/validate.js +22 -0
- package/dist/remote/remote-work-commands.js +6 -4
- package/dist/remote/server-client.js +15 -2
- package/dist/run-program.js +9 -0
- package/dist/schema/reference-parser.js +8 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/server/plan.js +103 -78
- package/package.json +1 -1
|
@@ -89,8 +89,11 @@ exports.prepareMounts = prepareMounts;
|
|
|
89
89
|
async function setUserPermissions(item, container, environment) {
|
|
90
90
|
if (item.data.user) {
|
|
91
91
|
// On a server only the volumes: a chown as the container's root on a path of the host
|
|
92
|
-
// would follow a link a task put there after the binds were checked
|
|
93
|
-
|
|
92
|
+
// would follow a link a task put there after the binds were checked. Never a mount: it
|
|
93
|
+
// is a path of the host the build file names, such as /var/run/docker.sock, whose owner
|
|
94
|
+
// is not hammerkit's to change
|
|
95
|
+
const mounted = new Set(item.data.mounts.map((mount) => mount.containerPath));
|
|
96
|
+
const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !mounted.has(bind.containerPath) && (!environment.hosted || !(0, path_1.isAbsolute)(bind.localPath)));
|
|
94
97
|
if (!environment.hosted) {
|
|
95
98
|
await (0, set_user_permission_1.setUserPermission)(item.data.cwd, item.status, environment, container, item.data.user);
|
|
96
99
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizePath = void 0;
|
|
3
|
+
exports.normalizeHostPath = exports.getHomeReference = exports.normalizePath = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
function normalizePath(cwd, pwd, path) {
|
|
6
6
|
if (path.startsWith('/')) {
|
|
@@ -14,4 +14,22 @@ function normalizePath(cwd, pwd, path) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
exports.normalizePath = normalizePath;
|
|
17
|
+
// What a path of the host starts with to name the home directory: `~` and `$HOME`, and
|
|
18
|
+
// `$PWD`, which in a mount has always meant the home directory, not the project it reads like
|
|
19
|
+
function getHomeReference(path) {
|
|
20
|
+
for (const reference of ['~', '$HOME']) {
|
|
21
|
+
if (path === reference || path.startsWith(`${reference}/`) || path.startsWith(`${reference}\\`)) {
|
|
22
|
+
return reference;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return path.startsWith('$PWD') ? '$PWD' : null;
|
|
26
|
+
}
|
|
27
|
+
exports.getHomeReference = getHomeReference;
|
|
28
|
+
// A path of the host in a mount: in the home directory when it starts with one of the
|
|
29
|
+
// references above, else in the directory of the build file unless it is absolute
|
|
30
|
+
function normalizeHostPath(cwd, home, path) {
|
|
31
|
+
const reference = getHomeReference(path);
|
|
32
|
+
return reference ? (0, path_1.join)(home, path.substring(reference.length)) : normalizePath(cwd, home, path);
|
|
33
|
+
}
|
|
34
|
+
exports.normalizeHostPath = normalizeHostPath;
|
|
17
35
|
//# sourceMappingURL=normalize-path.js.map
|
|
@@ -7,6 +7,12 @@ const path_1 = require("path");
|
|
|
7
7
|
function parseWorkMount(cwd, dir) {
|
|
8
8
|
const parts = dir.split(':');
|
|
9
9
|
if (parts.length === 1) {
|
|
10
|
+
const home = (0, normalize_path_1.getHomeReference)(dir);
|
|
11
|
+
if (home === '~' || home === '$HOME') {
|
|
12
|
+
// the container has no such directory: the same path on both sides would be one of the
|
|
13
|
+
// project there (`$PWD` has always been, so it stays)
|
|
14
|
+
throw new Error(`mount ${dir} is in the home directory, name where it goes in the container (${dir}:/path)`);
|
|
15
|
+
}
|
|
10
16
|
return parseLocalMount(cwd, dir, dir, dir, false);
|
|
11
17
|
}
|
|
12
18
|
else if (parts.length === 2) {
|
|
@@ -22,7 +28,7 @@ function parseWorkMount(cwd, dir) {
|
|
|
22
28
|
exports.parseWorkMount = parseWorkMount;
|
|
23
29
|
function parseLocalMount(cwd, localPath, containerPath, mount, readOnly) {
|
|
24
30
|
return {
|
|
25
|
-
localPath: (0, normalize_path_1.
|
|
31
|
+
localPath: (0, normalize_path_1.normalizeHostPath)(cwd, (0, os_1.homedir)(), localPath),
|
|
26
32
|
containerPath: (0, normalize_path_1.normalizePath)(cwd, cwd, containerPath),
|
|
27
33
|
isFile: (0, path_1.extname)(localPath).length > 1,
|
|
28
34
|
mount,
|
package/dist/planner/validate.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.checkIfContextExists = exports.hasDependencyCycle = exports.hasNeedCycle
|
|
|
4
4
|
const read_build_file_1 = require("../parser/read-build-file");
|
|
5
5
|
const plan_work_tasks_1 = require("./utils/plan-work-tasks");
|
|
6
6
|
const unreferenced_build_file_envs_1 = require("./unreferenced-build-file-envs");
|
|
7
|
+
const normalize_path_1 = require("./utils/normalize-path");
|
|
7
8
|
async function* validate(workTree, context) {
|
|
8
9
|
const cycleItems = [];
|
|
9
10
|
for (const item of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
|
|
@@ -16,6 +17,10 @@ async function* validate(workTree, context) {
|
|
|
16
17
|
yield { type: 'warn', message: 'missing healthcheck', item: service };
|
|
17
18
|
}
|
|
18
19
|
for (const mount of service.mounts) {
|
|
20
|
+
const pwd = getPwdMountWarning(mount);
|
|
21
|
+
if (pwd) {
|
|
22
|
+
yield { type: 'warn', message: pwd, item: service };
|
|
23
|
+
}
|
|
19
24
|
if (!(await context.file.exists(mount.localPath))) {
|
|
20
25
|
yield {
|
|
21
26
|
type: 'warn',
|
|
@@ -62,6 +67,12 @@ async function* validate(workTree, context) {
|
|
|
62
67
|
item: task,
|
|
63
68
|
};
|
|
64
69
|
}
|
|
70
|
+
for (const mount of task.type === 'container-task' ? task.mounts : []) {
|
|
71
|
+
const pwd = getPwdMountWarning(mount);
|
|
72
|
+
if (pwd) {
|
|
73
|
+
yield { type: 'warn', message: pwd, item: task };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
65
76
|
for (const src of task.src) {
|
|
66
77
|
if (!(await context.file.exists(src.absolutePath))) {
|
|
67
78
|
yield {
|
|
@@ -146,4 +157,15 @@ async function checkIfContextExists(service, context) {
|
|
|
146
157
|
return false;
|
|
147
158
|
}
|
|
148
159
|
exports.checkIfContextExists = checkIfContextExists;
|
|
160
|
+
// `$PWD` in a mount is the home directory, which reads like a path of the project
|
|
161
|
+
function getPwdMountWarning(mount) {
|
|
162
|
+
const source = mount.mount.split(':')[0];
|
|
163
|
+
if ((0, normalize_path_1.getHomeReference)(source) !== '$PWD') {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const rest = source.substring('$PWD'.length);
|
|
167
|
+
const home = rest === '' || rest.startsWith('/') || rest.startsWith('\\') ? `~${rest}` : '~/…';
|
|
168
|
+
return (`mount ${source} is ${mount.localPath}: $PWD in a mount is the home directory, not the project. ` +
|
|
169
|
+
`Write ${home} for the home directory, or a relative path for the project`);
|
|
170
|
+
}
|
|
149
171
|
//# sourceMappingURL=validate.js.map
|
|
@@ -334,7 +334,7 @@ function registerWorkCommands(remote, context) {
|
|
|
334
334
|
throw new Error(`the build file has no task ${(0, server_client_1.printable)(task)}`);
|
|
335
335
|
}
|
|
336
336
|
if (found.key === null) {
|
|
337
|
-
unkeyed.push(
|
|
337
|
+
unkeyed.push({ task, reason: found.reason });
|
|
338
338
|
}
|
|
339
339
|
else {
|
|
340
340
|
keys.push({ task, key: found.key });
|
|
@@ -357,12 +357,14 @@ function registerWorkCommands(remote, context) {
|
|
|
357
357
|
environment.stdout.write(`${(0, server_client_1.printable)(task)}: no run on ${serverName} passed it for these files${check?.reason ? `, ${(0, server_client_1.printable)(check.reason)}` : ''}\n`);
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
for (const
|
|
361
|
-
environment.stdout.write(`${
|
|
360
|
+
for (const { task, reason } of unkeyed) {
|
|
361
|
+
environment.stdout.write(`${(0, server_client_1.printable)(task)}: can not be verified, ${(0, server_client_1.printable)(reason)}\n`);
|
|
362
362
|
}
|
|
363
363
|
if (missing.length > 0 || unkeyed.length > 0) {
|
|
364
364
|
const run = missing.length > 0 ? `; run on the server: hammerkit remote run ${missing.map(server_client_1.printable).join(' ')}` : '';
|
|
365
|
-
|
|
365
|
+
// whole names: a task of an include is named like build:all
|
|
366
|
+
const names = [...missing, ...unkeyed.map(({ task }) => task)].map(server_client_1.printable);
|
|
367
|
+
fail(`not verified: ${names.join(', ')}${run}`, 1);
|
|
366
368
|
}
|
|
367
369
|
}));
|
|
368
370
|
remote
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = exports.printable = exports.ServerError = void 0;
|
|
3
|
+
exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = exports.printableLines = exports.printable = exports.ServerError = void 0;
|
|
4
4
|
const discovery_1 = require("../server/discovery");
|
|
5
5
|
const TIMEOUT_MS = 10000;
|
|
6
6
|
const WRITE_TIMEOUT_MS = 30000;
|
|
@@ -21,6 +21,17 @@ function printable(value) {
|
|
|
21
21
|
return text.length > 300 ? `${text.slice(0, 300)}… (+${text.length - 300} chars)` : text;
|
|
22
22
|
}
|
|
23
23
|
exports.printable = printable;
|
|
24
|
+
// Text of several lines, such as every problem of a refused publish: each line made
|
|
25
|
+
// printable and cut like `printable`, one per line, and at most `MAX_LINES` of them.
|
|
26
|
+
const MAX_LINES = 100;
|
|
27
|
+
function printableLines(value) {
|
|
28
|
+
const lines = String(value)
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.filter((line) => line.trim().length > 0);
|
|
31
|
+
const shown = lines.slice(0, MAX_LINES).map(printable);
|
|
32
|
+
return lines.length > MAX_LINES ? [...shown, `… (+${lines.length - MAX_LINES} lines)`] : shown;
|
|
33
|
+
}
|
|
34
|
+
exports.printableLines = printableLines;
|
|
24
35
|
// The same, never cut: for what somebody decides on, such as a command.
|
|
25
36
|
function printableWhole(value) {
|
|
26
37
|
return String(value)
|
|
@@ -92,7 +103,9 @@ async function callServer(server, token, method, path, body) {
|
|
|
92
103
|
throw new ServerError(`${server.url} did not accept the token`);
|
|
93
104
|
}
|
|
94
105
|
if (!response.ok) {
|
|
95
|
-
const
|
|
106
|
+
const lines = answer && typeof answer.error === 'string' ? printableLines(answer.error) : [];
|
|
107
|
+
// a refused publish says every problem, each on a line of its own
|
|
108
|
+
const said = lines.length > 1 ? `:\n${lines.map((line) => ` ${line}`).join('\n')}` : lines.length === 1 ? `: ${lines[0]}` : '';
|
|
96
109
|
throw new ServerError(`${server.url} answered ${response.status}${said}`, response.status);
|
|
97
110
|
}
|
|
98
111
|
return { status: response.status, body: answer };
|
package/dist/run-program.js
CHANGED
|
@@ -7,6 +7,8 @@ exports.runProgram = void 0;
|
|
|
7
7
|
const program_1 = require("./program");
|
|
8
8
|
const parse_error_1 = require("./schema/parse-error");
|
|
9
9
|
const colors_1 = __importDefault(require("colors"));
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const log_1 = require("./log");
|
|
10
12
|
function runProgram(env, argv, exitOverride) {
|
|
11
13
|
return (0, program_1.getProgram)(env, argv)
|
|
12
14
|
.then(({ program, args }) => {
|
|
@@ -18,8 +20,15 @@ function runProgram(env, argv, exitOverride) {
|
|
|
18
20
|
for (const error of err.zod.errors) {
|
|
19
21
|
env.stderr.write(` ${colors_1.default.underline(colors_1.default.blue(`error`))} at ${colors_1.default.gray(error.path.join('.'))} ${error.message} ${colors_1.default.gray(error.code)}\n`);
|
|
20
22
|
}
|
|
23
|
+
// printed, but still a failure: CI must not pass on a build file that doesn't parse
|
|
24
|
+
throw err;
|
|
21
25
|
}
|
|
22
26
|
else {
|
|
27
|
+
// what commander reports (program.error, a wrong option) is printed already; anything
|
|
28
|
+
// else, such as a build file that is no YAML, would otherwise end the process silently
|
|
29
|
+
if (!(err instanceof commander_1.CommanderError)) {
|
|
30
|
+
env.stderr.write(`${colors_1.default.red('error')}: ${(0, log_1.getErrorMessage)(err)}\n`);
|
|
31
|
+
}
|
|
23
32
|
throw err;
|
|
24
33
|
}
|
|
25
34
|
});
|
|
@@ -210,7 +210,9 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
|
|
|
210
210
|
for (const needName of schema.needs || []) {
|
|
211
211
|
if (typeof needName === 'string') {
|
|
212
212
|
taskOrService.needs.push({
|
|
213
|
-
|
|
213
|
+
// the host name it is reached by: a service of an include, `services:postgres`, as
|
|
214
|
+
// `services-postgres`, since Docker and DNS take no colon
|
|
215
|
+
relativeName: needName.replace(/:/g, '-'),
|
|
214
216
|
service: findService(reference, scope, needName),
|
|
215
217
|
});
|
|
216
218
|
}
|
|
@@ -221,6 +223,11 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
|
|
|
221
223
|
});
|
|
222
224
|
}
|
|
223
225
|
}
|
|
226
|
+
const names = taskOrService.needs.map((need) => need.relativeName);
|
|
227
|
+
const twice = names.find((name, index) => names.indexOf(name) !== index);
|
|
228
|
+
if (twice) {
|
|
229
|
+
throw new Error(`${taskOrService.relativeName} needs two services as ${twice}, give one another name with name:`);
|
|
230
|
+
}
|
|
224
231
|
}
|
|
225
232
|
function findTask(ctx, scope, name, type) {
|
|
226
233
|
const ref = (0, split_name_1.splitName)(name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerGithubAppTokenCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
4
5
|
const log_1 = require("../log");
|
|
5
6
|
const github_app_token_1 = require("./github-app-token");
|
|
6
7
|
// `hammerkit github-app-token <owner/repo>`: a secret provider command for the token a
|
|
@@ -18,7 +19,8 @@ function registerGithubAppTokenCommand(program, environment) {
|
|
|
18
19
|
}
|
|
19
20
|
catch (e) {
|
|
20
21
|
environment.stderr.write(`github-app-token: ${(0, log_1.getErrorMessage)(e)}\n`);
|
|
21
|
-
|
|
22
|
+
// said already: the CLI does not print it again
|
|
23
|
+
throw new commander_1.CommanderError(1, 'hammerkit.githubAppToken', (0, log_1.getErrorMessage)(e));
|
|
22
24
|
}
|
|
23
25
|
});
|
|
24
26
|
}
|
package/dist/server/plan.js
CHANGED
|
@@ -13,6 +13,7 @@ const work_scope_parser_1 = require("../schema/work-scope-parser");
|
|
|
13
13
|
const work_task_1 = require("../planner/work-task");
|
|
14
14
|
const work_service_1 = require("../planner/work-service");
|
|
15
15
|
const hosted_volumes_1 = require("../planner/utils/hosted-volumes");
|
|
16
|
+
const normalize_path_1 = require("../planner/utils/normalize-path");
|
|
16
17
|
const log_1 = require("../log");
|
|
17
18
|
const parse_error_1 = require("../schema/parse-error");
|
|
18
19
|
const task_arguments_1 = require("../planner/utils/task-arguments");
|
|
@@ -165,101 +166,125 @@ function describeSecret(secret) {
|
|
|
165
166
|
// server has no account, so nothing would stand between the build file and the
|
|
166
167
|
// operator's own credentials.
|
|
167
168
|
function findPolicyProblems(items, policy, root) {
|
|
168
|
-
|
|
169
|
+
// the problems of each item, said once for the items of a directory that share one, such
|
|
170
|
+
// as the tasks of an included file that all mount the docker socket
|
|
171
|
+
const grouped = new Map();
|
|
169
172
|
for (const item of items) {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
for (const source of 'src' in data ? data.src : []) {
|
|
178
|
-
if (!source.inherited && insideBundle(root, data.cwd, source.absolutePath).length === 0) {
|
|
179
|
-
problems.push(`${item.name}: src ${source.source} is outside of the bundle`);
|
|
173
|
+
const prefix = `${item.name}: `;
|
|
174
|
+
for (const problem of findItemPolicyProblems(item, policy, root)) {
|
|
175
|
+
const said = problem.startsWith(prefix) ? problem.slice(prefix.length) : problem;
|
|
176
|
+
const key = `${item.data.cwd}\0${said}`;
|
|
177
|
+
const group = grouped.get(key);
|
|
178
|
+
if (group) {
|
|
179
|
+
group.names.push(item.name);
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!generate.inherited && insideBundle(root, data.cwd, generate.path).length === 0) {
|
|
184
|
-
problems.push(`${item.name}: generates ${generate.path}, which is outside of the bundle`);
|
|
181
|
+
else {
|
|
182
|
+
grouped.set(key, { names: [item.name], problem: said });
|
|
185
183
|
}
|
|
186
184
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
}
|
|
186
|
+
return [...grouped.values()].map(({ names: [name, ...others], problem }) => others.length > 0 ? `${name} (also ${others.join(', ')}): ${problem}` : `${name}: ${problem}`);
|
|
187
|
+
}
|
|
188
|
+
function findItemPolicyProblems(item, policy, root) {
|
|
189
|
+
const problems = [];
|
|
190
|
+
const runtime = item.data.type === 'local-task' ? 'local' : 'docker';
|
|
191
|
+
if (policy.runtimes && !policy.runtimes.includes(runtime)) {
|
|
192
|
+
problems.push(`${item.name}: runs on the ${runtime} runtime, which this server does not allow (runtimes: ${policy.runtimes.join(', ')})`);
|
|
193
|
+
}
|
|
194
|
+
const data = item.data;
|
|
195
|
+
// what a task reads and writes is the bundle's: `src: ../..` would have the server
|
|
196
|
+
// copy its own files (its credentials, other callers' caches) into the task
|
|
197
|
+
for (const source of 'src' in data ? data.src : []) {
|
|
198
|
+
if (!source.inherited && insideBundle(root, data.cwd, source.absolutePath).length === 0) {
|
|
199
|
+
problems.push(`${item.name}: src ${source.source} is outside of the bundle`);
|
|
196
200
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
reason: ` to what reads ${reads.join(', ')}`,
|
|
202
|
-
});
|
|
201
|
+
}
|
|
202
|
+
for (const generate of (0, work_task_1.isWorkTask)(data) ? data.generates : []) {
|
|
203
|
+
if (!generate.inherited && insideBundle(root, data.cwd, generate.path).length === 0) {
|
|
204
|
+
problems.push(`${item.name}: generates ${generate.path}, which is outside of the bundle`);
|
|
203
205
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
}
|
|
207
|
+
// what holds a credential gets at most `runs.secretNetwork`, so a tool it runs can't
|
|
208
|
+
// send the credential anywhere else
|
|
209
|
+
const reads = getCredentialReads({
|
|
210
|
+
secrets: secretsOf(item).map(describeSecret),
|
|
211
|
+
serverEnvs: 'envs' in data ? data.envs.processNames ?? [] : [],
|
|
212
|
+
});
|
|
213
|
+
const ceilings = [];
|
|
214
|
+
if (policy.network && (0, work_network_1.isLimitedNetwork)(policy.network)) {
|
|
215
|
+
ceilings.push({ ceiling: policy.network, setting: 'runs.network', reason: '' });
|
|
216
|
+
}
|
|
217
|
+
if (reads.length > 0 && policy.secretNetwork && (0, work_network_1.isLimitedNetwork)(policy.secretNetwork)) {
|
|
218
|
+
ceilings.push({
|
|
219
|
+
ceiling: policy.secretNetwork,
|
|
220
|
+
setting: 'runs.secretNetwork',
|
|
221
|
+
reason: ` to what reads ${reads.join(', ')}`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
for (const { ceiling, setting, reason } of ceilings) {
|
|
225
|
+
// on a cluster a local task is a pod of a pool's tool image, limited like any other
|
|
226
|
+
if (data.type === 'local-task' && policy.backend !== 'kubernetes') {
|
|
227
|
+
problems.push(`${item.name}: is a local task, which can't be limited to the network this server allows${reason} ` +
|
|
228
|
+
`(${setting}: ${(0, work_network_1.formatNetwork)(ceiling)})`);
|
|
214
229
|
}
|
|
215
|
-
if (
|
|
216
|
-
for (
|
|
217
|
-
|
|
218
|
-
if (insideBundle(root, data.cwd, mount.localPath).length === 0) {
|
|
219
|
-
problems.push(`${item.name}: mounts ${mount.localPath}, which is outside of the bundle`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
230
|
+
else if ('network' in data && (0, work_network_1.exceedsNetwork)(data.network, ceiling)) {
|
|
231
|
+
problems.push(`${item.name}: asks for network ${(0, work_network_1.formatNetwork)(data.network)}, more than this server allows${reason} ` +
|
|
232
|
+
`(${setting}: ${(0, work_network_1.formatNetwork)(ceiling)})`);
|
|
222
233
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
234
|
+
}
|
|
235
|
+
if ((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data)) {
|
|
236
|
+
for (const mount of data.mounts) {
|
|
237
|
+
// a path of the server is not the build file's to mount: the docker socket, /etc, a home directory
|
|
238
|
+
if (insideBundle(root, data.cwd, mount.localPath).length === 0) {
|
|
239
|
+
problems.push(`${item.name}: mounts ${describeMountSource(mount)}, which is outside of the bundle`);
|
|
227
240
|
}
|
|
228
241
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// provider only through the egress proxy
|
|
244
|
-
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read from ${source.provider} in the ` +
|
|
245
|
-
`Job of the task, which network ${(0, work_network_1.formatNetwork)(item.data.network)} would cut off from it: ` +
|
|
246
|
-
`this server names no endpoints of ${source.provider} in backend.providers.${source.provider}.allow ` +
|
|
247
|
-
'for the egress proxy to let through, so the task keeps an open network');
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (source.type === 'provider' && policy.reserved?.includes(source.account)) {
|
|
251
|
-
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read as ${source.account}, which the server uses to read repositories`);
|
|
242
|
+
}
|
|
243
|
+
for (const volume of (0, work_service_1.isContainerWorkService)(data) ? data.volumes : []) {
|
|
244
|
+
// docker takes anything else for a path of the host, such as the docker socket
|
|
245
|
+
if (!(0, hosted_volumes_1.isVolumeName)(volume.name)) {
|
|
246
|
+
problems.push(`${item.name}: volume ${volume.name} is not a volume name, a path of the server can't be mounted`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const secret of secretsOf(item)) {
|
|
250
|
+
const source = secret.source;
|
|
251
|
+
if (policy.backend === 'kubernetes' && source.type === 'provider') {
|
|
252
|
+
// the server reads no secret of a provider on a cluster: the Job of a task does
|
|
253
|
+
if (!(0, work_task_1.isWorkTask)(item.data)) {
|
|
254
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read from ${source.provider}, which ` +
|
|
255
|
+
'this server does for tasks only, a service on its cluster gets no provider secret yet');
|
|
252
256
|
}
|
|
253
|
-
else if (
|
|
254
|
-
problems.push(`${item.name}:
|
|
257
|
+
else if (secret.cache) {
|
|
258
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} has cache: true, its digest would need ` +
|
|
259
|
+
'the value before the Job of the task reads it, which this server does not support yet');
|
|
255
260
|
}
|
|
256
|
-
else if (
|
|
257
|
-
|
|
261
|
+
else if ((0, work_network_1.isLimitedNetwork)(item.data.network) && !policy.proxiedProviders?.includes(source.provider)) {
|
|
262
|
+
// the init containers that read it share the pod's NetworkPolicy, and reach the
|
|
263
|
+
// provider only through the egress proxy
|
|
264
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read from ${source.provider} in the ` +
|
|
265
|
+
`Job of the task, which network ${(0, work_network_1.formatNetwork)(item.data.network)} would cut off from it: ` +
|
|
266
|
+
`this server names no endpoints of ${source.provider} in backend.providers.${source.provider}.allow ` +
|
|
267
|
+
'for the egress proxy to let through, so the task keeps an open network');
|
|
258
268
|
}
|
|
259
269
|
}
|
|
270
|
+
if (source.type === 'provider' && policy.reserved?.includes(source.account)) {
|
|
271
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read as ${source.account}, which the server uses to read repositories`);
|
|
272
|
+
}
|
|
273
|
+
else if (source.type === 'file') {
|
|
274
|
+
problems.push(`${item.name}: a secret can not read a file of the server (file:), use a secret provider`);
|
|
275
|
+
}
|
|
276
|
+
else if (source.type === 'env' && !policy.envs.includes(source.name)) {
|
|
277
|
+
problems.push(`${item.name}: the secret ${describeSecret(secret).target} reads env:${source.name}, which the server does not list in ${policy.envsFrom ?? 'runs.env.allow'}`);
|
|
278
|
+
}
|
|
260
279
|
}
|
|
261
280
|
return problems;
|
|
262
281
|
}
|
|
282
|
+
// A mount of the home directory says so: `$PWD` in a mount is the home directory, not the
|
|
283
|
+
// project, which reads like a path of the checkout
|
|
284
|
+
function describeMountSource(mount) {
|
|
285
|
+
const home = (0, normalize_path_1.getHomeReference)(mount.mount.split(':')[0]);
|
|
286
|
+
return home ? `${mount.localPath} (${home} is the home directory)` : mount.localPath;
|
|
287
|
+
}
|
|
263
288
|
function toPlan(root, item) {
|
|
264
289
|
const data = item.data;
|
|
265
290
|
const secrets = secretsOf(item).map(describeSecret);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hammerkit",
|
|
3
3
|
"description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
|
|
4
|
-
"version": "1.27.0-next.
|
|
4
|
+
"version": "1.27.0-next.18",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"build",
|
|
7
7
|
"build-tool",
|