hammerkit 1.27.0-next.1 → 1.27.0-next.11
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/executer/event-cache.js +20 -2
- package/dist/harness/sandbox-client.js +104 -66
- 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 +4 -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 +83 -26
- 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/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/log.js +1 -1
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/work-task-cache-description.js +2 -0
- package/dist/planner/utils/append-work-dependencies.js +5 -2
- package/dist/planner/utils/append-work-service.js +1 -0
- package/dist/planner/utils/append-work-task.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +74 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/program.js +12 -5
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/remote-commands.js +11 -2
- package/dist/remote/remote-work-commands.js +89 -1
- 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 +1 -0
- package/dist/secrets/github-app-token-command.js +4 -1
- package/dist/server/executor.js +1 -1
- package/dist/server/kubernetes-backend.js +80 -31
- package/dist/server/kubernetes-sandbox-storage.js +2 -1
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +46 -10
- package/dist/server/run-plan.js +12 -15
- package/dist/server/run-request.js +6 -0
- package/dist/server/runs.js +10 -3
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +35 -4
- package/dist/server/server.js +16 -2
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/package.json +1 -1
package/build.schema.json
CHANGED
|
@@ -289,6 +289,9 @@
|
|
|
289
289
|
],
|
|
290
290
|
"description": "network the task or service reaches besides its needs: open (default), none or { allow: [hosts or host:port] }"
|
|
291
291
|
},
|
|
292
|
+
"args": {
|
|
293
|
+
"type": "boolean"
|
|
294
|
+
},
|
|
292
295
|
"runner": {
|
|
293
296
|
"type": "string",
|
|
294
297
|
"minLength": 1
|
|
@@ -370,6 +373,9 @@
|
|
|
370
373
|
"network": {
|
|
371
374
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/network"
|
|
372
375
|
},
|
|
376
|
+
"args": {
|
|
377
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/args"
|
|
378
|
+
},
|
|
373
379
|
"runner": {
|
|
374
380
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/runner"
|
|
375
381
|
},
|
|
@@ -511,6 +517,9 @@
|
|
|
511
517
|
"network": {
|
|
512
518
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/network",
|
|
513
519
|
"description": "network the task or service reaches besides its needs: open (default), none or { allow: [hosts or host:port] }"
|
|
520
|
+
},
|
|
521
|
+
"args": {
|
|
522
|
+
"type": "boolean"
|
|
514
523
|
}
|
|
515
524
|
},
|
|
516
525
|
"required": [
|
|
@@ -5,6 +5,7 @@ const crypto_1 = require("crypto");
|
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const tar_1 = require("tar");
|
|
8
|
+
const tar_2 = require("../../utils/tar");
|
|
8
9
|
const registry_reference_1 = require("./registry/registry-reference");
|
|
9
10
|
const registry_client_1 = require("./registry/registry-client");
|
|
10
11
|
const MAX_TAG_LENGTH = 128;
|
|
@@ -74,8 +75,7 @@ function createRegistryCacheBackend(spec) {
|
|
|
74
75
|
const layerFile = (0, path_1.join)(dir, 'layer.tar');
|
|
75
76
|
const files = (await environment.file.listFiles(from)).sort();
|
|
76
77
|
// portable + mtime-free so identical entries produce identical digests
|
|
77
|
-
|
|
78
|
-
await (0, tar_1.create)({ file: layerFile, cwd: from, portable: true, noMtime: true }, files);
|
|
78
|
+
await (0, tar_2.packArchive)({ file: layerFile, cwd: from, portable: true, noMtime: true }, files);
|
|
79
79
|
const layer = await (0, registry_client_1.digestFile)(layerFile);
|
|
80
80
|
const config = Buffer.from(JSON.stringify({
|
|
81
81
|
architecture: 'amd64',
|
|
@@ -8,6 +8,8 @@ const execute_docker_1 = require("./execute-docker");
|
|
|
8
8
|
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
9
9
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
10
10
|
const path_1 = require("path");
|
|
11
|
+
const crypto_1 = require("crypto");
|
|
12
|
+
const claim_layout_1 = require("../kubernetes/claim-layout");
|
|
11
13
|
const move_files_1 = require("../file/move-files");
|
|
12
14
|
const work_instance_id_1 = require("../planner/work-instance-id");
|
|
13
15
|
async function restoreCache(environment, path, workTree) {
|
|
@@ -43,8 +45,24 @@ function* getArchivePaths(task, path) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
// `a-b` and `a/b` flatten alike: of those, the least deep (then the first in sorted
|
|
49
|
+
// order) keeps the name, the others' names carry a hash of their relative path, so a
|
|
50
|
+
// store and a restore in another directory or order find the same archive
|
|
51
|
+
const generates = task.generates
|
|
52
|
+
.filter((g) => !g.inherited)
|
|
53
|
+
.map((generate) => ({ generate, rel: (0, path_1.relative)(task.cwd, generate.path).split(path_1.sep).join(path_1.posix.sep) }));
|
|
54
|
+
const holders = new Map();
|
|
55
|
+
for (const { rel } of [...generates].sort((a, b) => (0, claim_layout_1.byDepth)(a.rel, b.rel))) {
|
|
56
|
+
const id = rel.split(path_1.posix.sep).join('-');
|
|
57
|
+
if (!holders.has(id)) {
|
|
58
|
+
holders.set(id, rel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const { generate, rel } of generates) {
|
|
62
|
+
let generateId = rel.split(path_1.posix.sep).join('-');
|
|
63
|
+
if (holders.get(generateId) !== rel) {
|
|
64
|
+
generateId += '-' + (0, crypto_1.createHash)('sha256').update(rel).digest('hex').slice(0, 12);
|
|
65
|
+
}
|
|
48
66
|
yield {
|
|
49
67
|
filename: (0, path_1.join)(path, generateId + '-generates.tgz'),
|
|
50
68
|
path: generate.path,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.quotePosix = exports.fetchFailure = exports.toSandboxPath = exports.checkSandboxPath = exports.exitCodeOf = exports.oneAtATime = exports.SandboxClient = exports.SandboxClientError = void 0;
|
|
3
|
+
exports.quotePosix = exports.fetchFailure = exports.toSandboxPath = exports.checkSandboxPath = exports.exitCodeOf = exports.followRun = exports.submitRun = exports.FINAL_RUN_STATES = exports.oneAtATime = exports.SandboxClient = exports.SandboxClientError = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const token_source_1 = require("../remote/token-source");
|
|
6
6
|
// a write carries up to 32 MiB
|
|
7
7
|
const TIMEOUT_MS = 60000;
|
|
8
8
|
// a run is asked for this long at a time, the log is read in between
|
|
9
9
|
const POLL_WAIT = '1s';
|
|
10
|
+
// a cancelled run waits this long for the server to take the cancel, before it gives up
|
|
11
|
+
const CANCEL_WAIT_MS = 10000;
|
|
10
12
|
class SandboxClientError extends Error {
|
|
11
13
|
constructor(message, status) {
|
|
12
14
|
super(message);
|
|
@@ -115,72 +117,26 @@ class SandboxClient {
|
|
|
115
117
|
if (options.timeoutSeconds) {
|
|
116
118
|
runOptions.timeout = `${Math.max(1, Math.ceil(options.timeoutSeconds))}s`;
|
|
117
119
|
}
|
|
118
|
-
const submitted = await
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
truncated = page.truncated;
|
|
138
|
-
for (const line of page.lines) {
|
|
139
|
-
lines.push(line.text);
|
|
140
|
-
options.onLine?.(line.text);
|
|
141
|
-
from = line.line + 1;
|
|
142
|
-
}
|
|
143
|
-
if (!page.next) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
120
|
+
const submitted = await submitRun(this.connection, {
|
|
121
|
+
definition: this.build.hash,
|
|
122
|
+
tasks: [task],
|
|
123
|
+
source: { type: 'sandbox', id: this.id },
|
|
124
|
+
options: runOptions,
|
|
125
|
+
});
|
|
126
|
+
const followed = await followRun(this.connection, submitted, {
|
|
127
|
+
task,
|
|
128
|
+
signal: options.signal,
|
|
129
|
+
onLine: options.onLine ? (line) => options.onLine?.(line.text) : undefined,
|
|
130
|
+
onWaiting: options.onWaiting,
|
|
131
|
+
});
|
|
132
|
+
const result = followed.run.results?.[task];
|
|
133
|
+
return {
|
|
134
|
+
run: followed.run,
|
|
135
|
+
result,
|
|
136
|
+
output: followed.lines.map((line) => line.text).join('\n'),
|
|
137
|
+
truncated: followed.truncated,
|
|
138
|
+
exitCode: exitCodeOf(result),
|
|
147
139
|
};
|
|
148
|
-
try {
|
|
149
|
-
let run = submitted;
|
|
150
|
-
let waitingSaid = false;
|
|
151
|
-
for (;;) {
|
|
152
|
-
if (options.signal?.aborted) {
|
|
153
|
-
throw new SandboxClientError('the run was cancelled');
|
|
154
|
-
}
|
|
155
|
-
if (run.state === 'awaiting-approval' && !waitingSaid) {
|
|
156
|
-
waitingSaid = true;
|
|
157
|
-
options.onWaiting?.(run);
|
|
158
|
-
}
|
|
159
|
-
await readLog();
|
|
160
|
-
if (['succeeded', 'failed', 'cancelled', 'rejected'].includes(run.state)) {
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
run = await json(await call(this.connection, 'GET', path, { query: { wait: POLL_WAIT } }));
|
|
164
|
-
}
|
|
165
|
-
const result = run.results?.[task];
|
|
166
|
-
if (truncated && result) {
|
|
167
|
-
// the end of a long log is kept in the result
|
|
168
|
-
for (const line of ['… the server cut the log off at its limit, the last lines were:', ...result.lastLog]) {
|
|
169
|
-
lines.push(line);
|
|
170
|
-
options.onLine?.(line);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
run,
|
|
175
|
-
result,
|
|
176
|
-
output: lines.join('\n'),
|
|
177
|
-
truncated,
|
|
178
|
-
exitCode: exitCodeOf(result),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
finally {
|
|
182
|
-
options.signal?.removeEventListener('abort', cancel);
|
|
183
|
-
}
|
|
184
140
|
}
|
|
185
141
|
/**
|
|
186
142
|
* A shell command on the sandbox: the shell task of the build file with the command as
|
|
@@ -245,6 +201,88 @@ function oneAtATime() {
|
|
|
245
201
|
};
|
|
246
202
|
}
|
|
247
203
|
exports.oneAtATime = oneAtATime;
|
|
204
|
+
// a run that is over, its state does not change any more
|
|
205
|
+
exports.FINAL_RUN_STATES = ['succeeded', 'failed', 'cancelled', 'rejected'];
|
|
206
|
+
// Submits a run, as `POST /v1/runs` takes it, and answers it as the server queued it or
|
|
207
|
+
// holds it for an approval.
|
|
208
|
+
async function submitRun(connection, request) {
|
|
209
|
+
return json(await call(connection, 'POST', '/v1/runs', { body: request }));
|
|
210
|
+
}
|
|
211
|
+
exports.submitRun = submitRun;
|
|
212
|
+
/**
|
|
213
|
+
* Follows a run to its end: its log as it grows, and its state. A signal cancels the run
|
|
214
|
+
* on the server and fails the follow.
|
|
215
|
+
*/
|
|
216
|
+
async function followRun(connection, submitted, options = {}) {
|
|
217
|
+
const path = `/v1/runs/${encodeURIComponent(submitted.id)}`;
|
|
218
|
+
let cancelling;
|
|
219
|
+
const cancel = () => {
|
|
220
|
+
cancelling ?? (cancelling = call(connection, 'POST', `${path}/cancel`).catch(() => undefined));
|
|
221
|
+
};
|
|
222
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
223
|
+
const lines = [];
|
|
224
|
+
let from = 1;
|
|
225
|
+
let truncated = false;
|
|
226
|
+
const add = (line) => {
|
|
227
|
+
lines.push(line);
|
|
228
|
+
options.onLine?.(line);
|
|
229
|
+
};
|
|
230
|
+
const readLog = async () => {
|
|
231
|
+
for (;;) {
|
|
232
|
+
const page = await json(await call(connection, 'GET', `${path}/logs`, { query: { task: options.task, from, limit: 1000 } }));
|
|
233
|
+
truncated = page.truncated;
|
|
234
|
+
for (const line of page.lines) {
|
|
235
|
+
add(line);
|
|
236
|
+
from = line.line + 1;
|
|
237
|
+
}
|
|
238
|
+
if (!page.next) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
let run = submitted;
|
|
245
|
+
let waitingSaid = false;
|
|
246
|
+
for (;;) {
|
|
247
|
+
if (options.signal?.aborted) {
|
|
248
|
+
// the process may exit after this, so the cancel has to reach the server first
|
|
249
|
+
cancel();
|
|
250
|
+
let timer;
|
|
251
|
+
await Promise.race([cancelling, new Promise((resolve) => (timer = setTimeout(resolve, CANCEL_WAIT_MS)))]);
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
throw new SandboxClientError('the run was cancelled');
|
|
254
|
+
}
|
|
255
|
+
if (run.state === 'awaiting-approval' && !waitingSaid) {
|
|
256
|
+
waitingSaid = true;
|
|
257
|
+
options.onWaiting?.(run);
|
|
258
|
+
}
|
|
259
|
+
await readLog();
|
|
260
|
+
if (exports.FINAL_RUN_STATES.includes(run.state)) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
run = await json(await call(connection, 'GET', path, { query: { wait: POLL_WAIT } }));
|
|
264
|
+
}
|
|
265
|
+
if (truncated) {
|
|
266
|
+
// the end of a long log is kept in the results
|
|
267
|
+
const tasks = options.task !== undefined ? [options.task] : Object.keys(run.results ?? {});
|
|
268
|
+
for (const task of tasks) {
|
|
269
|
+
const result = run.results?.[task];
|
|
270
|
+
if (!result) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const said = options.task !== undefined ? 'the last lines were' : `the last lines of ${task} were`;
|
|
274
|
+
for (const text of [`… the server cut the log off at its limit, ${said}:`, ...result.lastLog]) {
|
|
275
|
+
add({ line: 0, task, stream: 'stdout', text });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { run, lines, truncated };
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
exports.followRun = followRun;
|
|
248
286
|
function exitCodeOf(result) {
|
|
249
287
|
if (!result) {
|
|
250
288
|
return null;
|
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,17 +18,13 @@ 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
|
: {
|
|
@@ -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 &&
|