hammerkit 2.0.0-next.6 → 2.0.0-next.7
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ensurePersistentData = exports.packSource = exports.clearStateFiles = exports.restoreLiveFiles = exports.syncLiveFiles = exports.SYNC_FILES = exports.getStateDirsInUse = exports.REMOVE_OTHER_STATES = exports.execInPod = exports.clearClaim = exports.getPodForPersistence = exports.applyWithinQuota = exports.QUOTA_TIMEOUT = exports.TRANSFER_POD_RESOURCES = void 0;
|
|
3
|
+
exports.ensurePersistentData = exports.packSource = exports.clearStateFiles = exports.ENSURE_OUTPUT_FILES = exports.restoreLiveFiles = exports.syncLiveFiles = exports.SYNC_FILES = exports.getStateDirsInUse = exports.REMOVE_OTHER_STATES = exports.execInPod = exports.clearClaim = exports.getPodForPersistence = exports.applyWithinQuota = exports.QUOTA_TIMEOUT = exports.TRANSFER_POD_RESOURCES = void 0;
|
|
4
4
|
const volumes_1 = require("./volumes");
|
|
5
5
|
const apply_1 = require("./apply");
|
|
6
6
|
const path_1 = require("path");
|
|
@@ -88,7 +88,11 @@ signal, fn) {
|
|
|
88
88
|
// and takes no upload
|
|
89
89
|
const own = persistence.mounts.filter((m) => !m.workspace);
|
|
90
90
|
const mounts = type === 'read'
|
|
91
|
-
?
|
|
91
|
+
? // what a task stores are its outputs, which it mounts writable: its src and the
|
|
92
|
+
// outputs of its deps are not mounted, so an output inside a folder of its src
|
|
93
|
+
// (pkg/coverage/lcov.info) has a place in the pod even when the claim holds no src,
|
|
94
|
+
// after a restore into a new claim
|
|
95
|
+
own.filter((m) => service.data.type !== 'container-task' || !m.mount.readOnly).map((m) => m.mount)
|
|
92
96
|
: own.reduce((array, v) => {
|
|
93
97
|
if (!array.some((i) => i.name === v.mount.name)) {
|
|
94
98
|
array.push({
|
|
@@ -99,11 +103,13 @@ signal, fn) {
|
|
|
99
103
|
}
|
|
100
104
|
return array;
|
|
101
105
|
}, []);
|
|
106
|
+
// only the claims it mounts: one of a dep it doesn't read would only bind where it goes
|
|
107
|
+
const volumes = persistence.volumes.filter((volume) => mounts.some((mount) => mount.name === volume.name));
|
|
102
108
|
// Guarantee the claims exist right before scheduling the pod that mounts
|
|
103
109
|
// them. ensurePersistentData also creates them, but store/restore reach this
|
|
104
110
|
// function directly, and a claim shared with another work item can be removed
|
|
105
111
|
// concurrently — without this the pod would hang Pending on "pvc not found".
|
|
106
|
-
for (const volume of
|
|
112
|
+
for (const volume of volumes) {
|
|
107
113
|
await (0, ensure_kubernetes_persistent_volume_claim_exists_1.ensureKubernetesPersistentVolumeClaimExists)(instance, env, volume, service, persistence.sizes[volume.name], persistence.kept.includes(volume.name));
|
|
108
114
|
}
|
|
109
115
|
const name = (0, resources_1.getResourceName)(service, env, type === 'write' ? '-upload' : '-download');
|
|
@@ -111,7 +117,7 @@ signal, fn) {
|
|
|
111
117
|
// commit): the pod is not one its Service sends to
|
|
112
118
|
const serving = !!env.server?.serviceRun && service.data.type === 'container-service';
|
|
113
119
|
// and on a service run, it goes where the pods that mount the same claims run
|
|
114
|
-
const claims = env.server?.serviceRun ? (0, volumes_1.getClaimPodSpec)(
|
|
120
|
+
const claims = env.server?.serviceRun ? (0, volumes_1.getClaimPodSpec)(volumes) : { labels: {} };
|
|
115
121
|
const podSpec = {
|
|
116
122
|
kind: 'Pod',
|
|
117
123
|
apiVersion: 'v1',
|
|
@@ -144,7 +150,7 @@ signal, fn) {
|
|
|
144
150
|
...(0, job_placement_1.getContainerSecurity)(env),
|
|
145
151
|
},
|
|
146
152
|
],
|
|
147
|
-
volumes
|
|
153
|
+
volumes,
|
|
148
154
|
},
|
|
149
155
|
};
|
|
150
156
|
service.status.write('debug', `create pod ${name}`);
|
|
@@ -329,6 +335,33 @@ async function prepareStateFiles(instance, env, work, pod, dir, inUse) {
|
|
|
329
335
|
await clearStateDir(instance, env, pod, `${targetPath}/${dir}`);
|
|
330
336
|
return true;
|
|
331
337
|
}
|
|
338
|
+
// Makes each of `files` (paths in the claim of `work`, see KubernetesPersistence.files) an
|
|
339
|
+
// empty file unless it is one: Kubernetes would make a directory of a subPath that isn't
|
|
340
|
+
// there, and the task could not write the file. A directory there is what an earlier
|
|
341
|
+
// version left in a kept claim, and goes. A path ending in `/` is made a folder.
|
|
342
|
+
exports.ENSURE_OUTPUT_FILES = `cd "$1" || exit 1
|
|
343
|
+
shift
|
|
344
|
+
for f; do
|
|
345
|
+
case "$f" in */) mkdir -p "$f" || exit 1; continue ;; esac
|
|
346
|
+
if [ -d "$f" ] && [ ! -L "$f" ]; then rm -rf "$f" || exit 1; fi
|
|
347
|
+
if [ ! -e "$f" ] && [ ! -L "$f" ]; then mkdir -p "$(dirname "$f")" && : > "$f" || exit 1; fi
|
|
348
|
+
done`;
|
|
349
|
+
async function ensureOutputFiles(instance, env, work, pod, files) {
|
|
350
|
+
if (files.length === 0) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const made = await execInPod(instance, env, pod, [
|
|
354
|
+
'sh',
|
|
355
|
+
'-c',
|
|
356
|
+
exports.ENSURE_OUTPUT_FILES,
|
|
357
|
+
'sh',
|
|
358
|
+
`/dev/hammerkit/${work.id()}`,
|
|
359
|
+
...files,
|
|
360
|
+
]);
|
|
361
|
+
if (made) {
|
|
362
|
+
throw new Error(`unable to make the output files ${files.join(', ')} of ${work.name}: ${made}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
332
365
|
// what a failed upload, restore or task left: tar does not remove files the commit does not have
|
|
333
366
|
async function clearStateDir(instance, env, pod, path) {
|
|
334
367
|
const cleared = await execInPod(instance, env, pod, ['rm', '-rf', path]);
|
|
@@ -397,7 +430,7 @@ options = {}) {
|
|
|
397
430
|
const files = persistence.volumes.some((volume) => volume.persistentVolumeClaim?.claimName === claim)
|
|
398
431
|
? options.files
|
|
399
432
|
: undefined;
|
|
400
|
-
if (persistence.sources.length === 0 && !options.reset && !files) {
|
|
433
|
+
if (persistence.sources.length === 0 && persistence.files.length === 0 && !options.reset && !files) {
|
|
401
434
|
return;
|
|
402
435
|
}
|
|
403
436
|
// the folders of the states the pods of the run mount, before the upload pod mounts the claim
|
|
@@ -447,6 +480,7 @@ options = {}) {
|
|
|
447
480
|
}
|
|
448
481
|
});
|
|
449
482
|
}
|
|
483
|
+
await ensureOutputFiles(instance, env, service, name, persistence.files);
|
|
450
484
|
if (files) {
|
|
451
485
|
const dir = `/dev/hammerkit/${service.id()}/${files.dir}`;
|
|
452
486
|
const marked = await execInPod(instance, env, name, [
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.restoreKubernetesData = exports.getRestoreTarget = void 0;
|
|
3
|
+
exports.restoreKubernetesData = exports.getRestoreTarget = exports.RESTORE_FILE = void 0;
|
|
4
4
|
const event_cache_1 = require("../executer/event-cache");
|
|
5
5
|
const log_1 = require("../log");
|
|
6
6
|
const volumes_1 = require("./volumes");
|
|
7
7
|
const ensure_persistent_data_1 = require("./ensure-persistent-data");
|
|
8
8
|
const claim_layout_1 = require("./claim-layout");
|
|
9
|
+
// Unpacks the archive of an output that is a file to `$1`: by its content, since the name in
|
|
10
|
+
// the archive is the file's and the one in the claim may carry a hash (see getClaimSubPath)
|
|
11
|
+
exports.RESTORE_FILE = 'mkdir -p "$(dirname "$1")" && rm -rf "$1" && tar xzf - -O > "$1"';
|
|
9
12
|
// Where the upload pod (which mounts the work's claim at /dev/hammerkit/<id>) unpacks the
|
|
10
13
|
// archive of an output: the output's place in the claim, which its mount reads
|
|
11
14
|
function getRestoreTarget(work, path,
|
|
@@ -22,6 +25,13 @@ async function restoreKubernetesData(work, kubernetes, instance, environment, pa
|
|
|
22
25
|
options = {}) {
|
|
23
26
|
const persistence = await (0, volumes_1.getKubernetesPersistence)(work, kubernetes, options.stateKey);
|
|
24
27
|
const stateDir = kubernetes.server?.serviceRun ? (0, volumes_1.getItemFilesDir)(work, options.stateKey) : undefined;
|
|
28
|
+
// an output that is a file: its archive holds it alone (see storeKubernetesData), and it
|
|
29
|
+
// takes the place of whatever is at its path in the claim
|
|
30
|
+
const files = new Set(work.data.type === 'container-task'
|
|
31
|
+
? work.data.generates
|
|
32
|
+
.filter((generate) => generate.isFile && !generate.inherited)
|
|
33
|
+
.map((generate) => generate.path)
|
|
34
|
+
: []);
|
|
25
35
|
// a run stopped while the pod waits for room under the quota
|
|
26
36
|
const { signal } = environment.abortCtrl;
|
|
27
37
|
await (0, ensure_persistent_data_1.getPodForPersistence)(instance, kubernetes, work, persistence, 'write', signal, async (name) => {
|
|
@@ -39,7 +49,13 @@ options = {}) {
|
|
|
39
49
|
// a claim made for this run (a server removes the claims of a run when it
|
|
40
50
|
// ends) does not have the directory of the output yet
|
|
41
51
|
let failure = null;
|
|
42
|
-
const res = await instance.exec.exec(kubernetes.namespace, name, 'volume', [
|
|
52
|
+
const res = await instance.exec.exec(kubernetes.namespace, name, 'volume', [
|
|
53
|
+
'sh',
|
|
54
|
+
'-c',
|
|
55
|
+
files.has(generatedArchive.path) ? exports.RESTORE_FILE : 'mkdir -p "$1" && tar xzf - -C "$1"',
|
|
56
|
+
'sh',
|
|
57
|
+
targetPath,
|
|
58
|
+
], process.stdout, process.stderr, environment.file.readStream(generatedArchive.filename), false, (status) => {
|
|
43
59
|
work.status.write('debug', `upload ${generatedArchive.filename} to ${name}: ${status.status}`);
|
|
44
60
|
if (status.status !== 'Success') {
|
|
45
61
|
failure = status.message ?? status.reason ?? 'unknown';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.storeKubernetesData = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
4
5
|
const log_1 = require("../log");
|
|
5
6
|
const event_cache_1 = require("../executer/event-cache");
|
|
6
7
|
const volumes_1 = require("./volumes");
|
|
@@ -12,10 +13,18 @@ stateKey) {
|
|
|
12
13
|
const persistence = await (0, volumes_1.getKubernetesPersistence)(work, kubernetes, stateKey);
|
|
13
14
|
// a run stopped while the pod waits for room under the quota
|
|
14
15
|
const { signal } = environment.abortCtrl;
|
|
16
|
+
// an output that is a file is mounted as one: its archive holds it by its name, as on Docker
|
|
17
|
+
const files = new Set(work.data.type === 'container-task'
|
|
18
|
+
? work.data.generates
|
|
19
|
+
.filter((generate) => generate.isFile && !generate.inherited)
|
|
20
|
+
.map((generate) => generate.path)
|
|
21
|
+
: []);
|
|
15
22
|
await (0, ensure_persistent_data_1.getPodForPersistence)(instance, kubernetes, work, persistence, 'read', signal, async (name) => {
|
|
16
23
|
for (const generatedArchive of (0, event_cache_1.getArchivePaths)(work.data, path)) {
|
|
17
24
|
const writerStream = environment.file.createWriteStream(generatedArchive.filename);
|
|
18
|
-
const res = await instance.exec.exec(kubernetes.namespace, name, 'volume',
|
|
25
|
+
const res = await instance.exec.exec(kubernetes.namespace, name, 'volume', files.has(generatedArchive.path)
|
|
26
|
+
? ['tar', 'zcf', '-', '-C', path_1.posix.dirname(generatedArchive.path), path_1.posix.basename(generatedArchive.path)]
|
|
27
|
+
: ['tar', 'zcf', '-', '-C', generatedArchive.path, '.'], writerStream, process.stderr, null, false, (status) => {
|
|
19
28
|
work.status.write('debug', `archive ${generatedArchive.path} of ${name}: ${status}`);
|
|
20
29
|
writerStream.close();
|
|
21
30
|
});
|
|
@@ -59,6 +59,7 @@ stateKey) {
|
|
|
59
59
|
sources: [],
|
|
60
60
|
mounts: [],
|
|
61
61
|
initCommands: [],
|
|
62
|
+
files: [],
|
|
62
63
|
};
|
|
63
64
|
const workspace = kubernetes.server?.workspace;
|
|
64
65
|
for (const mount of work.data.mounts) {
|
|
@@ -79,8 +80,12 @@ stateKey) {
|
|
|
79
80
|
}
|
|
80
81
|
if (work.data.type === 'container-task') {
|
|
81
82
|
for (const generate of work.data.generates) {
|
|
82
|
-
appendVolume(generate.inherited ?? work, persistence, generate.path, null,
|
|
83
|
+
const subPath = appendVolume(generate.inherited ?? work, persistence, generate.path, null, generate.isFile, !!generate.inherited, kubernetes, dirOf(generate.inherited ?? work));
|
|
84
|
+
if (generate.isFile && !generate.inherited) {
|
|
85
|
+
persistence.files.push(subPath);
|
|
86
|
+
}
|
|
83
87
|
}
|
|
88
|
+
persistence.files.push(...getNestedMountPlaces(persistence, getVolumeName(work, kubernetes)));
|
|
84
89
|
}
|
|
85
90
|
else {
|
|
86
91
|
for (const volume of work.data.volumes) {
|
|
@@ -92,6 +97,27 @@ stateKey) {
|
|
|
92
97
|
return persistence;
|
|
93
98
|
}
|
|
94
99
|
exports.getKubernetesPersistence = getKubernetesPersistence;
|
|
100
|
+
// The places in the claim `claim` of the paths mounted inside a folder that is mounted read
|
|
101
|
+
// only from it: an output of a task inside a folder of its src (pkg/coverage/lcov.info with a
|
|
102
|
+
// src pkg). The container runtime makes the place of a mount where it isn't there, which it
|
|
103
|
+
// can't in a folder mounted read only, and the pod fails to start (exit 128). Each is in the
|
|
104
|
+
// claim's folder of the read-only path, a folder ending in `/`, or a file.
|
|
105
|
+
function getNestedMountPlaces(persistence, claim) {
|
|
106
|
+
const own = persistence.mounts.filter((m) => !m.workspace);
|
|
107
|
+
const folders = own.filter((m) => m.mount.name === claim && m.mount.readOnly && m.cwd === m.mount.mountPath && m.mount.subPath);
|
|
108
|
+
const places = [];
|
|
109
|
+
for (const mount of own) {
|
|
110
|
+
const parent = folders
|
|
111
|
+
.filter((folder) => folder !== mount && mount.mount.mountPath.startsWith(`${folder.mount.mountPath}/`))
|
|
112
|
+
.sort((a, b) => b.mount.mountPath.length - a.mount.mountPath.length)[0];
|
|
113
|
+
if (!parent) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const place = `${parent.mount.subPath}/${mount.mount.mountPath.slice(parent.mount.mountPath.length + 1)}`;
|
|
117
|
+
places.push(mount.cwd === mount.mount.mountPath ? `${place}/` : place);
|
|
118
|
+
}
|
|
119
|
+
return places;
|
|
120
|
+
}
|
|
95
121
|
function appendVolume(sourceWork, persistence, containerPath, localPath, isFile, readOnly, kubernetes,
|
|
96
122
|
// the folder of the state of `sourceWork` the path is in, see getStateFilesDir
|
|
97
123
|
stateDir,
|
|
@@ -134,6 +160,7 @@ claimPath = (0, claim_layout_1.getClaimSubPath)(sourceWork, containerPath)) {
|
|
|
134
160
|
stateKey: '', // TODO
|
|
135
161
|
});
|
|
136
162
|
}
|
|
163
|
+
return subPath;
|
|
137
164
|
}
|
|
138
165
|
exports.appendVolume = appendVolume;
|
|
139
166
|
/**
|
package/package.json
CHANGED