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
- ? own.map((m) => m.mount)
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 persistence.volumes) {
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)(persistence.volumes) : { labels: {} };
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: persistence.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', ['sh', '-c', 'mkdir -p "$1" && tar xzf - -C "$1"', 'sh', targetPath], process.stdout, process.stderr, environment.file.readStream(generatedArchive.filename), false, (status) => {
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', ['tar', 'zcf', '-', '-C', generatedArchive.path, '.'], writerStream, process.stderr, null, false, (status) => {
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, false, !!generate.inherited, kubernetes, dirOf(generate.inherited ?? work));
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "2.0.0-next.6",
4
+ "version": "2.0.0-next.7",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",