hammerkit 1.9.1 → 1.10.0

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.digestBuffer = exports.digestFile = exports.RegistryClient = exports.parseChallenge = exports.RegistryError = exports.OCI_LAYER_TAR = exports.OCI_CONFIG = exports.OCI_MANIFEST = void 0;
3
+ exports.digestBuffer = exports.digestFile = exports.RegistryClient = exports.getTokenScope = exports.parseChallenge = exports.RegistryError = exports.OCI_LAYER_TAR = exports.OCI_CONFIG = exports.OCI_MANIFEST = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const fs_1 = require("fs");
6
6
  const promises_1 = require("fs/promises");
@@ -32,6 +32,23 @@ function parseChallenge(header) {
32
32
  return { scheme, params };
33
33
  }
34
34
  exports.parseChallenge = parseChallenge;
35
+ // Always ask for pull and push, so one token serves the whole backend, plus
36
+ // what the challenge names for this repository: a DELETE is challenged for
37
+ // `delete`, which a pull/push token lacks. A registry grants the subset the
38
+ // credentials allow.
39
+ function getTokenScope(repository, challengeScope) {
40
+ const actions = ['pull', 'push'];
41
+ const prefix = `repository:${repository}:`;
42
+ if (challengeScope?.startsWith(prefix)) {
43
+ for (const action of challengeScope.substring(prefix.length).split(',')) {
44
+ if (action && !actions.includes(action)) {
45
+ actions.push(action);
46
+ }
47
+ }
48
+ }
49
+ return `${prefix}${actions.join(',')}`;
50
+ }
51
+ exports.getTokenScope = getTokenScope;
35
52
  function basicHeader(credentials) {
36
53
  return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}`;
37
54
  }
@@ -76,9 +93,7 @@ class RegistryClient {
76
93
  if (challenge.params.service) {
77
94
  tokenUrl.searchParams.set('service', challenge.params.service);
78
95
  }
79
- // always ask for push too: one token then serves the whole backend; a
80
- // registry grants the subset the credentials allow
81
- tokenUrl.searchParams.set('scope', `repository:${this.reference.repository}:pull,push`);
96
+ tokenUrl.searchParams.set('scope', getTokenScope(this.reference.repository, challenge.params.scope ?? null));
82
97
  const response = await fetch(tokenUrl, {
83
98
  headers: credentials ? { authorization: basicHeader(credentials) } : {},
84
99
  });
@@ -1,20 +1,89 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pull = void 0;
4
- async function pull(status, docker, imageName) {
5
- let searchImageName = imageName;
6
- if (imageName.indexOf(':') === -1) {
7
- searchImageName += ':latest';
8
- }
3
+ exports.normalizeArchitecture = exports.splitImageName = exports.isImagePresent = exports.pull = void 0;
4
+ const registry_credentials_1 = require("../cache/backends/registry/registry-credentials");
5
+ const registry_reference_1 = require("../cache/backends/registry/registry-reference");
6
+ async function pull(status, docker, imageName, environment) {
9
7
  const images = await docker.listImages({});
10
- if (images.some((i) => i.RepoTags?.some((repoTag) => repoTag === searchImageName))) {
8
+ if (isImagePresent(images, imageName)) {
9
+ await warnOnForeignPlatform(status, docker, imageName);
11
10
  return;
12
11
  }
13
12
  status.write('debug', `pull image ${imageName}`);
14
- const image = await docker.pull(imageName);
13
+ const authconfig = await getPullAuth(imageName, environment);
14
+ const image = await docker.pull(imageName, authconfig ? { authconfig } : {});
15
15
  await new Promise((resolve, reject) => {
16
16
  docker.modem.followProgress(image, (err, res) => (err ? reject(err) : resolve(res)));
17
17
  });
18
18
  }
19
19
  exports.pull = pull;
20
+ // A tag is present when an image carries it; an image pinned by digest
21
+ // (`repo:tag@sha256:…`) when an image was pulled with that digest, whatever
22
+ // tag it has now.
23
+ function isImagePresent(images, imageName) {
24
+ const { repository, tag, digest } = splitImageName(imageName);
25
+ if (digest) {
26
+ return images.some((i) => i.RepoDigests?.some((repoDigest) => repoDigest === `${repository}@${digest}`));
27
+ }
28
+ return images.some((i) => i.RepoTags?.some((repoTag) => repoTag === `${repository}:${tag ?? 'latest'}`));
29
+ }
30
+ exports.isImagePresent = isImagePresent;
31
+ function splitImageName(imageName) {
32
+ const [name, digest] = imageName.split('@');
33
+ const lastSlash = name.lastIndexOf('/');
34
+ const tagSeparator = name.indexOf(':', lastSlash + 1);
35
+ if (tagSeparator === -1) {
36
+ return { repository: name, tag: null, digest: digest ?? null };
37
+ }
38
+ return { repository: name.substring(0, tagSeparator), tag: name.substring(tagSeparator + 1), digest: digest ?? null };
39
+ }
40
+ exports.splitImageName = splitImageName;
41
+ // The credentials `docker login` stored for the image's registry, like the
42
+ // docker CLI uses them; dockerode sends none on its own, so a private image
43
+ // could only be used when it was already present.
44
+ async function getPullAuth(imageName, environment) {
45
+ const { repository } = splitImageName(imageName);
46
+ const { credentialHost } = (0, registry_reference_1.parseRegistryReference)(repository);
47
+ const credentials = await (0, registry_credentials_1.resolveRegistryCredentials)(credentialHost, environment);
48
+ if (!credentials) {
49
+ return null;
50
+ }
51
+ return { ...credentials, serveraddress: credentialHost };
52
+ }
53
+ const daemonArchitectures = new WeakMap();
54
+ const warnedImages = new Set();
55
+ // A digest pins a multi-arch index; a copy pulled earlier for another
56
+ // architecture (an amd64 one from a Compose file with `platform:`) carries the
57
+ // same digest, so docker uses it and the image runs emulated instead of native.
58
+ async function warnOnForeignPlatform(status, docker, imageName) {
59
+ const { repository, digest } = splitImageName(imageName);
60
+ if (!digest || warnedImages.has(imageName)) {
61
+ return;
62
+ }
63
+ try {
64
+ if (!daemonArchitectures.has(docker)) {
65
+ daemonArchitectures.set(docker, docker.info().then((info) => normalizeArchitecture(info.Architecture ?? '')));
66
+ }
67
+ const [image, daemonArchitecture] = await Promise.all([
68
+ docker.getImage(imageName).inspect(),
69
+ daemonArchitectures.get(docker),
70
+ ]);
71
+ const imageArchitecture = normalizeArchitecture(image.Architecture ?? '');
72
+ if (!imageArchitecture || !daemonArchitecture || imageArchitecture === daemonArchitecture) {
73
+ return;
74
+ }
75
+ warnedImages.add(imageName);
76
+ status.write('warn', `image ${imageName} is present for ${imageArchitecture} on this ${daemonArchitecture} docker host and runs ` +
77
+ `emulated; if the digest is a multi-arch index, remove the copy to pull the native one: ` +
78
+ `docker image rm ${repository}@${digest}`);
79
+ }
80
+ catch {
81
+ // only a hint: never fail a pull over it
82
+ }
83
+ }
84
+ function normalizeArchitecture(architecture) {
85
+ const aliases = { x86_64: 'amd64', aarch64: 'arm64' };
86
+ return aliases[architecture] ?? architecture;
87
+ }
88
+ exports.normalizeArchitecture = normalizeArchitecture;
20
89
  //# sourceMappingURL=pull.js.map
@@ -1,36 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getEnvironmentVariables = exports.buildEnvironmentVariables = void 0;
4
+ // `${NAME}` or `${NAME:-default}` anywhere in a value
5
+ const INTERPOLATION = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
6
+ // The shell / process environment first, then the .env files. A variable set to
7
+ // an empty string is set, as in a shell: CI providers leave many empty (a merge
8
+ // request id on a branch pipeline).
9
+ function resolveReference(name, environment, context) {
10
+ const processValue = environment.processEnvs[name];
11
+ if (processValue !== undefined && processValue !== null) {
12
+ return processValue;
13
+ }
14
+ for (const envFile of Object.values(context.envFiles)) {
15
+ const fileValue = envFile[name];
16
+ if (fileValue !== undefined && fileValue !== null) {
17
+ return fileValue;
18
+ }
19
+ }
20
+ return null;
21
+ }
4
22
  function buildEnvironmentVariables(envs, environment, context) {
5
23
  const replacements = [];
6
24
  const variables = {};
7
25
  for (const [key, value] of Object.entries(envs)) {
8
- if (value.startsWith('$')) {
26
+ if (value.startsWith('$') && !value.startsWith('${')) {
9
27
  const name = value.substring(1);
10
- let nameValue = environment.processEnvs[name] ?? null;
11
- if (!nameValue) {
12
- for (const envFile of Object.values(context.envFiles)) {
13
- nameValue = envFile[name] ?? null;
14
- if (nameValue) {
15
- break;
16
- }
17
- }
28
+ const nameValue = resolveReference(name, environment, context);
29
+ if (nameValue !== null) {
30
+ replacements.push({ key, name, available: true, value: nameValue });
18
31
  }
19
- if (nameValue) {
20
- replacements.push({
21
- key,
22
- name,
23
- available: true,
24
- value: nameValue,
25
- });
32
+ else {
33
+ replacements.push({ key, name, available: false, value: null });
34
+ }
35
+ }
36
+ else if (value.includes('${')) {
37
+ const interpolated = interpolate(value, envs, environment, context);
38
+ if (interpolated.missing) {
39
+ replacements.push({ key, name: interpolated.missing, available: false, value: null });
26
40
  }
27
41
  else {
28
- replacements.push({
29
- key,
30
- name,
31
- available: false,
32
- value: null,
33
- });
42
+ variables[key] = interpolated.value;
34
43
  }
35
44
  }
36
45
  else {
@@ -40,6 +49,28 @@ function buildEnvironmentVariables(envs, environment, context) {
40
49
  return { variables, replacements };
41
50
  }
42
51
  exports.buildEnvironmentVariables = buildEnvironmentVariables;
52
+ // Each `${NAME}` comes from the process environment or a .env file, else from a
53
+ // literal value of the same build file / task `envs`, else from its
54
+ // `:-default`. A reference without any of them is missing.
55
+ function interpolate(value, envs, environment, context) {
56
+ let missing = null;
57
+ const result = value.replace(INTERPOLATION, (match, name, fallback) => {
58
+ const resolved = resolveReference(name, environment, context) ?? getLiteral(envs, name) ?? fallback ?? null;
59
+ if (resolved === null) {
60
+ missing = missing ?? name;
61
+ return match;
62
+ }
63
+ return resolved;
64
+ });
65
+ return { value: result, missing };
66
+ }
67
+ function getLiteral(envs, name) {
68
+ const literal = envs[name];
69
+ if (literal === undefined || literal.includes('$')) {
70
+ return null;
71
+ }
72
+ return literal;
73
+ }
43
74
  function getEnvironmentVariables(envs) {
44
75
  const result = { ...envs.variables };
45
76
  for (const replacement of envs.replacements) {
@@ -23,7 +23,7 @@ async function dockerService(docker, item, options, environment) {
23
23
  try {
24
24
  await (0, execution_steps_1.prepareMounts)(item, environment);
25
25
  (0, abort_1.checkForAbort)(options.abort);
26
- await (0, execution_steps_1.pullImage)(item, docker);
26
+ await (0, execution_steps_1.pullImage)(item, docker, environment);
27
27
  (0, abort_1.checkForAbort)(options.abort);
28
28
  await (0, execution_steps_1.prepareVolume)(item, docker);
29
29
  (0, abort_1.checkForAbort)(options.abort);
@@ -79,7 +79,7 @@ async function dockerTask(docker, item, environment, options) {
79
79
  }
80
80
  await (0, execution_steps_1.prepareMounts)(item, environment);
81
81
  (0, abort_1.checkForAbort)(options.abort);
82
- await (0, execution_steps_1.pullImage)(item, docker);
82
+ await (0, execution_steps_1.pullImage)(item, docker, environment);
83
83
  (0, abort_1.checkForAbort)(options.abort);
84
84
  await (0, execution_steps_1.prepareVolume)(item, docker);
85
85
  (0, abort_1.checkForAbort)(options.abort);
@@ -8,8 +8,8 @@ const set_user_permission_1 = require("./set-user-permission");
8
8
  const work_task_1 = require("../planner/work-task");
9
9
  const work_service_1 = require("../planner/work-service");
10
10
  const get_container_binds_1 = require("./get-container-binds");
11
- async function pullImage(item, docker) {
12
- await (0, pull_1.pull)(item.status, docker, item.data.image);
11
+ async function pullImage(item, docker, environment) {
12
+ await (0, pull_1.pull)(item.status, docker, item.data.image, environment);
13
13
  }
14
14
  exports.pullImage = pullImage;
15
15
  async function prepareVolume(item, docker) {
@@ -7,7 +7,7 @@ function templateValue(value, envs) {
7
7
  }
8
8
  let result = value;
9
9
  for (const [key, envValue] of Object.entries(envs.variables)) {
10
- result = result.replace(new RegExp(`\\$${key}`, 'gi'), envValue);
10
+ result = result.replace(new RegExp(`\\$\\{${key}\\}|\\$${key}`, 'gi'), envValue);
11
11
  }
12
12
  return result;
13
13
  }
@@ -50,9 +50,22 @@ function getWorkContext(context, scope, environment) {
50
50
  applyService(filteredWorkTree, item);
51
51
  }
52
52
  }
53
+ keepRequirersInScope(filteredWorkTree);
53
54
  return filteredWorkTree;
54
55
  }
55
56
  exports.getWorkContext = getWorkContext;
57
+ // The whole build file is planned first, so a shared service lists every task
58
+ // and service that needs it. Those outside this run never start or end, and a
59
+ // service waiting for them to finish would keep the run open forever.
60
+ function keepRequirersInScope(workTree) {
61
+ const inScope = new Set([
62
+ ...Object.values(workTree.tasks),
63
+ ...Object.values(workTree.services),
64
+ ]);
65
+ for (const service of Object.values(workTree.services)) {
66
+ service.requiredBy = service.requiredBy.filter((required) => inScope.has(required));
67
+ }
68
+ }
56
69
  function applyTask(workTree, item) {
57
70
  if (workTree.tasks[item.name]) {
58
71
  return;
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": "1.9.1",
4
+ "version": "1.10.0",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",