hammerkit 1.9.2 → 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
|
-
|
|
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,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.splitImageName = exports.isImagePresent = exports.pull = void 0;
|
|
3
|
+
exports.normalizeArchitecture = exports.splitImageName = exports.isImagePresent = exports.pull = void 0;
|
|
4
4
|
const registry_credentials_1 = require("../cache/backends/registry/registry-credentials");
|
|
5
5
|
const registry_reference_1 = require("../cache/backends/registry/registry-reference");
|
|
6
6
|
async function pull(status, docker, imageName, environment) {
|
|
7
7
|
const images = await docker.listImages({});
|
|
8
8
|
if (isImagePresent(images, imageName)) {
|
|
9
|
+
await warnOnForeignPlatform(status, docker, imageName);
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
12
|
status.write('debug', `pull image ${imageName}`);
|
|
@@ -49,4 +50,40 @@ async function getPullAuth(imageName, environment) {
|
|
|
49
50
|
}
|
|
50
51
|
return { ...credentials, serveraddress: credentialHost };
|
|
51
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;
|
|
52
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) {
|
|
@@ -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