hammerkit 1.27.0-next.2 → 1.27.0-next.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.schema.json +9 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +1 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-task.js +5 -3
- package/dist/executer/execution-steps.js +5 -2
- package/dist/executer/scheduler/enqueue-next.js +7 -0
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +207 -65
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +626 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +1 -0
- package/dist/planner/utils/append-work-task.js +15 -2
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +97 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +14 -4
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +64 -3
- package/dist/remote/remote-commands.js +41 -9
- package/dist/remote/remote-work-commands.js +246 -1
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +5 -1
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/reference-parser.js +9 -1
- package/dist/schema/work-scope-parser.js +6 -0
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +56 -5
- package/dist/server/kubernetes-backend.js +303 -64
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +126 -62
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +64 -3
- package/dist/server/run-worker.js +146 -10
- package/dist/server/runs.js +347 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +213 -14
- package/dist/server/server.js +268 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- 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
|
package/dist/kubernetes/apply.js
CHANGED
|
@@ -36,28 +36,39 @@ exports.apply = apply;
|
|
|
36
36
|
* of a policy).
|
|
37
37
|
*/
|
|
38
38
|
async function replace(instance, spec) {
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
59
|
+
return await instance.objectApi.replace({
|
|
60
|
+
...spec,
|
|
61
|
+
metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
|
|
62
|
+
});
|
|
49
63
|
}
|
|
50
|
-
catch (
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
31
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
167
|
-
|
|
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:
|
|
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 &&
|