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.
@@ -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
- const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !environment.hosted || !(0, path_1.isAbsolute)(bind.localPath));
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.normalizePath)(cwd, (0, os_1.homedir)(), localPath),
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,
@@ -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(`${(0, server_client_1.printable)(task)}: can not be verified, ${(0, server_client_1.printable)(found.reason)}`);
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 line of unkeyed) {
361
- environment.stdout.write(`${line}\n`);
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
- fail(`not verified: ${[...missing, ...unkeyed.map((line) => line.split(':')[0])].join(', ')}${run}`, 1);
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 said = answer && typeof answer.error === 'string' ? `: ${printable(answer.error)}` : '';
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 };
@@ -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
- relativeName: needName,
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
- throw e;
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
  }
@@ -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
- const problems = [];
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 runtime = item.data.type === 'local-task' ? 'local' : 'docker';
171
- if (policy.runtimes && !policy.runtimes.includes(runtime)) {
172
- problems.push(`${item.name}: runs on the ${runtime} runtime, which this server does not allow (runtimes: ${policy.runtimes.join(', ')})`);
173
- }
174
- const data = item.data;
175
- // what a task reads and writes is the bundle's: `src: ../..` would have the server
176
- // copy its own files (its credentials, other callers' caches) into the task
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
- for (const generate of (0, work_task_1.isWorkTask)(data) ? data.generates : []) {
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
- // what holds a credential gets at most `runs.secretNetwork`, so a tool it runs can't
188
- // send the credential anywhere else
189
- const reads = getCredentialReads({
190
- secrets: secretsOf(item).map(describeSecret),
191
- serverEnvs: 'envs' in data ? data.envs.processNames ?? [] : [],
192
- });
193
- const ceilings = [];
194
- if (policy.network && (0, work_network_1.isLimitedNetwork)(policy.network)) {
195
- ceilings.push({ ceiling: policy.network, setting: 'runs.network', reason: '' });
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
- if (reads.length > 0 && policy.secretNetwork && (0, work_network_1.isLimitedNetwork)(policy.secretNetwork)) {
198
- ceilings.push({
199
- ceiling: policy.secretNetwork,
200
- setting: 'runs.secretNetwork',
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
- for (const { ceiling, setting, reason } of ceilings) {
205
- // on a cluster a local task is a pod of a pool's tool image, limited like any other
206
- if (data.type === 'local-task' && policy.backend !== 'kubernetes') {
207
- problems.push(`${item.name}: is a local task, which can't be limited to the network this server allows${reason} ` +
208
- `(${setting}: ${(0, work_network_1.formatNetwork)(ceiling)})`);
209
- }
210
- else if ('network' in data && (0, work_network_1.exceedsNetwork)(data.network, ceiling)) {
211
- problems.push(`${item.name}: asks for network ${(0, work_network_1.formatNetwork)(data.network)}, more than this server allows${reason} ` +
212
- `(${setting}: ${(0, work_network_1.formatNetwork)(ceiling)})`);
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 ((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data)) {
216
- for (const mount of data.mounts) {
217
- // a path of the server is not the build file's to mount: the docker socket, /etc, a home directory
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
- for (const volume of (0, work_service_1.isContainerWorkService)(data) ? data.volumes : []) {
224
- // docker takes anything else for a path of the host, such as the docker socket
225
- if (!(0, hosted_volumes_1.isVolumeName)(volume.name)) {
226
- problems.push(`${item.name}: volume ${volume.name} is not a volume name, a path of the server can't be mounted`);
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
- for (const secret of secretsOf(item)) {
230
- const source = secret.source;
231
- if (policy.backend === 'kubernetes' && source.type === 'provider') {
232
- // the server reads no secret of a provider on a cluster: the Job of a task does
233
- if (!(0, work_task_1.isWorkTask)(item.data)) {
234
- problems.push(`${item.name}: the secret ${describeSecret(secret).target} is read from ${source.provider}, which ` +
235
- 'this server does for tasks only, a service on its cluster gets no provider secret yet');
236
- }
237
- else if (secret.cache) {
238
- problems.push(`${item.name}: the secret ${describeSecret(secret).target} has cache: true, its digest would need ` +
239
- 'the value before the Job of the task reads it, which this server does not support yet');
240
- }
241
- else if ((0, work_network_1.isLimitedNetwork)(item.data.network) && !policy.proxiedProviders?.includes(source.provider)) {
242
- // the init containers that read it share the pod's NetworkPolicy, and reach the
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 (source.type === 'file') {
254
- problems.push(`${item.name}: a secret can not read a file of the server (file:), use a secret provider`);
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 (source.type === 'env' && !policy.envs.includes(source.name)) {
257
- 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'}`);
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.17",
4
+ "version": "1.27.0-next.18",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",