hammerkit 1.27.0-next.8 → 2.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/build.schema.json +176 -6
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry/registry-client.js +1 -0
  4. package/dist/cache/cache-sync.js +1 -1
  5. package/dist/cache/explain.js +17 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
  8. package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
  9. package/dist/dashboard/favicon.svg +1 -0
  10. package/dist/dashboard/index.html +15 -0
  11. package/dist/docker/item-network.js +13 -5
  12. package/dist/executer/archive-task-entry.js +4 -2
  13. package/dist/executer/deadline.js +17 -3
  14. package/dist/executer/deferred-restore.js +36 -0
  15. package/dist/executer/docker-service.js +30 -8
  16. package/dist/executer/docker-task.js +21 -1
  17. package/dist/executer/execute-work-service.js +2 -0
  18. package/dist/executer/execute-work-task.js +11 -4
  19. package/dist/executer/execution-steps.js +25 -4
  20. package/dist/executer/reset-work-tree.js +4 -0
  21. package/dist/executer/scheduler/enqueue-next.js +57 -6
  22. package/dist/executer/scheduler/state-key.js +42 -5
  23. package/dist/executer/watch-loop.js +6 -3
  24. package/dist/harness/connect.js +20 -6
  25. package/dist/harness/mcp-command.js +22 -6
  26. package/dist/harness/mcp.js +63 -114
  27. package/dist/harness/opencode.js +158 -50
  28. package/dist/harness/pi.js +29 -0
  29. package/dist/harness/sandbox-client.js +194 -23
  30. package/dist/harness/service-tools.js +141 -0
  31. package/dist/harness/task-tools.js +221 -0
  32. package/dist/kubernetes/apply.js +76 -17
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-http-route.js +1 -1
  36. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +138 -18
  37. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  38. package/dist/kubernetes/ensure-persistent-data.js +212 -10
  39. package/dist/kubernetes/follow-pod-logs.js +182 -0
  40. package/dist/kubernetes/get-service-ip.js +2 -2
  41. package/dist/kubernetes/job-placement.js +36 -12
  42. package/dist/kubernetes/job-secrets.js +9 -6
  43. package/dist/kubernetes/kept-claims.js +22 -10
  44. package/dist/kubernetes/kubernetes-instance.js +1 -0
  45. package/dist/kubernetes/network-policy.js +16 -8
  46. package/dist/kubernetes/remove-persistent-data.js +3 -1
  47. package/dist/kubernetes/remove-stale-items.js +46 -0
  48. package/dist/kubernetes/resources.js +25 -3
  49. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  50. package/dist/kubernetes/sandbox-runner.js +628 -0
  51. package/dist/kubernetes/secrets.js +7 -11
  52. package/dist/kubernetes/service-rollouts.js +74 -0
  53. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  54. package/dist/kubernetes/volumes.js +96 -11
  55. package/dist/main.js +2 -0
  56. package/dist/optimizer/calculate-checksum.js +3 -2
  57. package/dist/optimizer/check-key.js +94 -0
  58. package/dist/optimizer/check-keys.js +64 -0
  59. package/dist/optimizer/get-work-cache-stats.js +17 -3
  60. package/dist/optimizer/work-task-cache-description.js +49 -1
  61. package/dist/planner/service-definition.js +9 -4
  62. package/dist/planner/utils/append-work-dependencies.js +5 -3
  63. package/dist/planner/utils/append-work-service.js +18 -1
  64. package/dist/planner/utils/append-work-task.js +13 -3
  65. package/dist/planner/utils/get-work-runtime.js +6 -1
  66. package/dist/planner/utils/normalize-path.js +19 -1
  67. package/dist/planner/utils/parse-work-mount.js +7 -1
  68. package/dist/planner/utils/parse-work-port.js +4 -1
  69. package/dist/planner/validate.js +22 -0
  70. package/dist/planner/work-network.js +3 -1
  71. package/dist/planner/work-runtime-aggregate.js +32 -0
  72. package/dist/planner/work-runtime-kubernetes.js +153 -26
  73. package/dist/planner/work-task.js +6 -1
  74. package/dist/program.js +27 -9
  75. package/dist/remote/device-login.js +7 -2
  76. package/dist/remote/host-config.js +69 -3
  77. package/dist/remote/remote-commands.js +33 -7
  78. package/dist/remote/remote-status.js +112 -0
  79. package/dist/remote/remote-work-commands.js +562 -45
  80. package/dist/remote/server-client.js +15 -2
  81. package/dist/remote/token-source.js +64 -26
  82. package/dist/run-program.js +9 -0
  83. package/dist/schema/build-file-base-task-schema.js +3 -3
  84. package/dist/schema/build-file-container-service-schema.js +10 -0
  85. package/dist/schema/build-file-container-task-schema.js +16 -1
  86. package/dist/schema/build-file-include-schema.js +18 -4
  87. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  88. package/dist/schema/build-file-task-schema.js +5 -1
  89. package/dist/schema/capabilities-schema.js +56 -0
  90. package/dist/schema/reference-parser.js +119 -11
  91. package/dist/schema/removed-properties.js +20 -0
  92. package/dist/schema/schema-parser.js +94 -41
  93. package/dist/schema/work-scope-parser.js +33 -10
  94. package/dist/secrets/command-secret-provider.js +3 -1
  95. package/dist/secrets/github-app-token-command.js +3 -1
  96. package/dist/secrets/secret-catalog.js +43 -19
  97. package/dist/server/accounts-api.js +4 -1
  98. package/dist/server/approvals-api.js +1 -1
  99. package/dist/server/capacity-format.js +36 -0
  100. package/dist/server/capacity.js +300 -0
  101. package/dist/server/checks-api.js +83 -0
  102. package/dist/server/checks.js +116 -0
  103. package/dist/server/dashboard.js +196 -0
  104. package/dist/server/definitions-api.js +66 -3
  105. package/dist/server/endpoints.js +794 -0
  106. package/dist/server/executor.js +164 -9
  107. package/dist/server/kubernetes-backend.js +478 -70
  108. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  109. package/dist/server/overview.js +271 -0
  110. package/dist/server/plan.js +170 -121
  111. package/dist/server/policy.js +34 -2
  112. package/dist/server/proposals-api.js +17 -8
  113. package/dist/server/repo-access.js +5 -0
  114. package/dist/server/run-logs.js +4 -0
  115. package/dist/server/run-plan.js +38 -4
  116. package/dist/server/run-request.js +58 -3
  117. package/dist/server/run-worker.js +262 -13
  118. package/dist/server/runs.js +969 -36
  119. package/dist/server/sandbox-pod.js +28 -0
  120. package/dist/server/sandbox-storage.js +23 -0
  121. package/dist/server/sandboxes.js +8 -0
  122. package/dist/server/server-config.js +246 -10
  123. package/dist/server/server.js +429 -32
  124. package/dist/server/service-runs.js +82 -0
  125. package/dist/server/timings.js +132 -0
  126. package/dist/server/wait-reason.js +21 -0
  127. package/dist/utils/tar.js +28 -1
  128. package/dist/utils/terminal-prompt.js +53 -0
  129. package/package.json +8 -2
@@ -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 };
@@ -9,6 +9,54 @@ function isToken(value) {
9
9
  return /^[\x21-\x7e]+$/.test(value);
10
10
  }
11
11
  exports.isToken = isToken;
12
+ // What a failed token command said on stderr, for the user's own terminal: never its stdout,
13
+ // and never in an error message, which a harness hands on to an agent.
14
+ const MAX_STDERR_LINES = 20;
15
+ const MAX_STDERR_LINE = 300;
16
+ function writeStderr(environment, what, stderr) {
17
+ const lines = stderr
18
+ .split(/\r?\n/)
19
+ .map((line) => line.replace(/[^\x20-\x7e\t]/g, '?').slice(0, MAX_STDERR_LINE))
20
+ .filter((line) => line.trim())
21
+ .slice(-MAX_STDERR_LINES);
22
+ for (const line of lines) {
23
+ environment.stderr.write(`${what}: ${line}\n`);
24
+ }
25
+ }
26
+ function commandSource(environment, command, what) {
27
+ let current;
28
+ const run = () => new Promise((resolve, reject) => {
29
+ (0, child_process_1.exec)(command, { env: environment.processEnvs, timeout: 60000 }, (error, stdout, stderr) => {
30
+ const token = String(stdout).trim();
31
+ if (error || !token) {
32
+ writeStderr(environment, what, String(stderr));
33
+ const status = error?.killed
34
+ ? ', it did not end within a minute'
35
+ : typeof error?.code === 'number'
36
+ ? `, it exited with ${error.code}`
37
+ : '';
38
+ reject(new Error(`${what} printed no token${status}`));
39
+ }
40
+ else if (!isToken(token)) {
41
+ // never the output itself: it can hold more secrets than the token
42
+ reject(new Error(`${what} printed more than a token, it has to print the token alone`));
43
+ }
44
+ else {
45
+ resolve(token);
46
+ }
47
+ });
48
+ });
49
+ return {
50
+ token: () => {
51
+ current = current ?? run();
52
+ current.catch(() => (current = undefined));
53
+ return current;
54
+ },
55
+ renew: async () => {
56
+ current = undefined;
57
+ },
58
+ };
59
+ }
12
60
  /**
13
61
  * The token a client of a server sends: a harness, and the `remote` commands. In this order:
14
62
  *
@@ -16,42 +64,29 @@ exports.isToken = isToken;
16
64
  * identity, such as a workload identity or a service account of the identity provider. It
17
65
  * runs again when the server no longer accepts the token.
18
66
  * - `HAMMERKIT_TOKEN`: such a token, as the agent's platform hands it over.
67
+ * - the token command of the server in the host config (`remote add --token-command`): the
68
+ * same as `HAMMERKIT_TOKEN_COMMAND`, for one server, so each server can have the service
69
+ * account and audience it accepts. It only ever comes from the host config, never from a
70
+ * build file: whoever writes it decides what runs on this machine.
19
71
  * - the sign-in of `hammerkit remote login <server>`.
20
72
  *
21
73
  * The token only goes to the URL the server is registered with in the host config.
22
74
  */
23
75
  function getTokenSource(environment, name, server) {
76
+ // a variable holds for every server: say so when it takes the place of the one registered
77
+ const overrides = (variable) => {
78
+ if (server.tokenCommand) {
79
+ environment.stderr.write(`${variable} is set, so the token command of ${name} is not used\n`);
80
+ }
81
+ };
24
82
  const command = environment.processEnvs.HAMMERKIT_TOKEN_COMMAND;
25
83
  if (command) {
26
- let current;
27
- const run = () => new Promise((resolve, reject) => {
28
- (0, child_process_1.exec)(command, { env: environment.processEnvs, timeout: 60000 }, (error, stdout) => {
29
- const token = String(stdout).trim();
30
- if (error || !token) {
31
- reject(new Error('HAMMERKIT_TOKEN_COMMAND printed no token'));
32
- }
33
- else if (!isToken(token)) {
34
- // never the output itself: it can hold more secrets than the token
35
- reject(new Error('HAMMERKIT_TOKEN_COMMAND printed more than a token, it has to print the token alone'));
36
- }
37
- else {
38
- resolve(token);
39
- }
40
- });
41
- });
42
- return {
43
- token: () => {
44
- current = current ?? run();
45
- current.catch(() => (current = undefined));
46
- return current;
47
- },
48
- renew: async () => {
49
- current = undefined;
50
- },
51
- };
84
+ overrides('HAMMERKIT_TOKEN_COMMAND');
85
+ return commandSource(environment, command, 'HAMMERKIT_TOKEN_COMMAND');
52
86
  }
53
87
  const fixed = environment.processEnvs.HAMMERKIT_TOKEN?.trim();
54
88
  if (fixed) {
89
+ overrides('HAMMERKIT_TOKEN');
55
90
  return {
56
91
  token: async () => {
57
92
  if (!isToken(fixed)) {
@@ -61,6 +96,9 @@ function getTokenSource(environment, name, server) {
61
96
  },
62
97
  };
63
98
  }
99
+ if (server.tokenCommand) {
100
+ return commandSource(environment, server.tokenCommand, `the token command of ${name}`);
101
+ }
64
102
  return { token: () => (0, credentials_1.getAccessToken)(environment, name, server) };
65
103
  }
66
104
  exports.getTokenSource = getTokenSource;
@@ -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
  });
@@ -29,9 +29,6 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
29
29
  src: (0, zod_1.array)((0, zod_1.string)()).optional(),
30
30
  generates: (0, zod_1.array)(build_file_volume_schema_1.buildFileVolumeSchema).optional(),
31
31
  shell: (0, zod_1.string)().optional(),
32
- // A task that watches its own sources and restarts itself (e.g. `ng serve`,
33
- // `tsc -w`). In watch mode hammerkit then does not watch/restart it itself.
34
- continuous: (0, zod_1.boolean)().optional(),
35
32
  // Maximum execution time, e.g. `30s`, `10m`, `1h30m`. The task fails when it
36
33
  // runs longer. Does not affect the cache key (ADR-0002).
37
34
  timeout: duration_schema_1.durationSchema.optional(),
@@ -49,5 +46,8 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
49
46
  // Name of the server (see `hammerkit remote`) the task has to run on; defaults
50
47
  // to the build file's `runner`. Never a local fallback when it is unknown.
51
48
  runner: runner_schema_1.runnerSchema.optional(),
49
+ // false: a server leaves it out of a published version, for a task only this machine
50
+ // runs (a dev watcher, a task that mounts the docker socket)
51
+ publish: (0, zod_1.boolean)().optional(),
52
52
  });
53
53
  //# sourceMappingURL=build-file-base-task-schema.js.map
@@ -41,12 +41,22 @@ exports.buildFileContainerServiceSchema = (0, zod_1.object)({
41
41
  // the service account its provider secrets are read as, unless a secret names one
42
42
  account: (0, zod_1.string)().optional(),
43
43
  resources: resources_schema_1.resourcesSchema.optional(),
44
+ // on Kubernetes, how often its container may crash and be restarted before the
45
+ // service fails (2 by default): it fails when it crashes once more
46
+ restarts: (0, zod_1.number)()
47
+ .int()
48
+ .min(0)
49
+ .max(10)
50
+ .describe('crashes restarted before it fails on Kubernetes, 0 to 10, 2 by default')
51
+ .optional(),
44
52
  // on Kubernetes, the size of the claim that holds its sources, mounts and volumes
45
53
  volumeSize: size_schema_1.sizeSchema.describe('size of its claim on Kubernetes, e.g. 10Gi').optional(),
46
54
  // what the service reaches besides its needs; tasks needing it always reach it
47
55
  network: network_schema_1.networkSchema.optional(),
48
56
  // whether its `cmd` takes the arguments after `--` of `up`: false refuses them
49
57
  args: (0, zod_1.boolean)().optional(),
58
+ // false: a server leaves it out of a published version
59
+ publish: (0, zod_1.boolean)().optional(),
50
60
  })
51
61
  .strict()
52
62
  .describe('container service');
@@ -1,16 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildFileContainerTaskSchema = void 0;
3
+ exports.CONTAINER_TASK_PROPERTIES = exports.buildFileExtendingContainerTaskSchema = exports.buildFileContainerTaskSchema = void 0;
4
4
  const build_file_base_task_schema_1 = require("./build-file-base-task-schema");
5
5
  const zod_1 = require("zod");
6
6
  const size_schema_1 = require("./size-schema");
7
+ const capabilities_schema_1 = require("./capabilities-schema");
7
8
  exports.buildFileContainerTaskSchema = build_file_base_task_schema_1.buildFileBaseTaskSchema
8
9
  .extend({
9
10
  image: (0, zod_1.string)(),
10
11
  mounts: (0, zod_1.array)((0, zod_1.string)()).optional(),
11
12
  // on Kubernetes, the size of the claim that holds its sources, mounts and outputs
12
13
  volumeSize: size_schema_1.sizeSchema.describe('size of its claim on Kubernetes, e.g. 10Gi').optional(),
14
+ // Linux capabilities added to the container, such as SYS_ADMIN for a daemonless image
15
+ // build. A server allows them only in a sandboxed runtime (`backend.capabilities`).
16
+ capAdd: capabilities_schema_1.capabilitiesSchema.describe('Linux capabilities added to the container, e.g. [SYS_ADMIN]').optional(),
13
17
  })
14
18
  .strict()
15
19
  .describe('container task');
20
+ // A task that extends another takes the image of its base, so it may set the
21
+ // container properties without an image of its own; once extended, a task whose
22
+ // base has no image either is refused them (applySchemaExtension).
23
+ exports.buildFileExtendingContainerTaskSchema = exports.buildFileContainerTaskSchema
24
+ .extend({
25
+ image: (0, zod_1.string)().optional(),
26
+ extend: (0, zod_1.string)(),
27
+ })
28
+ .strict()
29
+ .describe('container task taking its image from the task it extends');
30
+ exports.CONTAINER_TASK_PROPERTIES = ['mounts', 'volumeSize', 'capAdd'];
16
31
  //# sourceMappingURL=build-file-container-task-schema.js.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isGitSource = exports.buildFileIncludeSchema = exports.buildFileGitSourceSchema = void 0;
3
+ exports.isPublished = exports.isGitSource = exports.buildFileIncludeSchema = exports.buildFileLocalSourceSchema = exports.buildFileGitSourceSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const envs_schema_1 = require("./envs-schema");
6
6
  // A repo URL or ref starting with `-` would be read by git as an option
@@ -21,11 +21,25 @@ exports.buildFileGitSourceSchema = (0, zod_1.object)({
21
21
  path: (0, zod_1.string)().optional().describe('Build file or directory within the repository'),
22
22
  // overrides for the top-level `envs` the included file declares as its inputs
23
23
  with: envs_schema_1.envsSchema.optional(),
24
+ publish: (0, zod_1.boolean)().optional().describe('false: a server leaves out what this file declares'),
24
25
  }).strict();
25
- // A string is a local path; an object is a git source (ADR-0004).
26
- exports.buildFileIncludeSchema = (0, zod_1.union)([(0, zod_1.string)(), exports.buildFileGitSourceSchema]);
26
+ // A local path, with values for the inputs of the file it names like `with` of a git
27
+ // source (a value may pass on an `envs` value of the including file as `${NAME}`), and
28
+ // whether a server publishes what it declares.
29
+ exports.buildFileLocalSourceSchema = (0, zod_1.object)({
30
+ path: (0, zod_1.string)().min(1).describe('Build file or directory, relative to the including file'),
31
+ with: envs_schema_1.envsSchema.optional(),
32
+ publish: (0, zod_1.boolean)().optional().describe('false: a server leaves out what this file declares'),
33
+ }).strict();
34
+ // A string is a local path; an object is a local path with options, or a git source (ADR-0004).
35
+ exports.buildFileIncludeSchema = (0, zod_1.union)([(0, zod_1.string)(), exports.buildFileLocalSourceSchema, exports.buildFileGitSourceSchema]);
27
36
  function isGitSource(include) {
28
- return typeof include !== 'string';
37
+ return typeof include !== 'string' && 'git' in include;
29
38
  }
30
39
  exports.isGitSource = isGitSource;
40
+ // `publish: false`: what the file declares is left out of a version a server publishes
41
+ function isPublished(include) {
42
+ return typeof include === 'string' || include.publish !== false;
43
+ }
44
+ exports.isPublished = isPublished;
31
45
  //# sourceMappingURL=build-file-include-schema.js.map
@@ -13,5 +13,7 @@ exports.buildFileKubernetesServiceSchema = (0, zod_1.object)({
13
13
  selector: build_file_kubernetes_service_selector_schema_1.buildFileKubernetesServiceSelectorSchema,
14
14
  labels: labels_schema_1.labelsSchema.optional(),
15
15
  ports: (0, zod_1.array)((0, zod_1.union)([(0, zod_1.string)(), (0, zod_1.number)()])),
16
+ // false: a server leaves it out of a published version
17
+ publish: (0, zod_1.boolean)().optional(),
16
18
  }).strict();
17
19
  //# sourceMappingURL=build-file-kubernetes-service-schema.js.map
@@ -4,7 +4,11 @@ exports.isBuildFileLocalTaskSchema = exports.isBuildFileContainerTaskSchema = ex
4
4
  const zod_1 = require("zod");
5
5
  const build_file_container_task_schema_1 = require("./build-file-container-task-schema");
6
6
  const build_file_local_task_schema_1 = require("./build-file-local-task-schema");
7
- exports.buildFileTaskSchema = (0, zod_1.union)([build_file_container_task_schema_1.buildFileContainerTaskSchema, build_file_local_task_schema_1.buildFileLocalTaskSchema]);
7
+ exports.buildFileTaskSchema = (0, zod_1.union)([
8
+ build_file_container_task_schema_1.buildFileContainerTaskSchema,
9
+ build_file_local_task_schema_1.buildFileLocalTaskSchema,
10
+ build_file_container_task_schema_1.buildFileExtendingContainerTaskSchema,
11
+ ]);
8
12
  const isBuildFileContainerTaskSchema = (val) => 'image' in val;
9
13
  exports.isBuildFileContainerTaskSchema = isBuildFileContainerTaskSchema;
10
14
  const isBuildFileLocalTaskSchema = (val) => !('image' in val);
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.capabilitiesSchema = exports.LINUX_CAPABILITIES = void 0;
4
+ const zod_1 = require("zod");
5
+ // The Linux capabilities, without the CAP_ prefix (capabilities(7))
6
+ exports.LINUX_CAPABILITIES = [
7
+ 'AUDIT_CONTROL',
8
+ 'AUDIT_READ',
9
+ 'AUDIT_WRITE',
10
+ 'BLOCK_SUSPEND',
11
+ 'BPF',
12
+ 'CHECKPOINT_RESTORE',
13
+ 'CHOWN',
14
+ 'DAC_OVERRIDE',
15
+ 'DAC_READ_SEARCH',
16
+ 'FOWNER',
17
+ 'FSETID',
18
+ 'IPC_LOCK',
19
+ 'IPC_OWNER',
20
+ 'KILL',
21
+ 'LEASE',
22
+ 'LINUX_IMMUTABLE',
23
+ 'MAC_ADMIN',
24
+ 'MAC_OVERRIDE',
25
+ 'MKNOD',
26
+ 'NET_ADMIN',
27
+ 'NET_BIND_SERVICE',
28
+ 'NET_BROADCAST',
29
+ 'NET_RAW',
30
+ 'PERFMON',
31
+ 'SETFCAP',
32
+ 'SETGID',
33
+ 'SETPCAP',
34
+ 'SETUID',
35
+ 'SYS_ADMIN',
36
+ 'SYS_BOOT',
37
+ 'SYS_CHROOT',
38
+ 'SYS_MODULE',
39
+ 'SYS_NICE',
40
+ 'SYS_PACCT',
41
+ 'SYS_PTRACE',
42
+ 'SYS_RAWIO',
43
+ 'SYS_RESOURCE',
44
+ 'SYS_TIME',
45
+ 'SYS_TTY_CONFIG',
46
+ 'SYSLOG',
47
+ 'WAKE_ALARM',
48
+ ];
49
+ // Capabilities a container gets on top of its runtime's default set, by name such as
50
+ // SYS_ADMIN. `ALL` is not one: a task names what it needs.
51
+ exports.capabilitiesSchema = (0, zod_1.array)((0, zod_1.enum)(exports.LINUX_CAPABILITIES, {
52
+ errorMap: (issue, ctx) => issue.code === 'invalid_enum_value'
53
+ ? { message: `unknown capability ${JSON.stringify(issue.received)}, expected a name such as SYS_ADMIN` }
54
+ : { message: ctx.defaultError },
55
+ })).refine((names) => new Set(names).size === names.length, 'a capability is listed twice');
56
+ //# sourceMappingURL=capabilities-schema.js.map
@@ -1,20 +1,69 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.findService = exports.findTask = exports.parseReferences = void 0;
3
+ exports.findService = exports.findTask = exports.parseReferences = exports.getPublishedFiles = void 0;
4
4
  const build_file_task_schema_1 = require("./build-file-task-schema");
5
5
  const build_file_service_schema_1 = require("./build-file-service-schema");
6
6
  const split_name_1 = require("../planner/utils/split-name");
7
7
  const read_env_file_1 = require("../parser/read-env-file");
8
8
  const label_values_1 = require("../executer/label-values");
9
9
  const merge_environment_variables_1 = require("../environment/merge-environment-variables");
10
+ const build_file_container_task_schema_1 = require("./build-file-container-task-schema");
10
11
  const resolve_cache_1 = require("../cache/resolve-cache");
11
12
  const project_root_1 = require("../planner/project-root");
12
13
  const secret_catalog_1 = require("../secrets/secret-catalog");
14
+ const host_config_1 = require("../remote/host-config");
15
+ // The providers and accounts of the host config, see HostSecretDeclarations
16
+ async function readHostSecrets(environment) {
17
+ const config = await (0, host_config_1.readHostConfig)(environment);
18
+ return {
19
+ fileName: (0, host_config_1.getHostConfigPath)(environment),
20
+ secretProviders: config.secretProviders,
21
+ secretAccounts: config.secretAccounts,
22
+ };
23
+ }
24
+ // What a task or service names that a server left out (`publish: false`)
25
+ class UnpublishedError extends Error {
26
+ constructor(name, reason) {
27
+ super(`${name} is not published (${reason})`);
28
+ this.name = name;
29
+ this.reason = reason;
30
+ }
31
+ }
32
+ // The task or service `user` names another one by, which a server left out: the error says
33
+ // which published one still needs it, as an error elsewhere is left as it is
34
+ function findPublished(user, verb, find) {
35
+ try {
36
+ return find();
37
+ }
38
+ catch (e) {
39
+ if (e instanceof UnpublishedError) {
40
+ throw new Error(`${user} ${verb} ${e.name}, which is not published (${e.reason})`);
41
+ }
42
+ throw e;
43
+ }
44
+ }
45
+ // The build files a server publishes: the entry file and what it includes or references,
46
+ // except through an include with `publish: false`. The key of each is in ctx.files.
47
+ function getPublishedFiles(mainScope) {
48
+ const published = new Set([mainScope]);
49
+ const queue = [mainScope];
50
+ for (let scope = queue.shift(); scope; scope = queue.shift()) {
51
+ for (const reference of Object.values(scope.references)) {
52
+ if (reference.publish && !published.has(reference.scope)) {
53
+ published.add(reference.scope);
54
+ queue.push(reference.scope);
55
+ }
56
+ }
57
+ }
58
+ return published;
59
+ }
60
+ exports.getPublishedFiles = getPublishedFiles;
13
61
  function combinePath(namePrefix, name) {
14
62
  return namePrefix.length > 0 ? `${namePrefix}:${name}` : name;
15
63
  }
16
64
  async function parseReferences(ctx, mainScope, environment) {
17
65
  const declaredCaches = {};
66
+ const unpublished = {};
18
67
  const reference = {
19
68
  files: {},
20
69
  tasks: {},
@@ -24,9 +73,28 @@ async function parseReferences(ctx, mainScope, environment) {
24
73
  caches: {},
25
74
  secrets: (0, secret_catalog_1.emptySecretCatalog)(),
26
75
  projectRoot: await (0, project_root_1.findProjectRoot)(mainScope.cwd, environment),
76
+ unpublished,
77
+ };
78
+ // a server leaves out what is not published: it doesn't run, and isn't checked against
79
+ // its policy. Locally everything is there.
80
+ const publishedFiles = environment.hosted ? getPublishedFiles(mainScope) : null;
81
+ const isLeftOut = (file, name, schema) => {
82
+ if (!publishedFiles) {
83
+ return false;
84
+ }
85
+ if (!publishedFiles.has(file)) {
86
+ unpublished[name] = 'its build file is included with publish: false';
87
+ }
88
+ else if (schema.publish === false) {
89
+ unpublished[name] = 'publish: false';
90
+ }
91
+ return name in unpublished;
27
92
  };
28
93
  for (const file of Object.values(ctx.files)) {
29
- if (environment.hosted && (file.schema.secretProviders || file.schema.secretAccounts)) {
94
+ // what a server leaves out of a file it doesn't publish: its tasks and services (see
95
+ // isLeftOut), and where it would run or store things, which only a local run reads
96
+ const published = !publishedFiles || publishedFiles.has(file);
97
+ if (published && environment.hosted && (file.schema.secretProviders || file.schema.secretAccounts)) {
30
98
  throw new Error(`${file.fileName} declares secret providers or accounts, which a server defines in its own config`);
31
99
  }
32
100
  if (!reference.envFiles[file.cwd]) {
@@ -38,6 +106,9 @@ async function parseReferences(ctx, mainScope, environment) {
38
106
  for (const [taskName, task] of Object.entries(file.schema.tasks)) {
39
107
  const taskEnvs = (0, merge_environment_variables_1.mergeEnvironmentVariables)(task.envs, buildFileEnvs);
40
108
  const relativeName = combinePath(file.namePrefix, taskName);
109
+ if (isLeftOut(file, relativeName, task)) {
110
+ continue;
111
+ }
41
112
  reference.tasks[relativeName] = {
42
113
  type: 'task',
43
114
  schema: task,
@@ -51,7 +122,7 @@ async function parseReferences(ctx, mainScope, environment) {
51
122
  };
52
123
  }
53
124
  }
54
- if (file.schema.environments) {
125
+ if (file.schema.environments && published) {
55
126
  for (const [envName, env] of Object.entries(file.schema.environments)) {
56
127
  reference.environments[envName] = {
57
128
  type: 'environment',
@@ -61,7 +132,7 @@ async function parseReferences(ctx, mainScope, environment) {
61
132
  };
62
133
  }
63
134
  }
64
- if (file.schema.caches) {
135
+ if (file.schema.caches && published) {
65
136
  for (const [cacheName, cache] of Object.entries(file.schema.caches)) {
66
137
  declaredCaches[cacheName] = cache;
67
138
  }
@@ -69,6 +140,9 @@ async function parseReferences(ctx, mainScope, environment) {
69
140
  if (file.schema.services) {
70
141
  for (const [serviceName, service] of Object.entries(file.schema.services)) {
71
142
  const relativeName = combinePath(file.namePrefix, serviceName);
143
+ if (isLeftOut(file, relativeName, service)) {
144
+ continue;
145
+ }
72
146
  const serviceEnvs = (0, merge_environment_variables_1.mergeEnvironmentVariables)((0, build_file_service_schema_1.isBuildFileKubernetesServiceSchema)(service) ? {} : service.envs, buildFileEnvs);
73
147
  reference.services[relativeName] = {
74
148
  type: 'service',
@@ -98,7 +172,7 @@ async function parseReferences(ctx, mainScope, environment) {
98
172
  reference.caches = (0, resolve_cache_1.withBuiltinCaches)(declaredCaches);
99
173
  reference.secrets = environment.hosted
100
174
  ? { ...environment.hosted.catalog, explicitAccounts: true }
101
- : (0, secret_catalog_1.collectSecretCatalog)(Object.values(ctx.files), environment, reference);
175
+ : (0, secret_catalog_1.collectSecretCatalog)(Object.values(ctx.files), environment, reference, await readHostSecrets(environment));
102
176
  return reference;
103
177
  }
104
178
  exports.parseReferences = parseReferences;
@@ -122,7 +196,8 @@ function applySchemaExtension(reference, task) {
122
196
  task.extension = 'applied';
123
197
  return;
124
198
  }
125
- const extend = findTask(reference, task.scope, task.schema.extend, null).task;
199
+ const name = task.schema.extend;
200
+ const extend = findPublished(task.relativeName, 'extends', () => findTask(reference, task.scope, name, null)).task;
126
201
  applySchemaExtension(reference, extend);
127
202
  task.schema.shell = task.schema.shell ?? extend.schema.shell;
128
203
  task.schema.cache = task.schema.cache ?? extend.schema.cache;
@@ -148,6 +223,7 @@ function applySchemaExtension(reference, task) {
148
223
  if (!reset.has('mounts'))
149
224
  taskSchema.mounts = extendArray(taskSchema.mounts, extend.schema.mounts);
150
225
  }
226
+ rejectContainerPropertiesWithoutImage(task);
151
227
  // deps and needs resolve in the file that declares them, so every base up the
152
228
  // chain contributes its own, not only the nearest; what a task resets is not
153
229
  // passed on to the tasks extending it either
@@ -162,6 +238,16 @@ function applySchemaExtension(reference, task) {
162
238
  }
163
239
  task.extension = 'applied';
164
240
  }
241
+ function rejectContainerPropertiesWithoutImage(task) {
242
+ if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
243
+ return;
244
+ }
245
+ const schema = task.schema;
246
+ const property = build_file_container_task_schema_1.CONTAINER_TASK_PROPERTIES.find((key) => schema[key] !== undefined);
247
+ if (property) {
248
+ throw new Error(`task ${task.relativeName} sets ${property}, but neither it nor its base has an image`);
249
+ }
250
+ }
165
251
  function extendArray(base, extend) {
166
252
  if (!extend) {
167
253
  return base;
@@ -182,7 +268,7 @@ function extendObject(base, extend) {
182
268
  }
183
269
  function resolveDeps(reference, taskOrService, scope, schema) {
184
270
  for (const depName of schema.deps || []) {
185
- const depTask = findTask(reference, scope, depName, null);
271
+ const depTask = findPublished(taskOrService.relativeName, 'depends on', () => findTask(reference, scope, depName, null));
186
272
  taskOrService.deps.push({
187
273
  task: depTask.task,
188
274
  type: depTask.type,
@@ -195,7 +281,8 @@ function resolveInit(reference, service) {
195
281
  if ((0, build_file_service_schema_1.isBuildFileKubernetesServiceSchema)(service.schema) || !service.schema.init) {
196
282
  return;
197
283
  }
198
- const initTask = findTask(reference, service.scope, service.schema.init, null);
284
+ const init = service.schema.init;
285
+ const initTask = findPublished(service.relativeName, 'runs as init', () => findTask(reference, service.scope, init, null));
199
286
  service.init = {
200
287
  task: initTask.task,
201
288
  type: initTask.type,
@@ -210,19 +297,27 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
210
297
  for (const needName of schema.needs || []) {
211
298
  if (typeof needName === 'string') {
212
299
  taskOrService.needs.push({
213
- relativeName: needName,
214
- service: findService(reference, scope, needName),
300
+ // the host name it is reached by: a service of an include, `services:postgres`, as
301
+ // `services-postgres`, since Docker and DNS take no colon
302
+ relativeName: needName.replace(/:/g, '-'),
303
+ service: findPublished(taskOrService.relativeName, 'needs', () => findService(reference, scope, needName)),
215
304
  });
216
305
  }
217
306
  else {
218
307
  taskOrService.needs.push({
219
308
  relativeName: needName.name,
220
- service: findService(reference, scope, needName.service),
309
+ service: findPublished(taskOrService.relativeName, 'needs', () => findService(reference, scope, needName.service)),
221
310
  });
222
311
  }
223
312
  }
313
+ const names = taskOrService.needs.map((need) => need.relativeName);
314
+ const twice = names.find((name, index) => names.indexOf(name) !== index);
315
+ if (twice) {
316
+ throw new Error(`${taskOrService.relativeName} needs two services as ${twice}, give one another name with name:`);
317
+ }
224
318
  }
225
319
  function findTask(ctx, scope, name, type) {
320
+ refuseNotFetched(scope, name);
226
321
  const ref = (0, split_name_1.splitName)(name);
227
322
  if (ref.prefix) {
228
323
  if (scope.references[ref.prefix]) {
@@ -238,15 +333,25 @@ function findTask(ctx, scope, name, type) {
238
333
  throw new Error('unable to find ' + name); // TODO improve
239
334
  }
240
335
  exports.findTask = findTask;
336
+ // A name in a git source a server did not fetch, since it is left out
337
+ function refuseNotFetched(scope, name) {
338
+ if (scope.notFetched) {
339
+ throw new UnpublishedError(combinePath(scope.namePrefix, name), 'its build file is included with publish: false');
340
+ }
341
+ }
241
342
  function resolveTask(ctx, scope, name, type) {
242
343
  const fullName = combinePath(scope.namePrefix, name);
243
344
  const buildFileTask = ctx.tasks[fullName];
345
+ if (!buildFileTask && ctx.unpublished?.[fullName]) {
346
+ throw new UnpublishedError(fullName, ctx.unpublished[fullName]);
347
+ }
244
348
  if (!buildFileTask) {
245
349
  throw new Error('unable to find ' + fullName);
246
350
  }
247
351
  return { task: buildFileTask, type };
248
352
  }
249
353
  function findService(ctx, scope, name) {
354
+ refuseNotFetched(scope, name);
250
355
  const ref = (0, split_name_1.splitName)(name);
251
356
  if (ref.prefix) {
252
357
  if (scope.references[ref.prefix]) {
@@ -265,6 +370,9 @@ exports.findService = findService;
265
370
  function resolveService(ctx, scope, name) {
266
371
  const fullName = combinePath(scope.namePrefix, name);
267
372
  const service = ctx.services[fullName];
373
+ if (!service && ctx.unpublished?.[fullName]) {
374
+ throw new UnpublishedError(fullName, ctx.unpublished[fullName]);
375
+ }
268
376
  if (!service) {
269
377
  throw new Error('unable to find ' + fullName);
270
378
  }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertNoRemovedProperties = void 0;
4
+ // Properties a build file may no longer have, with what takes their place: a file that
5
+ // still has one fails to parse with that, rather than with an unknown key.
6
+ function assertNoRemovedProperties(input, fileName) {
7
+ const tasks = isObject(input) && isObject(input.tasks) ? input.tasks : {};
8
+ for (const [name, task] of Object.entries(tasks)) {
9
+ if (isObject(task) && 'continuous' in task) {
10
+ throw new Error(`task ${name} in ${fileName}: continuous is for services only since hammerkit 2. A dev server that ` +
11
+ `watches its own files (ng serve, vite, nodemon) is a service with continuous: true, see ` +
12
+ `https://hammerkit.dev/docs/service/container#continuous`);
13
+ }
14
+ }
15
+ }
16
+ exports.assertNoRemovedProperties = assertNoRemovedProperties;
17
+ function isObject(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ //# sourceMappingURL=removed-properties.js.map