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.
- package/dist/cache/backends/registry/registry-client.js +19 -4
- package/dist/docker/pull.js +77 -8
- package/dist/environment/replace-env-variables.js +53 -22
- package/dist/executer/docker-service.js +1 -1
- package/dist/executer/docker-task.js +1 -1
- package/dist/executer/execution-steps.js +2 -2
- package/dist/planner/utils/template-value.js +1 -1
- package/dist/schema/work-scope-parser.js +13 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
});
|
package/dist/docker/pull.js
CHANGED
|
@@ -1,20 +1,89 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pull = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
11
|
-
if (
|
|
12
|
-
|
|
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
|
-
|
|
20
|
-
replacements.push({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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(
|
|
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