hammerkit 1.27.0-next.2 → 1.27.0-next.21

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 (103) hide show
  1. package/build.schema.json +27 -0
  2. package/dist/cache/backends/registry-cache-backend.js +2 -2
  3. package/dist/cache/explain.js +17 -0
  4. package/dist/cli.js +1 -0
  5. package/dist/docker/item-network.js +13 -5
  6. package/dist/executer/deadline.js +17 -3
  7. package/dist/executer/docker-service.js +30 -8
  8. package/dist/executer/event-cache.js +20 -2
  9. package/dist/executer/execute-work-task.js +5 -3
  10. package/dist/executer/execution-steps.js +25 -4
  11. package/dist/executer/reset-work-tree.js +4 -0
  12. package/dist/executer/scheduler/enqueue-next.js +7 -0
  13. package/dist/executer/scheduler/state-key.js +42 -5
  14. package/dist/harness/connect.js +20 -6
  15. package/dist/harness/mcp-command.js +22 -6
  16. package/dist/harness/mcp.js +63 -114
  17. package/dist/harness/opencode.js +158 -50
  18. package/dist/harness/pi.js +29 -0
  19. package/dist/harness/sandbox-client.js +207 -65
  20. package/dist/harness/service-tools.js +141 -0
  21. package/dist/harness/task-tools.js +221 -0
  22. package/dist/kubernetes/apply.js +27 -16
  23. package/dist/kubernetes/claim-layout.js +71 -0
  24. package/dist/kubernetes/ensure-http-route.js +1 -1
  25. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
  26. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
  27. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  28. package/dist/kubernetes/ensure-persistent-data.js +30 -20
  29. package/dist/kubernetes/get-service-ip.js +2 -2
  30. package/dist/kubernetes/job-secrets.js +33 -19
  31. package/dist/kubernetes/kept-claims.js +22 -10
  32. package/dist/kubernetes/network-policy.js +85 -27
  33. package/dist/kubernetes/remove-persistent-data.js +3 -1
  34. package/dist/kubernetes/resources.js +25 -3
  35. package/dist/kubernetes/restore-kubernetes-data.js +9 -3
  36. package/dist/kubernetes/sandbox-runner.js +626 -0
  37. package/dist/kubernetes/secrets.js +7 -11
  38. package/dist/kubernetes/volumes.js +6 -3
  39. package/dist/network/egress-proxy.js +45 -6
  40. package/dist/optimizer/calculate-checksum.js +3 -2
  41. package/dist/optimizer/check-key.js +94 -0
  42. package/dist/optimizer/check-keys.js +64 -0
  43. package/dist/optimizer/get-work-cache-stats.js +17 -3
  44. package/dist/optimizer/work-task-cache-description.js +51 -1
  45. package/dist/planner/utils/append-work-dependencies.js +10 -5
  46. package/dist/planner/utils/append-work-service.js +1 -0
  47. package/dist/planner/utils/append-work-task.js +15 -2
  48. package/dist/planner/utils/get-work-runtime.js +6 -1
  49. package/dist/planner/utils/normalize-path.js +19 -1
  50. package/dist/planner/utils/parse-work-mount.js +7 -1
  51. package/dist/planner/utils/parse-work-port.js +4 -1
  52. package/dist/planner/utils/task-arguments.js +3 -0
  53. package/dist/planner/validate.js +22 -0
  54. package/dist/planner/work-network.js +3 -1
  55. package/dist/planner/work-runtime-aggregate.js +32 -0
  56. package/dist/planner/work-runtime-docker.js +2 -1
  57. package/dist/planner/work-runtime-kubernetes.js +97 -48
  58. package/dist/planner/work-runtime-local.js +2 -1
  59. package/dist/planner/work-task.js +6 -1
  60. package/dist/program.js +35 -13
  61. package/dist/remote/annotations.js +13 -1
  62. package/dist/remote/device-login.js +7 -2
  63. package/dist/remote/host-config.js +64 -3
  64. package/dist/remote/remote-commands.js +41 -9
  65. package/dist/remote/remote-work-commands.js +246 -1
  66. package/dist/remote/server-client.js +15 -2
  67. package/dist/remote/token-source.js +64 -26
  68. package/dist/run-program.js +9 -0
  69. package/dist/schema/build-file-base-task-schema.js +5 -1
  70. package/dist/schema/build-file-container-service-schema.js +2 -0
  71. package/dist/schema/build-file-include-schema.js +10 -4
  72. package/dist/schema/reference-parser.js +9 -1
  73. package/dist/schema/schema-parser.js +36 -4
  74. package/dist/schema/work-scope-parser.js +33 -10
  75. package/dist/secrets/github-app-token-command.js +3 -1
  76. package/dist/server/accounts-api.js +3 -1
  77. package/dist/server/approvals-api.js +1 -1
  78. package/dist/server/checks-api.js +83 -0
  79. package/dist/server/checks.js +116 -0
  80. package/dist/server/definitions-api.js +4 -2
  81. package/dist/server/endpoints.js +612 -0
  82. package/dist/server/executor.js +56 -5
  83. package/dist/server/kubernetes-backend.js +303 -64
  84. package/dist/server/kubernetes-sandbox-storage.js +48 -3
  85. package/dist/server/oauth.js +12 -1
  86. package/dist/server/plan.js +126 -62
  87. package/dist/server/policy.js +32 -2
  88. package/dist/server/repo-access.js +5 -0
  89. package/dist/server/run-plan.js +50 -19
  90. package/dist/server/run-request.js +64 -3
  91. package/dist/server/run-worker.js +146 -10
  92. package/dist/server/runs.js +347 -27
  93. package/dist/server/sandbox-pod.js +28 -0
  94. package/dist/server/sandbox-storage.js +23 -0
  95. package/dist/server/sandboxes-api.js +10 -4
  96. package/dist/server/sandboxes.js +8 -0
  97. package/dist/server/server-command.js +39 -20
  98. package/dist/server/server-config.js +213 -14
  99. package/dist/server/server.js +268 -15
  100. package/dist/server/service-runs.js +82 -0
  101. package/dist/utils/abort-on-signals.js +46 -6
  102. package/dist/utils/tar.js +36 -0
  103. package/package.json +1 -1
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskToolArgs = exports.taskToolSchema = exports.createTaskTools = exports.describeTasks = exports.describeRun = exports.readLogHint = exports.readTaskListing = exports.LIST_TASKS_MS = void 0;
4
+ const sandbox_client_1 = require("./sandbox-client");
5
+ // the most tasks a description lists, and of a task's description or command it shows
6
+ const MAX_LISTED = 40;
7
+ const MAX_DESCRIPTION_CHARS = 200;
8
+ const MAX_COMMAND_CHARS = 100;
9
+ // how long a client waits for the tasks of the build file when it starts, it goes on without them
10
+ exports.LIST_TASKS_MS = 15000;
11
+ // The tasks of the published version, or why they could not be read within `ms`.
12
+ async function readTaskListing(list, ms = exports.LIST_TASKS_MS) {
13
+ let timer;
14
+ const timeout = new Promise((_, reject) => {
15
+ timer = setTimeout(() => reject(new Error(`no answer within ${ms / 1000} s`)), ms);
16
+ timer.unref?.();
17
+ });
18
+ try {
19
+ return await Promise.race([list(), timeout]);
20
+ }
21
+ catch (e) {
22
+ return { error: e instanceof Error ? e.message : String(e) };
23
+ }
24
+ finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
28
+ exports.readTaskListing = readTaskListing;
29
+ // the hint at the end of a cut output or a result, for the model
30
+ function readLogHint(run, task) {
31
+ return `read more of the log with run_logs { "run": "${run}", "task": "${task}" }`;
32
+ }
33
+ exports.readLogHint = readLogHint;
34
+ // A run of a task in a few lines: how it went, why it ran, its last lines.
35
+ function describeRun(task, ran) {
36
+ const result = ran.result;
37
+ const lines = [`run ${ran.run.id}: ${ran.run.state}`];
38
+ if (result) {
39
+ lines.push(`task ${task}: ${result.status}` +
40
+ (result.exitCode !== undefined ? `, exit code ${result.exitCode}` : '') +
41
+ (result.durationMs !== undefined ? `, ${result.durationMs} ms` : '') +
42
+ (result.cache?.hit ? ', from the cache' : ''));
43
+ if (result.error) {
44
+ lines.push(`error: ${result.error}`);
45
+ }
46
+ if (result.cache?.causes?.length) {
47
+ lines.push(`ran because: ${result.cache.causes.join('; ')}`);
48
+ }
49
+ }
50
+ else if (ran.run.error) {
51
+ lines.push(`error: ${ran.run.error}`);
52
+ }
53
+ if (result && result.lastLog.length > 0) {
54
+ lines.push('last lines:', ...result.lastLog);
55
+ }
56
+ lines.push(readLogHint(ran.run.id, task));
57
+ return lines.join('\n');
58
+ }
59
+ exports.describeRun = describeRun;
60
+ /**
61
+ * The tasks for the model, one line each: what it is for (or its command, when the build
62
+ * file says nothing) and whether it takes arguments. `shell` is left out, the shell tool runs it.
63
+ */
64
+ function describeTasks(listing, shell) {
65
+ if (listing === undefined) {
66
+ return '';
67
+ }
68
+ if (!Array.isArray(listing)) {
69
+ return `The tasks of the build file could not be listed (${listing.error}); name one as the build file does.`;
70
+ }
71
+ const tasks = listing.filter((task) => !shell || task.name !== shell);
72
+ if (tasks.length === 0) {
73
+ return 'The published build file has no tasks to run.';
74
+ }
75
+ const lines = tasks.slice(0, MAX_LISTED).map((task) => {
76
+ const parts = [`- ${task.name}`];
77
+ if (task.description) {
78
+ parts.push(`: ${cut(oneLine(task.description), MAX_DESCRIPTION_CHARS)}`);
79
+ }
80
+ else if (task.command) {
81
+ parts.push(`: runs \`${cut(oneLine(task.command), MAX_COMMAND_CHARS)}\``);
82
+ }
83
+ if (task.args === null) {
84
+ parts.push(' (takes args)');
85
+ }
86
+ else if (task.args !== undefined || task.command === null) {
87
+ parts.push(' (takes no args)');
88
+ }
89
+ return parts.join('');
90
+ });
91
+ if (tasks.length > MAX_LISTED) {
92
+ lines.push(`- and ${tasks.length - MAX_LISTED} more`);
93
+ }
94
+ return ['The tasks of the published build file:', ...lines].join('\n');
95
+ }
96
+ exports.describeTasks = describeTasks;
97
+ function oneLine(text) {
98
+ return text.replace(/\s+/g, ' ').trim();
99
+ }
100
+ function cut(text, max) {
101
+ return text.length > max ? `${text.slice(0, max)}…` : text;
102
+ }
103
+ /**
104
+ * `run_task`, `run_logs` and `check`, with the tasks the description of `run_task` lists. Without a
105
+ * listing it names none, as the MCP tools before a version could be read.
106
+ */
107
+ function createTaskTools(listing, shell) {
108
+ const tasks = describeTasks(listing, shell);
109
+ return [
110
+ {
111
+ name: 'run_task',
112
+ title: 'Run a task',
113
+ description: 'Run a task of the published build file on the sandbox, such as test or build, and answer how it went: ' +
114
+ 'its status, exit code, why it was not taken from the cache, and its last lines. args are appended to ' +
115
+ 'its last command, for example a test file. It runs in the container, with the network and the ' +
116
+ 'options the build file gives the task.' +
117
+ (tasks ? `\n\n${tasks}` : ''),
118
+ parameters: {
119
+ task: { type: 'string', description: 'The name of the task' },
120
+ args: { type: 'string[]', description: 'Arguments for its last command' },
121
+ },
122
+ required: ['task'],
123
+ async call(client, args, signal) {
124
+ const ran = await client.runTask(args.task, { args: args.args?.length ? args.args : undefined, signal });
125
+ return describeRun(args.task, ran);
126
+ },
127
+ },
128
+ {
129
+ name: 'run_logs',
130
+ title: 'Read the log of a run',
131
+ description: 'Read the log of a run as numbered lines: one task, the last lines (tail), a range (from, limit) or ' +
132
+ 'the lines that contain a text (grep, a fixed string) with lines of context around them.',
133
+ parameters: {
134
+ run: { type: 'string', description: 'The id of the run' },
135
+ task: { type: 'string', description: 'Only the lines of this task' },
136
+ tail: {
137
+ type: 'integer',
138
+ description: 'The last lines, or with grep the last matches',
139
+ minimum: 1,
140
+ maximum: 1000,
141
+ },
142
+ from: { type: 'integer', description: 'The first line to read', minimum: 1 },
143
+ limit: { type: 'integer', description: 'How many lines to read', minimum: 1, maximum: 1000 },
144
+ grep: { type: 'string', description: 'Only lines that contain this text' },
145
+ ignore_case: { type: 'boolean', description: 'Ignore case in grep' },
146
+ context: { type: 'integer', description: 'Lines before and after each match', minimum: 0, maximum: 20 },
147
+ },
148
+ required: ['run'],
149
+ async call(client, args) {
150
+ const page = await client.logs(args.run, {
151
+ task: args.task || undefined,
152
+ tail: args.tail || undefined,
153
+ from: args.from || undefined,
154
+ limit: args.limit || undefined,
155
+ grep: args.grep || undefined,
156
+ i: args.ignore_case || undefined,
157
+ context: args.context || undefined,
158
+ });
159
+ const lines = page.lines.map((line) => `${line.line}${line.match === false ? '-' : ':'} ${args.task ? '' : `[${line.task}] `}${line.text}`);
160
+ lines.push('', `(${page.total} lines${args.grep ? ` searched, ${page.lines.filter((line) => line.match !== false).length} match here` : ''}${page.next ? `, read on with from ${page.next}` : ''}${page.truncated ? ', the server cut the log off at its limit' : ''})`);
161
+ return lines.join('\n');
162
+ },
163
+ },
164
+ {
165
+ name: 'check',
166
+ title: 'Run a check',
167
+ description: sandbox_client_1.CHECK_DESCRIPTION,
168
+ parameters: { task: { type: 'string', description: 'The name of the task' } },
169
+ required: ['task'],
170
+ call(client, args, signal) {
171
+ return (0, sandbox_client_1.runCheck)(client, args.task, signal);
172
+ },
173
+ },
174
+ ];
175
+ }
176
+ exports.createTaskTools = createTaskTools;
177
+ // The parameters as a JSON schema, for MCP and opencode 2.
178
+ function taskToolSchema(tool) {
179
+ return {
180
+ type: 'object',
181
+ properties: Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
182
+ if (parameter.type === 'string[]') {
183
+ return [name, { type: 'array', items: { type: 'string' }, description: parameter.description }];
184
+ }
185
+ if (parameter.type === 'integer') {
186
+ const { maximum } = parameter;
187
+ return [
188
+ name,
189
+ {
190
+ type: 'integer',
191
+ description: parameter.description,
192
+ minimum: parameter.minimum,
193
+ ...(maximum !== undefined ? { maximum } : {}),
194
+ },
195
+ ];
196
+ }
197
+ return [name, { type: parameter.type, description: parameter.description }];
198
+ })),
199
+ required: tool.required,
200
+ };
201
+ }
202
+ exports.taskToolSchema = taskToolSchema;
203
+ // The parameters with the zod of opencode 1.
204
+ function taskToolArgs(tool, z) {
205
+ return Object.fromEntries(Object.entries(tool.parameters).map(([name, parameter]) => {
206
+ let type;
207
+ if (parameter.type === 'string[]') {
208
+ type = z.array(z.string());
209
+ }
210
+ else if (parameter.type === 'integer') {
211
+ type = z.number().int().min(parameter.minimum);
212
+ type = parameter.maximum !== undefined ? type.max(parameter.maximum) : type;
213
+ }
214
+ else {
215
+ type = parameter.type === 'boolean' ? z.boolean() : z.string();
216
+ }
217
+ return [name, (tool.required.includes(name) ? type : type.optional()).describe(parameter.description)];
218
+ }));
219
+ }
220
+ exports.taskToolArgs = taskToolArgs;
221
+ //# sourceMappingURL=task-tools.js.map
@@ -36,28 +36,39 @@ exports.apply = apply;
36
36
  * of a policy).
37
37
  */
38
38
  async function replace(instance, spec) {
39
- let current;
40
- try {
41
- current = await instance.objectApi.read(spec);
42
- }
43
- catch (e) {
44
- if (statusCodeOf(e) !== 404) {
45
- throw e;
39
+ for (let attempt = 1;; attempt++) {
40
+ let current;
41
+ try {
42
+ current = await instance.objectApi.read(spec);
43
+ }
44
+ catch (e) {
45
+ if (statusCodeOf(e) !== 404) {
46
+ throw e;
47
+ }
48
+ try {
49
+ return await instance.objectApi.create(spec);
50
+ }
51
+ catch (createError) {
52
+ if (statusCodeOf(createError) !== 409) {
53
+ throw createError;
54
+ }
55
+ current = await instance.objectApi.read(spec);
56
+ }
46
57
  }
47
58
  try {
48
- return await instance.objectApi.create(spec);
59
+ return await instance.objectApi.replace({
60
+ ...spec,
61
+ metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
62
+ });
49
63
  }
50
- catch (createError) {
51
- if (statusCodeOf(createError) !== 409) {
52
- throw createError;
64
+ catch (e) {
65
+ // replaced by someone else since it was read (the server sets up a caller's
66
+ // namespace for a run and for a sandbox at the same time): read it again
67
+ if (statusCodeOf(e) !== 409 || attempt >= 3) {
68
+ throw e;
53
69
  }
54
- current = await instance.objectApi.read(spec);
55
70
  }
56
71
  }
57
- return await instance.objectApi.replace({
58
- ...spec,
59
- metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
60
- });
61
72
  }
62
73
  exports.replace = replace;
63
74
  //# sourceMappingURL=apply.js.map
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasOutputsFlatteningAlike = exports.hasPathsEndingAlike = exports.byDepth = exports.getClaimSubPath = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const path_1 = require("path");
6
+ const portable_path_1 = require("../planner/utils/portable-path");
7
+ // The paths a work item keeps in its claim. Its consumers mount its outputs by the same
8
+ // paths, so they find the same folders.
9
+ function getClaimPaths(work) {
10
+ const data = work.data;
11
+ const paths = [];
12
+ if ('mounts' in data) {
13
+ paths.push(...data.mounts.map((mount) => mount.containerPath));
14
+ }
15
+ if ('src' in data) {
16
+ paths.push(...data.src.filter((src) => !src.inherited).map((src) => src.absolutePath));
17
+ }
18
+ if ('generates' in data) {
19
+ paths.push(...data.generates.filter((generate) => !generate.inherited).map((generate) => generate.path));
20
+ }
21
+ if ('volumes' in data) {
22
+ paths.push(...data.volumes.filter((volume) => !volume.inherited).map((volume) => volume.containerPath));
23
+ }
24
+ return paths;
25
+ }
26
+ // Where `path` is in the claim of `work`, relative to its root. A path is named after its
27
+ // last segment, so claims without two such paths are laid out as they always were (a
28
+ // service's data among them). Of the paths with the same last segment, the least deep one
29
+ // (then the first in sorted order) keeps the name, and the others also carry a hash of
30
+ // their path. Both go by the path
31
+ // relative to the project, as the task's id does, so a run in another checkout directory
32
+ // (each run of a server has its own) or with its `generates` in another order finds the
33
+ // same folders in a kept claim.
34
+ function getClaimSubPath(work, path) {
35
+ const name = (0, path_1.basename)(path) || 'root';
36
+ const alike = getClaimPaths(work).filter((other) => other !== path && (0, path_1.basename)(other) === (0, path_1.basename)(path));
37
+ if (alike.length === 0) {
38
+ return name;
39
+ }
40
+ const own = (0, portable_path_1.portablePath)(work.data.projectRoot, path);
41
+ const first = [own, ...alike.map((other) => (0, portable_path_1.portablePath)(work.data.projectRoot, other))].sort(byDepth)[0];
42
+ if (first === own) {
43
+ return name;
44
+ }
45
+ return `${name}-${(0, crypto_1.createHash)('sha256').update(own).digest('hex').slice(0, 12)}`;
46
+ }
47
+ exports.getClaimSubPath = getClaimSubPath;
48
+ function byDepth(a, b) {
49
+ return a.split('/').length - b.split('/').length || (a < b ? -1 : a > b ? 1 : 0);
50
+ }
51
+ exports.byDepth = byDepth;
52
+ // Whether two different paths of the claim of `work` end alike. Before getClaimSubPath
53
+ // named them apart, they shared one folder, and outputs made then are mixed.
54
+ function hasPathsEndingAlike(work) {
55
+ const paths = new Set(getClaimPaths(work));
56
+ return new Set([...paths].map((path) => (0, path_1.basename)(path))).size < paths.size;
57
+ }
58
+ exports.hasPathsEndingAlike = hasPathsEndingAlike;
59
+ // Whether two outputs of `work` flatten to the same name of a cache archive (`a-b` and
60
+ // `a/b`, see getArchivePaths). Before they were named apart, one archive held both.
61
+ function hasOutputsFlatteningAlike(work) {
62
+ const data = work.data;
63
+ if (!('generates' in data) || !('cwd' in data)) {
64
+ return false;
65
+ }
66
+ const paths = new Set(data.generates.filter((generate) => !generate.inherited).map((generate) => generate.path));
67
+ const names = new Set([...paths].map((path) => (0, path_1.relative)(data.cwd, path).split(path_1.sep).join('-')));
68
+ return names.size < paths.size;
69
+ }
70
+ exports.hasOutputsFlatteningAlike = hasOutputsFlatteningAlike;
71
+ //# sourceMappingURL=claim-layout.js.map
@@ -36,7 +36,7 @@ async function ensureHttpRoute(instance, env, route, service) {
36
36
  ],
37
37
  backendRefs: [
38
38
  {
39
- name: (0, resources_1.getResourceName)(service),
39
+ name: (0, resources_1.getResourceName)(service, env),
40
40
  port: route.servicePort ?? service.data.ports[0].containerPort,
41
41
  },
42
42
  ],
@@ -21,7 +21,7 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
21
21
  // the needed services by name, and the proxy of a limited network
22
22
  network) {
23
23
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
24
- const name = (0, resources_1.getResourceName)(service);
24
+ const name = (0, resources_1.getResourceName)(service, env);
25
25
  const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
26
26
  const serviceCommand = (0, work_service_1.getServiceCommand)(service.data, { kubernetes: true });
27
27
  const probe = healthcheckCommand
@@ -47,25 +47,21 @@ network) {
47
47
  'hammerkit.dev/version': (0, version_1.getVersion)(),
48
48
  },
49
49
  labels: {
50
- 'hammerkit.dev/id': service.id(),
50
+ ...(0, resources_1.getItemLabels)(service, env),
51
51
  'hammerkit.dev/state': stateKey,
52
- ...(0, job_placement_1.getRunLabels)(env),
53
52
  [exports.DEFINITION_LABEL]: (0, service_definition_1.getServiceDefinitionHash)(service),
54
53
  },
55
54
  },
56
55
  spec: {
57
56
  selector: {
58
- matchLabels: {
59
- 'hammerkit.dev/id': service.id(),
60
- },
57
+ matchLabels: (0, resources_1.getItemLabels)(service, env),
61
58
  },
62
59
  replicas: 1,
63
60
  template: {
64
61
  metadata: {
65
62
  labels: {
66
- 'hammerkit.dev/id': service.id(),
63
+ ...(0, resources_1.getItemLabels)(service, env),
67
64
  'hammerkit.dev/state': stateKey,
68
- ...(0, job_placement_1.getRunLabels)(env),
69
65
  ...workspace.labels,
70
66
  ...(0, network_policy_1.getNetworkLabels)(env, service),
71
67
  },
@@ -127,7 +123,7 @@ network) {
127
123
  return;
128
124
  }
129
125
  await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
130
- podSelector: `hammerkit.dev/id=${service.id()}`,
126
+ podSelector: (0, resources_1.getItemSelector)(service, env),
131
127
  timeout: service.data.healthcheck?.timeout ?? null,
132
128
  timeoutMessage: service.data.healthcheck
133
129
  ? (0, work_healthcheck_1.getHealthcheckTimeoutMessage)(service.name, service.data.healthcheck)
@@ -65,7 +65,7 @@ async function ensureKubernetesPersistentVolumeClaimExists(instance, env, volume
65
65
  },
66
66
  },
67
67
  };
68
- service.status.console('stdout', `ensure volume ${volume.name}`);
68
+ service.status.write('debug', `ensure volume ${volume.name}`);
69
69
  // Don't block on the PVC reaching "Bound". Storage classes with
70
70
  // volumeBindingMode WaitForFirstConsumer (docker-desktop's "standard",
71
71
  // k3d/k3s local-path, most cloud defaults) keep the claim Pending until a pod
@@ -4,11 +4,10 @@ exports.ensureKubernetesServiceExists = void 0;
4
4
  const apply_1 = require("./apply");
5
5
  const resources_1 = require("./resources");
6
6
  const version_1 = require("../version");
7
- const job_placement_1 = require("./job-placement");
8
7
  // A service without ports gets a headless Service: a ClusterIP one needs a port,
9
8
  // and its tasks reach every port of the pod, as on docker.
10
9
  async function ensureKubernetesServiceExists(instance, env, service) {
11
- const name = (0, resources_1.getResourceName)(service);
10
+ const name = (0, resources_1.getResourceName)(service, env);
12
11
  const headless = service.data.ports.length === 0;
13
12
  const svc = {
14
13
  kind: 'Service',
@@ -19,21 +18,19 @@ async function ensureKubernetesServiceExists(instance, env, service) {
19
18
  annotations: {
20
19
  'hammerkit.dev/version': (0, version_1.getVersion)(),
21
20
  },
22
- labels: {
23
- 'hammerkit.dev/id': service.id(),
24
- ...(0, job_placement_1.getRunLabels)(env),
25
- },
21
+ labels: (0, resources_1.getItemLabels)(service, env),
26
22
  },
27
23
  spec: {
28
24
  type: 'ClusterIP',
29
25
  ...(headless ? { clusterIP: 'None' } : {}),
30
- selector: {
31
- 'hammerkit.dev/id': service.id(),
32
- },
26
+ // the pods of the service of this run, on a server where runs share the namespace
27
+ selector: (0, resources_1.getItemLabels)(service, env),
33
28
  ...(headless
34
29
  ? {}
35
30
  : {
31
+ // a Service of more than one port needs a name for each
36
32
  ports: service.data.ports.map((port) => ({
33
+ name: `tcp-${port.hostPort ?? port.containerPort}`,
37
34
  port: port.hostPort ?? port.containerPort,
38
35
  targetPort: port.containerPort,
39
36
  })),
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensurePersistentData = exports.clearClaim = exports.getPodForPersistence = void 0;
3
+ exports.ensurePersistentData = exports.packSource = exports.clearClaim = exports.getPodForPersistence = void 0;
4
4
  const apply_1 = require("./apply");
5
5
  const path_1 = require("path");
6
- const tar_1 = require("tar");
6
+ const tar_1 = require("../utils/tar");
7
7
  const resources_1 = require("./resources");
8
8
  const await_running_state_1 = require("./await-running-state");
9
9
  const sleep_1 = require("../utils/sleep");
@@ -67,17 +67,14 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
67
67
  for (const volume of persistence.volumes) {
68
68
  await (0, ensure_kubernetes_persistent_volume_claim_exists_1.ensureKubernetesPersistentVolumeClaimExists)(instance, env, volume, service, persistence.sizes[volume.name], persistence.kept.includes(volume.name));
69
69
  }
70
- const name = (0, resources_1.getResourceName)(service, type === 'write' ? '-upload' : '-download');
70
+ const name = (0, resources_1.getResourceName)(service, env, type === 'write' ? '-upload' : '-download');
71
71
  const podSpec = {
72
72
  kind: 'Pod',
73
73
  apiVersion: 'v1',
74
74
  metadata: {
75
75
  namespace: env.namespace,
76
76
  name,
77
- labels: {
78
- 'hammerkit.dev/id': service.id(),
79
- ...(0, job_placement_1.getRunLabels)(env),
80
- },
77
+ labels: (0, resources_1.getItemLabels)(service, env),
81
78
  annotations: {
82
79
  'hammerkit.dev/version': (0, version_1.getVersion)(),
83
80
  },
@@ -104,18 +101,18 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
104
101
  volumes: persistence.volumes,
105
102
  },
106
103
  };
107
- service.status.console('stdout', `create pod ${name}`);
104
+ service.status.write('debug', `create pod ${name}`);
108
105
  await removeStalePod(instance, env.namespace, name);
109
106
  const pod = await (0, apply_1.apply)(instance, podSpec);
110
107
  if (pod.status?.phase !== 'Running') {
111
- service.status.console('stdout', `await running state ${name}`);
108
+ service.status.write('debug', `await running state ${name}`);
112
109
  await (0, await_running_state_1.awaitRunningState)(instance, env, name, 'Running');
113
110
  }
114
111
  try {
115
112
  await fn(name);
116
113
  }
117
114
  finally {
118
- service.status.console('stdout', `delete pod ${name}`);
115
+ service.status.write('debug', `delete pod ${name}`);
119
116
  await instance.coreApi.deleteNamespacedPod({ name, namespace: env.namespace });
120
117
  }
121
118
  }
@@ -124,7 +121,7 @@ exports.getPodForPersistence = getPodForPersistence;
124
121
  // the task runs on it or outputs are restored into it, so it holds what a new one would
125
122
  async function clearClaim(instance, env, work, pod) {
126
123
  const targetPath = `/dev/hammerkit/${work.id()}`;
127
- work.status.console('stdout', `clear what an earlier run left in ${targetPath}`);
124
+ work.status.write('info', `clear what an earlier run left in ${targetPath}`);
128
125
  let failure = null;
129
126
  const res = await instance.exec.exec(env.namespace, pod, 'volume', ['find', targetPath, '-mindepth', '1', '-maxdepth', '1', '-exec', 'rm', '-rf', '{}', '+'], process.stdout, process.stderr, null, false, (status) => {
130
127
  if (status.status !== 'Success') {
@@ -137,6 +134,25 @@ async function clearClaim(instance, env, work, pod) {
137
134
  });
138
135
  }
139
136
  exports.clearClaim = clearClaim;
137
+ // The archive that uploads a source into the claim: its file or directory, at its place in
138
+ // the claim (see getClaimSubPath) instead of under its own name
139
+ function packSource(source) {
140
+ const fileItem = (0, path_1.basename)(source.localPath);
141
+ return stream_1.Readable.from((0, tar_1.packArchive)({
142
+ cwd: (0, path_1.dirname)(source.localPath),
143
+ filter(path) {
144
+ return source.matcher(path, source.localPath);
145
+ },
146
+ onWriteEntry(entry) {
147
+ entry.path = source.subPath + entry.path.slice(fileItem.length);
148
+ // the second of two hard-linked files links to the first by its path in the archive
149
+ if (entry.type === 'Link' && entry.linkpath?.startsWith(fileItem)) {
150
+ entry.linkpath = source.subPath + entry.linkpath.slice(fileItem.length);
151
+ }
152
+ },
153
+ }, [fileItem]));
154
+ }
155
+ exports.packSource = packSource;
140
156
  async function ensurePersistentData(instance, env, environment, service, persistence,
141
157
  // the kept claim of the work holds what an earlier run left, which goes first
142
158
  options = {}) {
@@ -153,7 +169,7 @@ options = {}) {
153
169
  if (persistence.sources.length === 0 && !options.reset) {
154
170
  return;
155
171
  }
156
- service.status.console('stdout', 'start upload container');
172
+ service.status.write('debug', 'start upload container');
157
173
  await getPodForPersistence(instance, env, service, persistence, 'write', async (name) => {
158
174
  if (options.reset) {
159
175
  await clearClaim(instance, env, service, name);
@@ -163,14 +179,8 @@ options = {}) {
163
179
  if (!(await environment.file.exists(source.localPath))) {
164
180
  continue;
165
181
  }
166
- const fileItem = (0, path_1.basename)(source.localPath);
167
- const data = stream_1.Readable.from((0, tar_1.create)({
168
- cwd: (0, path_1.dirname)(source.localPath),
169
- filter(path) {
170
- return source.matcher(path, source.localPath);
171
- },
172
- }, [fileItem]));
173
- service.status.console('stdout', `upload ${source.localPath}`);
182
+ const data = packSource(source);
183
+ service.status.write('info', `upload ${source.localPath}`);
174
184
  const targetPath = `/dev/hammerkit/${service.id()}`;
175
185
  const res = await instance.exec.exec(env.namespace, name, 'volume', ['tar', 'xf', '-', '-C', targetPath], process.stdout, process.stderr, data, false, (status) => {
176
186
  service.status.write('debug', `upload ${source.localPath} to ${name}: ${status}`);
@@ -5,7 +5,7 @@ const resources_1 = require("./resources");
5
5
  // The address a task reaches the service on: its clusterIP, or for a headless
6
6
  // Service (a service without ports) the address of its ready pod.
7
7
  async function getServiceIp(instance, env, workService) {
8
- const name = (0, resources_1.getResourceName)(workService);
8
+ const name = (0, resources_1.getResourceName)(workService, env);
9
9
  const svc = await instance.coreApi.readNamespacedService({ name, namespace: env.namespace });
10
10
  const clusterIP = svc.spec?.clusterIP ?? null;
11
11
  if (clusterIP !== 'None') {
@@ -13,7 +13,7 @@ async function getServiceIp(instance, env, workService) {
13
13
  }
14
14
  const pods = await instance.coreApi.listNamespacedPod({
15
15
  namespace: env.namespace,
16
- labelSelector: `hammerkit.dev/id=${workService.id()}`,
16
+ labelSelector: (0, resources_1.getItemSelector)(workService, env),
17
17
  });
18
18
  const ready = pods.items.find((pod) => !pod.metadata?.deletionTimestamp &&
19
19
  pod.status?.podIP &&