containerify 2.6.1 → 3.0.1
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/README.md +28 -7
- package/lib/cli.js +30 -9
- package/lib/httpRequest.d.ts +23 -0
- package/lib/httpRequest.js +98 -0
- package/lib/registry.d.ts +6 -8
- package/lib/registry.js +251 -278
- package/lib/types.d.ts +12 -0
- package/lib/types.js +6 -0
- package/lib/utils.d.ts +5 -0
- package/lib/utils.js +12 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -30,37 +30,58 @@ containerify --fromImage nginx:alpine --folder . --toImage frontend:latest --cus
|
|
|
30
30
|
|
|
31
31
|
This will take the `nginx:alpine` image, and copy the files from `./dist/` into `/usr/share/nginx/html`.
|
|
32
32
|
|
|
33
|
+
### Using with GitHub Container Registry (ghcr.io)
|
|
34
|
+
|
|
35
|
+
1. Create a token from https://github.com/settings/tokens/new?scopes=write:packages or use GITHUB_TOKEN from Github Actions
|
|
36
|
+
2. Example: `containerify --registry https://ghcr.io/v2/ --token "$GITHUB_TOKEN" --fromImage docker-mirror/node:alpine --toImage <some image name>:<some tag> --folder . `
|
|
37
|
+
|
|
38
|
+
### Using with AWS ECR
|
|
39
|
+
|
|
40
|
+
1. Create the repository in AWS from the console or through using the CLI
|
|
41
|
+
2. Create token using `aws ecr get-authorization-token --output text --query 'authorizationData[].authorizationToken'`
|
|
42
|
+
3. Example: `containerify --toToken "Basic $TOKEN" --toRegistry https://<AWS ACCOUNT ID>.dkr.ecr.<AWS region for repository>.amazonaws.com/v2/ --fromImage node:alpine --toImage <name of repository>:<some tag> --folder .`
|
|
43
|
+
|
|
44
|
+
### Using with GitLab Container Registry (registry.gitlab.com)
|
|
45
|
+
|
|
46
|
+
1. Create the repository in GitLab
|
|
47
|
+
2. Login using your username and password, [CI-credentials](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html), or [obtain a token from GitLab](https://docs.gitlab.com/ee/api/container_registry.html#obtain-token-from-gitlab)
|
|
48
|
+
3. Example using CI-credentials `containerify --toToken "Basic $(echo -n '${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD}' | base64)" --to registry.gitlab.com/<Gitlab organisation>/<repository>:<tag>`
|
|
49
|
+
|
|
33
50
|
### Command line options
|
|
34
51
|
|
|
35
52
|
```
|
|
36
53
|
Usage: containerify [options]
|
|
37
54
|
|
|
38
55
|
Options:
|
|
56
|
+
--from <registry/image:tag> Optional: Shorthand to specify fromRegistry and fromImage in one argument
|
|
57
|
+
--to <registry/image:tag> Optional: Shorthand to specify toRegistry and toImage in one argument
|
|
39
58
|
--fromImage <name:tag> Required: Image name of base image - [path/]image:tag
|
|
40
59
|
--toImage <name:tag> Required: Image name of target image - [path/]image:tag
|
|
41
60
|
--folder <full path> Required: Base folder of node application (contains package.json)
|
|
42
61
|
--file <path> Optional: Name of configuration file (defaults to containerify.json if found on path)
|
|
62
|
+
--doCrossMount Optional: Cross mount image layers from the base image (only works if fromImage and toImage are in the same registry) (default: false)
|
|
43
63
|
--fromRegistry <registry url> Optional: URL of registry to pull base image from - Default: https://registry-1.docker.io/v2/
|
|
44
64
|
--fromToken <token> Optional: Authentication token for from registry
|
|
45
65
|
--toRegistry <registry url> Optional: URL of registry to push base image to - Default: https://registry-1.docker.io/v2/
|
|
46
|
-
--optimisticToRegistryCheck
|
|
66
|
+
--optimisticToRegistryCheck Treat redirects as layer existing in remote registry. Potentially unsafe, but can save bandwidth.
|
|
47
67
|
--toToken <token> Optional: Authentication token for target registry
|
|
48
68
|
--toTar <path> Optional: Export to tar file
|
|
69
|
+
--toDocker Optional: Export to local docker registry
|
|
49
70
|
--registry <path> Optional: Convenience argument for setting both from and to registry
|
|
50
71
|
--platform <platform> Optional: Preferred platform, e.g. linux/amd64 or arm64
|
|
51
72
|
--token <path> Optional: Convenience argument for setting token for both from and to registry
|
|
52
|
-
--user <user> Optional: User account to run process in container - default: 1000
|
|
53
|
-
--workdir <directory> Optional: Workdir where node app will be added and run from - default: /app
|
|
54
|
-
--entrypoint <entrypoint> Optional: Entrypoint when starting container - default: npm start
|
|
73
|
+
--user <user> Optional: User account to run process in container - default: 1000 (empty for customContent)
|
|
74
|
+
--workdir <directory> Optional: Workdir where node app will be added and run from - default: /app (empty for customContent)
|
|
75
|
+
--entrypoint <entrypoint> Optional: Entrypoint when starting container - default: npm start (empty for customContent)
|
|
55
76
|
--labels <labels> Optional: Comma-separated list of key value pairs to use as labels
|
|
56
77
|
--label <label> Optional: Single label (name=value). This option can be used multiple times.
|
|
57
78
|
--envs <envs> Optional: Comma-separated list of key value pairs to use av environment variables.
|
|
58
79
|
--env <env> Optional: Single environment variable (name=value). This option can be used multiple times.
|
|
59
|
-
--setTimeStamp <timestamp> Optional: Set a specific ISO 8601 timestamp on all entries (e.g. git commit hash). Default: 1970 in tar files, and current time on
|
|
60
|
-
manifest/config
|
|
80
|
+
--setTimeStamp <timestamp> Optional: Set a specific ISO 8601 timestamp on all entries (e.g. git commit hash). Default: 1970 in tar files, and current time on manifest/config
|
|
61
81
|
--verbose Verbose logging
|
|
62
82
|
--allowInsecureRegistries Allow insecure registries (with self-signed/untrusted cert)
|
|
63
|
-
--customContent <dirs/files> Optional: Skip normal node_modules and applayer and include specified root folder files/directories instead
|
|
83
|
+
--customContent <dirs/files> Optional: Skip normal node_modules and applayer and include specified root folder files/directories instead. You can specify as
|
|
84
|
+
local-path:absolute-container-path if you want to place it in a specific location
|
|
64
85
|
--extraContent <dirs/files> Optional: Add specific content. Specify as local-path:absolute-container-path,local-path2:absolute-container-path2 etc
|
|
65
86
|
--layerOwner <gid:uid> Optional: Set specific gid and uid on files in the added layers
|
|
66
87
|
--buildFolder <path> Optional: Use a specific build folder when creating the image
|
package/lib/cli.js
CHANGED
|
@@ -21,14 +21,18 @@ const appLayerCreator_1 = require("./appLayerCreator");
|
|
|
21
21
|
const dockerExporter_1 = require("./dockerExporter");
|
|
22
22
|
const tarExporter_1 = require("./tarExporter");
|
|
23
23
|
const logger_1 = require("./logger");
|
|
24
|
+
const types_1 = require("./types");
|
|
24
25
|
const utils_1 = require("./utils");
|
|
25
26
|
const fileutil_1 = require("./fileutil");
|
|
26
27
|
const version_1 = require("./version");
|
|
27
28
|
const possibleArgs = {
|
|
29
|
+
"--from <registry/image:tag>": "Optional: Shorthand to specify fromRegistry and fromImage in one argument",
|
|
30
|
+
"--to <registry/image:tag>": "Optional: Shorthand to specify toRegistry and toImage in one argument",
|
|
28
31
|
"--fromImage <name:tag>": "Required: Image name of base image - [path/]image:tag",
|
|
29
32
|
"--toImage <name:tag>": "Required: Image name of target image - [path/]image:tag",
|
|
30
33
|
"--folder <full path>": "Required: Base folder of node application (contains package.json)",
|
|
31
34
|
"--file <path>": "Optional: Name of configuration file (defaults to containerify.json if found on path)",
|
|
35
|
+
"--doCrossMount": "Optional: Cross mount image layers from the base image (only works if fromImage and toImage are in the same registry) (default: false)",
|
|
32
36
|
"--fromRegistry <registry url>": "Optional: URL of registry to pull base image from - Default: https://registry-1.docker.io/v2/",
|
|
33
37
|
"--fromToken <token>": "Optional: Authentication token for from registry",
|
|
34
38
|
"--toRegistry <registry url>": "Optional: URL of registry to push base image to - Default: https://registry-1.docker.io/v2/",
|
|
@@ -85,6 +89,7 @@ const defaultOptions = {
|
|
|
85
89
|
workdir: "/app",
|
|
86
90
|
user: "1000",
|
|
87
91
|
entrypoint: "npm start",
|
|
92
|
+
doCrossMount: false,
|
|
88
93
|
};
|
|
89
94
|
if (cliOptions.file && !fs.existsSync(cliOptions.file)) {
|
|
90
95
|
logger_1.default.error(`Config file '${cliOptions.file}' not found`);
|
|
@@ -146,12 +151,25 @@ function exitWithErrorIf(check, error) {
|
|
|
146
151
|
}
|
|
147
152
|
if (options.verbose)
|
|
148
153
|
logger_1.default.enableDebug();
|
|
149
|
-
if (options.allowInsecureRegistries)
|
|
150
|
-
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
151
154
|
exitWithErrorIf(!!options.registry && !!options.fromRegistry, "Do not set both --registry and --fromRegistry");
|
|
155
|
+
exitWithErrorIf(!!options.from && !!options.fromRegistry, "Do not set both --from and --fromRegistry");
|
|
156
|
+
exitWithErrorIf(!!options.registry && !!options.from, "Do not set both --registry and --from");
|
|
152
157
|
exitWithErrorIf(!!options.registry && !!options.toRegistry, "Do not set both --registry and --toRegistry");
|
|
158
|
+
exitWithErrorIf(!!options.to && !!options.toRegistry, "Do not set both --toRegistry and --to");
|
|
159
|
+
exitWithErrorIf(!!options.registry && !!options.to, "Do not set both --registry and --to");
|
|
160
|
+
if (options.from) {
|
|
161
|
+
const { registry, image } = (0, registry_1.parseFullImageUrl)(options.from);
|
|
162
|
+
options.fromRegistry = registry;
|
|
163
|
+
options.fromImage = image;
|
|
164
|
+
}
|
|
165
|
+
if (options.to) {
|
|
166
|
+
const { registry, image } = (0, registry_1.parseFullImageUrl)(options.to);
|
|
167
|
+
options.toRegistry = registry;
|
|
168
|
+
options.toImage = image;
|
|
169
|
+
}
|
|
153
170
|
exitWithErrorIf(!!options.token && !!options.fromToken, "Do not set both --token and --fromToken");
|
|
154
171
|
exitWithErrorIf(!!options.token && !!options.toToken, "Do not set both --token and --toToken");
|
|
172
|
+
exitWithErrorIf(!!options.doCrossMount && options.toRegistry != options.fromRegistry, `Cross mounting only works if fromRegistry and toRegistry are the same (${options.fromRegistry} != ${options.toRegistry})`);
|
|
155
173
|
if (options.setTimeStamp) {
|
|
156
174
|
try {
|
|
157
175
|
options.setTimeStamp = new Date(options.setTimeStamp).toISOString();
|
|
@@ -207,7 +225,7 @@ Object.keys(options.extraContent).forEach((k) => {
|
|
|
207
225
|
exitWithErrorIf(!fs.existsSync(options.folder + k), "Could not find `" + k + "` in the folder " + options.folder);
|
|
208
226
|
});
|
|
209
227
|
function run(options) {
|
|
210
|
-
var _a
|
|
228
|
+
var _a;
|
|
211
229
|
return __awaiter(this, void 0, void 0, function* () {
|
|
212
230
|
if (!(yield fse.pathExists(options.folder)))
|
|
213
231
|
throw new Error("No such folder: " + options.folder);
|
|
@@ -215,10 +233,10 @@ function run(options) {
|
|
|
215
233
|
logger_1.default.debug("Using " + tmpdir);
|
|
216
234
|
const fromdir = yield (0, fileutil_1.ensureEmptyDir)(path.join(tmpdir, "from"));
|
|
217
235
|
const todir = yield (0, fileutil_1.ensureEmptyDir)(path.join(tmpdir, "to"));
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
yield fromRegistry.download(options.fromImage, fromdir, (0, utils_1.getPreferredPlatform)(options.platform), options.layerCacheFolder);
|
|
236
|
+
const allowInsecure = options.allowInsecureRegistries ? types_1.InsecureRegistrySupport.YES : types_1.InsecureRegistrySupport.NO;
|
|
237
|
+
const fromRegistryUrl = (_a = options.fromRegistry) !== null && _a !== void 0 ? _a : registry_1.DEFAULT_DOCKER_REGISTRY;
|
|
238
|
+
const fromRegistry = yield (0, registry_1.createRegistry)(fromRegistryUrl, options.fromImage, allowInsecure, options.fromToken);
|
|
239
|
+
const originalManifest = yield fromRegistry.download(options.fromImage, fromdir, (0, utils_1.getPreferredPlatform)(options.platform), options.layerCacheFolder);
|
|
222
240
|
yield appLayerCreator_1.default.addLayers(tmpdir, fromdir, todir, options);
|
|
223
241
|
if (options.toDocker) {
|
|
224
242
|
if (!(yield dockerExporter_1.default.isAvailable())) {
|
|
@@ -232,8 +250,11 @@ function run(options) {
|
|
|
232
250
|
yield tarExporter_1.default.saveToTar(todir, tmpdir, options.toTar, [options.toImage], options);
|
|
233
251
|
}
|
|
234
252
|
if (options.toRegistry) {
|
|
235
|
-
|
|
236
|
-
|
|
253
|
+
if (!options.token && allowInsecure == types_1.InsecureRegistrySupport.NO) {
|
|
254
|
+
throw new Error("Need auth token to upload to " + options.toRegistry);
|
|
255
|
+
}
|
|
256
|
+
const toRegistry = yield (0, registry_1.createRegistry)(options.toRegistry, options.toImage, allowInsecure, options.toToken, options.optimisticToRegistryCheck);
|
|
257
|
+
yield toRegistry.upload(options.toImage, todir, options.doCrossMount, originalManifest, options.fromImage);
|
|
237
258
|
}
|
|
238
259
|
logger_1.default.debug("Deleting " + tmpdir + " ...");
|
|
239
260
|
yield fse.remove(tmpdir);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
import * as https from "https";
|
|
5
|
+
import * as http from "http";
|
|
6
|
+
import { InsecureRegistrySupport } from "./types";
|
|
7
|
+
import { OutgoingHttpHeaders } from "http";
|
|
8
|
+
export declare const redirectCodes: number[];
|
|
9
|
+
export declare function isOk(httpStatus: number): boolean;
|
|
10
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "HEAD";
|
|
11
|
+
export declare function createHttpOptions(method: HttpMethod, url: string, headers: OutgoingHttpHeaders): https.RequestOptions;
|
|
12
|
+
export declare function buildHeaders(accept: string, auth: string): OutgoingHttpHeaders;
|
|
13
|
+
export declare function request(options: https.RequestOptions, allowInsecure: InsecureRegistrySupport, callback: (res: http.IncomingMessage) => void): http.ClientRequest;
|
|
14
|
+
export declare function toError(res: http.IncomingMessage): string;
|
|
15
|
+
export declare function waitForResponseEnd(res: http.IncomingMessage, cb: (data: Buffer) => void): void;
|
|
16
|
+
export declare function dlJson<T>(uri: string, headers: OutgoingHttpHeaders, allowInsecure: InsecureRegistrySupport): Promise<T>;
|
|
17
|
+
type Callback = (result: {
|
|
18
|
+
error: string;
|
|
19
|
+
} | {
|
|
20
|
+
res: http.IncomingMessage;
|
|
21
|
+
}) => void;
|
|
22
|
+
export declare function followRedirects(uri: string, headers: OutgoingHttpHeaders, allowInsecure: InsecureRegistrySupport, cb: Callback, count?: number): void;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.followRedirects = exports.dlJson = exports.waitForResponseEnd = exports.toError = exports.request = exports.buildHeaders = exports.createHttpOptions = exports.isOk = exports.redirectCodes = void 0;
|
|
13
|
+
const https = require("https");
|
|
14
|
+
const http = require("http");
|
|
15
|
+
const URL = require("url");
|
|
16
|
+
const logger_1 = require("./logger");
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
exports.redirectCodes = [308, 307, 303, 302, 301];
|
|
19
|
+
function isOk(httpStatus) {
|
|
20
|
+
return httpStatus >= 200 && httpStatus < 300;
|
|
21
|
+
}
|
|
22
|
+
exports.isOk = isOk;
|
|
23
|
+
function createHttpOptions(method, url, headers) {
|
|
24
|
+
const options = Object.assign({}, URL.parse(url));
|
|
25
|
+
options.headers = headers;
|
|
26
|
+
options.method = method;
|
|
27
|
+
return options;
|
|
28
|
+
}
|
|
29
|
+
exports.createHttpOptions = createHttpOptions;
|
|
30
|
+
function buildHeaders(accept, auth) {
|
|
31
|
+
const headers = { accept: accept };
|
|
32
|
+
if (auth)
|
|
33
|
+
headers.authorization = auth;
|
|
34
|
+
return headers;
|
|
35
|
+
}
|
|
36
|
+
exports.buildHeaders = buildHeaders;
|
|
37
|
+
function request(options, allowInsecure, callback) {
|
|
38
|
+
if (allowInsecure == types_1.InsecureRegistrySupport.YES)
|
|
39
|
+
options.rejectUnauthorized = false;
|
|
40
|
+
const req = (options.protocol == "https:" ? https : http).request(options, (res) => {
|
|
41
|
+
callback(res);
|
|
42
|
+
});
|
|
43
|
+
req.on("error", (e) => {
|
|
44
|
+
logger_1.default.error("ERROR: " + e, options.method, options.path);
|
|
45
|
+
throw e;
|
|
46
|
+
});
|
|
47
|
+
return req;
|
|
48
|
+
}
|
|
49
|
+
exports.request = request;
|
|
50
|
+
function toError(res) {
|
|
51
|
+
return `Unexpected HTTP status ${res.statusCode} : ${res.statusMessage}`;
|
|
52
|
+
}
|
|
53
|
+
exports.toError = toError;
|
|
54
|
+
function waitForResponseEnd(res, cb) {
|
|
55
|
+
const data = [];
|
|
56
|
+
res.on("data", (d) => data.push(d));
|
|
57
|
+
res.on("end", () => cb(Buffer.concat(data)));
|
|
58
|
+
}
|
|
59
|
+
exports.waitForResponseEnd = waitForResponseEnd;
|
|
60
|
+
function dl(uri, headers, allowInsecure) {
|
|
61
|
+
logger_1.default.debug("dl", uri);
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
followRedirects(uri, headers, allowInsecure, (result) => {
|
|
64
|
+
var _a;
|
|
65
|
+
if ("error" in result)
|
|
66
|
+
return reject(result.error);
|
|
67
|
+
const { res } = result;
|
|
68
|
+
logger_1.default.debug(res.statusCode, res.statusMessage, res.headers["content-type"], res.headers["content-length"]);
|
|
69
|
+
if (!isOk((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0))
|
|
70
|
+
return reject(toError(res));
|
|
71
|
+
waitForResponseEnd(res, (data) => resolve(data.toString()));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function dlJson(uri, headers, allowInsecure) {
|
|
76
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
77
|
+
const data = yield dl(uri, headers, allowInsecure);
|
|
78
|
+
return JSON.parse(Buffer.from(data).toString("utf-8"));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
exports.dlJson = dlJson;
|
|
82
|
+
function followRedirects(uri, headers, allowInsecure, cb, count = 0) {
|
|
83
|
+
logger_1.default.debug("rc", uri);
|
|
84
|
+
const options = createHttpOptions("GET", uri, headers);
|
|
85
|
+
request(options, allowInsecure, (res) => {
|
|
86
|
+
var _a;
|
|
87
|
+
if (exports.redirectCodes.includes((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0)) {
|
|
88
|
+
if (count > 10)
|
|
89
|
+
return cb({ error: "Too many redirects for " + uri });
|
|
90
|
+
const location = res.headers.location;
|
|
91
|
+
if (!location)
|
|
92
|
+
return cb({ error: "Redirect, but missing location header" });
|
|
93
|
+
return followRedirects(location, headers, allowInsecure, cb, count + 1);
|
|
94
|
+
}
|
|
95
|
+
cb({ res });
|
|
96
|
+
}).end();
|
|
97
|
+
}
|
|
98
|
+
exports.followRedirects = followRedirects;
|
package/lib/registry.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function createRegistry(registryBaseUrl: string,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
download: (imageStr: string, folder: string, platform: Platform, cacheFolder?: string) => Promise<void>;
|
|
8
|
-
upload: (imageStr: string, folder: string) => Promise<void>;
|
|
1
|
+
import { InsecureRegistrySupport, Registry } from "./types";
|
|
2
|
+
export declare function createRegistry(registryBaseUrl: string, imagePath: string, allowInsecure: InsecureRegistrySupport, auth?: string, optimisticToRegistryCheck?: boolean): Promise<Registry>;
|
|
3
|
+
export declare const DEFAULT_DOCKER_REGISTRY = "https://registry-1.docker.io/v2/";
|
|
4
|
+
export declare function parseFullImageUrl(imageStr: string): {
|
|
5
|
+
registry: string;
|
|
6
|
+
image: string;
|
|
9
7
|
};
|
package/lib/registry.js
CHANGED
|
@@ -9,9 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
13
|
-
const https = require("https");
|
|
14
|
-
const http = require("http");
|
|
12
|
+
exports.parseFullImageUrl = exports.DEFAULT_DOCKER_REGISTRY = exports.createRegistry = void 0;
|
|
15
13
|
const URL = require("url");
|
|
16
14
|
const fss = require("fs");
|
|
17
15
|
const fs_1 = require("fs");
|
|
@@ -21,54 +19,8 @@ const fileutil = require("./fileutil");
|
|
|
21
19
|
const logger_1 = require("./logger");
|
|
22
20
|
const MIMETypes_1 = require("./MIMETypes");
|
|
23
21
|
const utils_1 = require("./utils");
|
|
24
|
-
const
|
|
25
|
-
function
|
|
26
|
-
return (options.protocol == "https:" ? https : http).request(options, (res) => {
|
|
27
|
-
callback(res);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
function isOk(httpStatus) {
|
|
31
|
-
return httpStatus >= 200 && httpStatus < 300;
|
|
32
|
-
}
|
|
33
|
-
function getHash(digest) {
|
|
34
|
-
return digest.split(":")[1];
|
|
35
|
-
}
|
|
36
|
-
function parseImage(imageStr) {
|
|
37
|
-
const ar = imageStr.split(":");
|
|
38
|
-
const tag = ar[1] || "latest";
|
|
39
|
-
const ipath = ar[0];
|
|
40
|
-
return { path: ipath, tag: tag };
|
|
41
|
-
}
|
|
42
|
-
function toError(res) {
|
|
43
|
-
return `Unexpected HTTP status ${res.statusCode} : ${res.statusMessage}`;
|
|
44
|
-
}
|
|
45
|
-
function dl(uri, headers) {
|
|
46
|
-
logger_1.default.debug("dl", uri);
|
|
47
|
-
return new Promise((resolve, reject) => {
|
|
48
|
-
followRedirects(uri, headers, (result) => {
|
|
49
|
-
var _a;
|
|
50
|
-
if ("error" in result)
|
|
51
|
-
return reject(result.error);
|
|
52
|
-
const { res } = result;
|
|
53
|
-
logger_1.default.debug(res.statusCode, res.statusMessage, res.headers["content-type"], res.headers["content-length"]);
|
|
54
|
-
if (!isOk((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0))
|
|
55
|
-
return reject(toError(res));
|
|
56
|
-
const data = [];
|
|
57
|
-
res
|
|
58
|
-
.on("data", (chunk) => data.push(chunk.toString()))
|
|
59
|
-
.on("end", () => {
|
|
60
|
-
resolve(data.reduce((a, b) => a.concat(b)));
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
function dlJson(uri, headers) {
|
|
66
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
67
|
-
const data = yield dl(uri, headers);
|
|
68
|
-
return JSON.parse(Buffer.from(data).toString("utf-8"));
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
function dlToFile(uri, file, headers, cacheFolder, skipCache = false) {
|
|
22
|
+
const httpRequest_1 = require("./httpRequest");
|
|
23
|
+
function dlToFile(uri, file, headers, allowInsecure, cacheFolder, skipCache = false) {
|
|
72
24
|
return new Promise((resolve, reject) => {
|
|
73
25
|
const [filename] = file.split("/").slice(-1);
|
|
74
26
|
if (cacheFolder && !skipCache) {
|
|
@@ -76,7 +28,7 @@ function dlToFile(uri, file, headers, cacheFolder, skipCache = false) {
|
|
|
76
28
|
.createReadStream(cacheFolder + filename)
|
|
77
29
|
.on("error", () => {
|
|
78
30
|
logger_1.default.debug("Not found in layer cache " + cacheFolder + filename + " - Downloading...");
|
|
79
|
-
dlToFile(uri, file, headers, cacheFolder, true).then(() => resolve());
|
|
31
|
+
dlToFile(uri, file, headers, allowInsecure, cacheFolder, true).then(() => resolve());
|
|
80
32
|
})
|
|
81
33
|
.pipe(fss.createWriteStream(file))
|
|
82
34
|
.on("finish", () => {
|
|
@@ -85,14 +37,14 @@ function dlToFile(uri, file, headers, cacheFolder, skipCache = false) {
|
|
|
85
37
|
});
|
|
86
38
|
return;
|
|
87
39
|
}
|
|
88
|
-
followRedirects(uri, headers, (result) => {
|
|
40
|
+
(0, httpRequest_1.followRedirects)(uri, headers, allowInsecure, (result) => {
|
|
89
41
|
var _a;
|
|
90
42
|
if ("error" in result)
|
|
91
43
|
return reject(result.error);
|
|
92
44
|
const { res } = result;
|
|
93
45
|
logger_1.default.debug(res.statusCode, res.statusMessage, res.headers["content-type"], res.headers["content-length"]);
|
|
94
|
-
if (!isOk((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0))
|
|
95
|
-
return reject(toError(res));
|
|
46
|
+
if (!(0, httpRequest_1.isOk)((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0))
|
|
47
|
+
return reject((0, httpRequest_1.toError)(res));
|
|
96
48
|
res.pipe(fss.createWriteStream(file)).on("finish", () => {
|
|
97
49
|
logger_1.default.debug("Done " + file + " - " + res.headers["content-length"] + " bytes ");
|
|
98
50
|
if (cacheFolder) {
|
|
@@ -104,268 +56,289 @@ function dlToFile(uri, file, headers, cacheFolder, skipCache = false) {
|
|
|
104
56
|
});
|
|
105
57
|
});
|
|
106
58
|
}
|
|
107
|
-
function
|
|
108
|
-
logger_1.default.debug("rc", uri);
|
|
109
|
-
const options = Object.assign({}, URL.parse(uri));
|
|
110
|
-
options.headers = headers;
|
|
111
|
-
options.method = "GET";
|
|
112
|
-
request(options, (res) => {
|
|
113
|
-
var _a;
|
|
114
|
-
if (redirectCodes.includes((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0)) {
|
|
115
|
-
if (count > 10)
|
|
116
|
-
return cb({ error: "Too many redirects for " + uri });
|
|
117
|
-
const location = res.headers.location;
|
|
118
|
-
if (!location)
|
|
119
|
-
return cb({ error: "Redirect, but missing location header" });
|
|
120
|
-
return followRedirects(location, headers, cb, count + 1);
|
|
121
|
-
}
|
|
122
|
-
cb({ res });
|
|
123
|
-
}).end();
|
|
124
|
-
}
|
|
125
|
-
function buildHeaders(accept, auth) {
|
|
126
|
-
const headers = { accept: accept };
|
|
127
|
-
if (auth)
|
|
128
|
-
headers.authorization = auth;
|
|
129
|
-
return headers;
|
|
130
|
-
}
|
|
131
|
-
function headOk(url, headers, optimisticCheck = false, depth = 0) {
|
|
59
|
+
function checkIfLayerExists(url, headers, allowInsecure, optimisticCheck = false, depth = 0) {
|
|
132
60
|
if (depth >= 5) {
|
|
133
61
|
logger_1.default.info("Followed five redirects, assuming layer does not exist");
|
|
134
62
|
return new Promise((resolve) => resolve(false));
|
|
135
63
|
}
|
|
136
64
|
return new Promise((resolve, reject) => {
|
|
137
65
|
logger_1.default.debug(`HEAD ${url}`);
|
|
138
|
-
const options =
|
|
139
|
-
options
|
|
140
|
-
options.method = "HEAD";
|
|
141
|
-
request(options, (res) => {
|
|
142
|
-
var _a;
|
|
66
|
+
const options = (0, httpRequest_1.createHttpOptions)("HEAD", url, headers);
|
|
67
|
+
(0, httpRequest_1.request)(options, allowInsecure, (res) => {
|
|
143
68
|
logger_1.default.debug(`HEAD ${url}`, res.statusCode);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (redirectCodes.includes((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0) && res.headers.location) {
|
|
152
|
-
if (optimisticCheck)
|
|
69
|
+
(0, httpRequest_1.waitForResponseEnd)(res, (data) => {
|
|
70
|
+
var _a;
|
|
71
|
+
// Not found
|
|
72
|
+
if (res.statusCode == 404)
|
|
73
|
+
return resolve(false);
|
|
74
|
+
// OK
|
|
75
|
+
if (res.statusCode == 200)
|
|
153
76
|
return resolve(true);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
77
|
+
// Redirected
|
|
78
|
+
if (httpRequest_1.redirectCodes.includes((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0) && res.headers.location) {
|
|
79
|
+
if (optimisticCheck)
|
|
80
|
+
return resolve(true);
|
|
81
|
+
return resolve(checkIfLayerExists(res.headers.location, headers, allowInsecure, optimisticCheck, ++depth));
|
|
82
|
+
}
|
|
83
|
+
// Unauthorized
|
|
84
|
+
// Possibly related to https://gitlab.com/gitlab-org/gitlab/-/issues/23132
|
|
85
|
+
if (res.statusCode == 401) {
|
|
86
|
+
return resolve(false);
|
|
87
|
+
}
|
|
88
|
+
// Other error
|
|
89
|
+
logger_1.default.error(`HEAD ${url}`, res.statusCode, res.statusMessage, data.toString());
|
|
90
|
+
reject((0, httpRequest_1.toError)(res));
|
|
91
|
+
});
|
|
162
92
|
}).end();
|
|
163
93
|
});
|
|
164
94
|
}
|
|
165
|
-
function uploadContent(uploadUrl, file, fileConfig, auth) {
|
|
95
|
+
function uploadContent(uploadUrl, file, fileConfig, allowInsecure, auth, contentType = "application/octet-stream") {
|
|
166
96
|
return new Promise((resolve, reject) => {
|
|
167
97
|
logger_1.default.debug("Uploading: ", file);
|
|
168
98
|
let url = uploadUrl;
|
|
169
99
|
if (fileConfig.digest)
|
|
170
100
|
url += (url.indexOf("?") == -1 ? "?" : "&") + "digest=" + fileConfig.digest;
|
|
171
|
-
const options =
|
|
172
|
-
options.method = "PUT";
|
|
173
|
-
options.headers = {
|
|
101
|
+
const options = (0, httpRequest_1.createHttpOptions)("PUT", url, {
|
|
174
102
|
authorization: auth,
|
|
175
103
|
"content-length": fileConfig.size,
|
|
176
|
-
"content-type":
|
|
177
|
-
};
|
|
178
|
-
logger_1.default.debug(
|
|
179
|
-
const req = request(options, (res) => {
|
|
104
|
+
"content-type": contentType,
|
|
105
|
+
});
|
|
106
|
+
logger_1.default.debug(options.method, url);
|
|
107
|
+
const req = (0, httpRequest_1.request)(options, allowInsecure, (res) => {
|
|
180
108
|
var _a;
|
|
181
109
|
logger_1.default.debug(res.statusCode, res.statusMessage, res.headers["content-type"], res.headers["content-length"]);
|
|
182
110
|
if ([200, 201, 202, 203].includes((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0)) {
|
|
183
111
|
resolve();
|
|
184
112
|
}
|
|
185
113
|
else {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
res.on("end", () => {
|
|
189
|
-
reject(`Error uploading to ${uploadUrl}. Got ${res.statusCode} ${res.statusMessage}:\n${data.join("")}`);
|
|
114
|
+
(0, httpRequest_1.waitForResponseEnd)(res, (data) => {
|
|
115
|
+
reject(`Error uploading to ${uploadUrl}. Got ${res.statusCode} ${res.statusMessage}:\n${data.toString()}`);
|
|
190
116
|
});
|
|
191
117
|
}
|
|
192
118
|
});
|
|
193
|
-
fss
|
|
119
|
+
fss
|
|
120
|
+
.createReadStream(file)
|
|
121
|
+
.pipe(req)
|
|
122
|
+
.on("error", (e) => {
|
|
123
|
+
reject("Error reading file for upload: " + e);
|
|
124
|
+
});
|
|
194
125
|
});
|
|
195
126
|
}
|
|
196
|
-
function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
yield
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const data = [];
|
|
228
|
-
res
|
|
229
|
-
.on("data", (c) => data.push(c.toString()))
|
|
230
|
-
.on("end", () => {
|
|
231
|
-
reject(`Error getting upload URL from ${url}. Got ${res.statusCode} ${res.statusMessage}:\n${data.join("")}`);
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}).end();
|
|
127
|
+
function processToken(registryBaseUrl, allowInsecure, imagePath, token) {
|
|
128
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
const { hostname } = URL.parse(registryBaseUrl);
|
|
130
|
+
const image = (0, utils_1.parseImage)(imagePath);
|
|
131
|
+
if ((hostname === null || hostname === void 0 ? void 0 : hostname.endsWith(".docker.io")) && !token) {
|
|
132
|
+
const resp = yield (0, httpRequest_1.dlJson)(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image.path}:pull`, {}, allowInsecure);
|
|
133
|
+
return `Bearer ${resp.token}`;
|
|
134
|
+
}
|
|
135
|
+
if ((hostname === null || hostname === void 0 ? void 0 : hostname.endsWith(".gitlab.com")) && (token === null || token === void 0 ? void 0 : token.startsWith("Basic"))) {
|
|
136
|
+
if (token === null || token === void 0 ? void 0 : token.includes(":")) {
|
|
137
|
+
token = "Basic " + Buffer.from(token === null || token === void 0 ? void 0 : token.replace("Basic ", "")).toString("base64");
|
|
138
|
+
}
|
|
139
|
+
const resp = yield (0, httpRequest_1.dlJson)(`https://gitlab.com/jwt/auth?service=container_registry&scope=repository:${image.path}:pull,push`, { Authorization: token }, allowInsecure);
|
|
140
|
+
return `Bearer ${resp.token}`;
|
|
141
|
+
}
|
|
142
|
+
if (!token)
|
|
143
|
+
return ""; //We allow to pull from tokenless registries
|
|
144
|
+
if (token.startsWith("Basic "))
|
|
145
|
+
return token;
|
|
146
|
+
if (token.startsWith("ghp_"))
|
|
147
|
+
return "Bearer " + Buffer.from(token).toString("base64");
|
|
148
|
+
return "Bearer " + token;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function createRegistry(registryBaseUrl, imagePath, allowInsecure, auth, optimisticToRegistryCheck = false) {
|
|
152
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
153
|
+
const token = yield processToken(registryBaseUrl, allowInsecure, imagePath, auth);
|
|
154
|
+
function exists(image, layer) {
|
|
155
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
const url = `${registryBaseUrl}${image.path}/blobs/${layer.digest}`;
|
|
157
|
+
return yield checkIfLayerExists(url, (0, httpRequest_1.buildHeaders)(layer.mediaType, token), allowInsecure, optimisticToRegistryCheck, 0);
|
|
235
158
|
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return
|
|
159
|
+
}
|
|
160
|
+
function uploadLayerContent(uploadUrl, layer, dir) {
|
|
161
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
logger_1.default.info(layer.digest);
|
|
163
|
+
const file = path.join(dir, (0, utils_1.getHash)(layer.digest) + (0, utils_1.getLayerTypeFileEnding)(layer));
|
|
164
|
+
yield uploadContent(uploadUrl, file, layer, allowInsecure, token);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function getUploadUrl(image, mountParameters = undefined) {
|
|
168
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const parameters = new URLSearchParams(mountParameters);
|
|
171
|
+
const url = `${registryBaseUrl}${image.path}/blobs/uploads/${parameters.size > 0 ? "?" + parameters : ""}`;
|
|
172
|
+
const options = URL.parse(url);
|
|
173
|
+
options.method = "POST";
|
|
174
|
+
options.headers = { authorization: auth };
|
|
175
|
+
(0, httpRequest_1.request)(options, allowInsecure, (res) => {
|
|
176
|
+
logger_1.default.debug("POST", `${url}`, res.statusCode);
|
|
177
|
+
if (res.statusCode == 202) {
|
|
178
|
+
const { location } = res.headers;
|
|
179
|
+
if (location) {
|
|
180
|
+
if (location.startsWith("http")) {
|
|
181
|
+
resolve({ uploadUrl: location });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const regURL = URL.parse(registryBaseUrl);
|
|
185
|
+
resolve({
|
|
186
|
+
uploadUrl: `${regURL.protocol}//${regURL.hostname}${regURL.port ? ":" + regURL.port : ""}${location}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
reject("Missing location for 202");
|
|
191
|
+
}
|
|
192
|
+
else if (mountParameters && res.statusCode == 201) {
|
|
193
|
+
const returnedDigest = res.headers["docker-content-digest"];
|
|
194
|
+
if (returnedDigest && returnedDigest != mountParameters.mount) {
|
|
195
|
+
reject(`ERROR: Layer mounted with wrong digest: Expected ${mountParameters.mount} but got ${returnedDigest}`);
|
|
196
|
+
}
|
|
197
|
+
resolve({ mountSuccess: true });
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
(0, httpRequest_1.waitForResponseEnd)(res, (data) => {
|
|
201
|
+
reject(`Error getting upload URL from ${url}. Got ${res.statusCode} ${res.statusMessage}:\n${data.toString()}`);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}).end();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function dlManifest(image, preferredPlatform, allowInsecure) {
|
|
209
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
210
|
+
// Accept both manifests and index/manifest lists
|
|
211
|
+
const res = yield (0, httpRequest_1.dlJson)(`${registryBaseUrl}${image.path}/manifests/${image.tag}`, (0, httpRequest_1.buildHeaders)(`${MIMETypes_1.OCI.index}, ${MIMETypes_1.OCI.manifest}, ${MIMETypes_1.DockerV2.index}, ${MIMETypes_1.DockerV2.manifest}`, token), allowInsecure);
|
|
212
|
+
// We've received an OCI Index or Docker Manifest List and need to find which manifest we want
|
|
213
|
+
if (res.mediaType === MIMETypes_1.OCI.index || res.mediaType === MIMETypes_1.DockerV2.index) {
|
|
214
|
+
const availableManifests = res.manifests;
|
|
215
|
+
const adequateManifest = pickManifest(availableManifests, preferredPlatform);
|
|
216
|
+
return dlManifest(Object.assign(Object.assign({}, image), { tag: adequateManifest.digest }), preferredPlatform, allowInsecure);
|
|
217
|
+
}
|
|
218
|
+
return res;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function pickManifest(manifests, preferredPlatform) {
|
|
222
|
+
const matchingArchitectures = new Set();
|
|
223
|
+
const matchingOSes = new Set();
|
|
224
|
+
// Find sets of matching architecture and os
|
|
225
|
+
for (const manifest of manifests) {
|
|
226
|
+
if (manifest.platform.architecture === preferredPlatform.architecture) {
|
|
227
|
+
matchingArchitectures.add(manifest);
|
|
228
|
+
}
|
|
229
|
+
if (manifest.platform.os === preferredPlatform.os) {
|
|
230
|
+
matchingOSes.add(manifest);
|
|
231
|
+
}
|
|
247
232
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const matchingArchitectures = new Set();
|
|
253
|
-
const matchingOSes = new Set();
|
|
254
|
-
// Find sets of matching architecture and os
|
|
255
|
-
for (const manifest of manifests) {
|
|
256
|
-
if (manifest.platform.architecture === preferredPlatform.architecture) {
|
|
257
|
-
matchingArchitectures.add(manifest);
|
|
233
|
+
// If the intersection of matching architectures and OS is one we've found our optimal match
|
|
234
|
+
const intersection = new Set([...matchingArchitectures].filter((x) => matchingOSes.has(x)));
|
|
235
|
+
if (intersection.size == 1) {
|
|
236
|
+
return intersection.values().next().value;
|
|
258
237
|
}
|
|
259
|
-
|
|
260
|
-
|
|
238
|
+
// If we don't have a perfect match we give a warning and try the first matching architecture
|
|
239
|
+
if (matchingArchitectures.size >= 1) {
|
|
240
|
+
const matchingArch = matchingArchitectures.values().next().value;
|
|
241
|
+
logger_1.default.info(`[WARN] Preferred OS '${preferredPlatform.os}' not available.`);
|
|
242
|
+
logger_1.default.info("[WARN] Using closest available manifest:", JSON.stringify(matchingArch.platform));
|
|
243
|
+
return matchingArch;
|
|
261
244
|
}
|
|
245
|
+
// If there's no image matching the wanted architecture we bail
|
|
246
|
+
logger_1.default.error(`No image matching requested architecture: '${preferredPlatform.architecture}'`);
|
|
247
|
+
logger_1.default.error("Available platforms:", JSON.stringify(manifests.map((m) => m.platform)));
|
|
248
|
+
throw new Error("No image matching requested architecture");
|
|
262
249
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
250
|
+
function dlConfig(image, config, allowInsecure) {
|
|
251
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
252
|
+
return yield (0, httpRequest_1.dlJson)(`${registryBaseUrl}${image.path}/blobs/${config.digest}`, (0, httpRequest_1.buildHeaders)("*/*", token), allowInsecure);
|
|
253
|
+
});
|
|
267
254
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
255
|
+
function dlLayer(image, layer, folder, allowInsecure, cacheFolder) {
|
|
256
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
257
|
+
const file = (0, utils_1.getHash)(layer.digest) + (0, utils_1.getLayerTypeFileEnding)(layer);
|
|
258
|
+
yield dlToFile(`${registryBaseUrl}${image.path}/blobs/${layer.digest}`, path.join(folder, file), (0, httpRequest_1.buildHeaders)(layer.mediaType, token), allowInsecure, cacheFolder);
|
|
259
|
+
return file;
|
|
260
|
+
});
|
|
274
261
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
logger_1.default.info(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
};
|
|
262
|
+
function upload(imageStr, folder, doCrossMount, originalManifest, originalRepository) {
|
|
263
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
264
|
+
const image = (0, utils_1.parseImage)(imageStr);
|
|
265
|
+
const manifestFile = path.join(folder, "manifest.json");
|
|
266
|
+
const manifest = (yield fse.readJson(manifestFile));
|
|
267
|
+
logger_1.default.info("Checking layer status...");
|
|
268
|
+
const layerStatus = yield Promise.all(manifest.layers.map((l) => __awaiter(this, void 0, void 0, function* () {
|
|
269
|
+
return { layer: l, exists: yield exists(image, l) };
|
|
270
|
+
})));
|
|
271
|
+
const layersForUpload = layerStatus.filter((l) => !l.exists);
|
|
272
|
+
logger_1.default.debug("Needs upload:", layersForUpload.map((l) => l.layer.digest));
|
|
273
|
+
logger_1.default.info("Uploading layers...");
|
|
274
|
+
yield Promise.all(layersForUpload.map((l) => __awaiter(this, void 0, void 0, function* () {
|
|
275
|
+
if (doCrossMount && originalManifest.layers.find((x) => x.digest == l.layer.digest)) {
|
|
276
|
+
const mount = yield getUploadUrl(image, { mount: l.layer.digest, from: originalRepository });
|
|
277
|
+
if ("mountSuccess" in mount) {
|
|
278
|
+
logger_1.default.info(`Cross mounted layer ${l.layer.digest} from '${originalRepository}'`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
yield uploadLayerContent(mount.uploadUrl, l.layer, folder);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const url = yield getUploadUrl(image);
|
|
285
|
+
if ("mountSuccess" in url)
|
|
286
|
+
throw new Error("Mounting not supported for this upload");
|
|
287
|
+
yield uploadLayerContent(url.uploadUrl, l.layer, folder);
|
|
288
|
+
}
|
|
289
|
+
})));
|
|
290
|
+
logger_1.default.info("Uploading config...");
|
|
291
|
+
const configUploadUrl = yield getUploadUrl(image);
|
|
292
|
+
if ("mountSuccess" in configUploadUrl)
|
|
293
|
+
throw new Error("Mounting not supported for config upload");
|
|
294
|
+
const configFile = path.join(folder, (0, utils_1.getHash)(manifest.config.digest) + ".json");
|
|
295
|
+
yield uploadContent(configUploadUrl.uploadUrl, configFile, manifest.config, allowInsecure, token);
|
|
296
|
+
logger_1.default.info("Uploading manifest...");
|
|
297
|
+
const manifestSize = yield fileutil.sizeOf(manifestFile);
|
|
298
|
+
yield uploadContent(`${registryBaseUrl}${image.path}/manifests/${image.tag}`, manifestFile, { mediaType: manifest.mediaType, size: manifestSize }, allowInsecure, token, manifest.mediaType);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function download(imageStr, folder, preferredPlatform, cacheFolder) {
|
|
302
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
303
|
+
const image = (0, utils_1.parseImage)(imageStr);
|
|
304
|
+
logger_1.default.info("Downloading manifest...");
|
|
305
|
+
const manifest = yield dlManifest(image, preferredPlatform, allowInsecure);
|
|
306
|
+
yield fs_1.promises.writeFile(path.join(folder, "manifest.json"), JSON.stringify(manifest));
|
|
307
|
+
logger_1.default.info("Downloading config...");
|
|
308
|
+
const config = yield dlConfig(image, manifest.config, allowInsecure);
|
|
309
|
+
if (config.architecture != preferredPlatform.architecture) {
|
|
310
|
+
logger_1.default.info(`[WARN] Image architecture (${config.architecture}) does not match preferred architecture (${preferredPlatform.architecture}).`);
|
|
311
|
+
}
|
|
312
|
+
if (config.os != preferredPlatform.os) {
|
|
313
|
+
logger_1.default.info(`[WARN] Image OS (${config.os}) does not match preferred OS (${preferredPlatform.os}).`);
|
|
314
|
+
}
|
|
315
|
+
yield fs_1.promises.writeFile(path.join(folder, "config.json"), JSON.stringify(config));
|
|
316
|
+
logger_1.default.info("Downloading layers...");
|
|
317
|
+
yield Promise.all(manifest.layers.map((layer) => dlLayer(image, layer, folder, allowInsecure, cacheFolder)));
|
|
318
|
+
logger_1.default.info("Image downloaded.");
|
|
319
|
+
return manifest;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
download: download,
|
|
324
|
+
upload: upload,
|
|
325
|
+
registryBaseUrl,
|
|
326
|
+
};
|
|
327
|
+
});
|
|
341
328
|
}
|
|
342
329
|
exports.createRegistry = createRegistry;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
function download(imageStr, folder, platform, cacheFolder) {
|
|
352
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
353
|
-
const image = parseImage(imageStr);
|
|
354
|
-
if (!auth)
|
|
355
|
-
auth = yield getToken(image);
|
|
356
|
-
yield createRegistry(registryBaseUrl, auth).download(imageStr, folder, platform, cacheFolder);
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
function upload(imageStr, folder) {
|
|
360
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
361
|
-
if (!auth)
|
|
362
|
-
throw new Error("Need auth token to upload to Docker");
|
|
363
|
-
yield createRegistry(registryBaseUrl, auth).upload(imageStr, folder);
|
|
364
|
-
});
|
|
330
|
+
exports.DEFAULT_DOCKER_REGISTRY = "https://registry-1.docker.io/v2/";
|
|
331
|
+
function parseFullImageUrl(imageStr) {
|
|
332
|
+
const [registry, ...rest] = imageStr.split("/");
|
|
333
|
+
if (registry == "docker.io") {
|
|
334
|
+
return {
|
|
335
|
+
registry: exports.DEFAULT_DOCKER_REGISTRY,
|
|
336
|
+
image: rest.join("/"),
|
|
337
|
+
};
|
|
365
338
|
}
|
|
366
339
|
return {
|
|
367
|
-
|
|
368
|
-
|
|
340
|
+
registry: `https://${registry}/v2/`,
|
|
341
|
+
image: rest.join("/"),
|
|
369
342
|
};
|
|
370
343
|
}
|
|
371
|
-
exports.
|
|
344
|
+
exports.parseFullImageUrl = parseFullImageUrl;
|
package/lib/types.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export type HistoryLine = {
|
|
|
54
54
|
comment?: string;
|
|
55
55
|
};
|
|
56
56
|
export type Options = {
|
|
57
|
+
from?: string;
|
|
58
|
+
to?: string;
|
|
57
59
|
fromImage: string;
|
|
58
60
|
toImage: string;
|
|
59
61
|
folder: string;
|
|
@@ -61,6 +63,7 @@ export type Options = {
|
|
|
61
63
|
fromRegistry?: string;
|
|
62
64
|
fromToken?: string;
|
|
63
65
|
toRegistry?: string;
|
|
66
|
+
doCrossMount: boolean;
|
|
64
67
|
optimisticToRegistryCheck?: boolean;
|
|
65
68
|
toToken?: string;
|
|
66
69
|
toTar?: string;
|
|
@@ -87,4 +90,13 @@ export type Options = {
|
|
|
87
90
|
entrypoint?: string;
|
|
88
91
|
};
|
|
89
92
|
};
|
|
93
|
+
export declare enum InsecureRegistrySupport {
|
|
94
|
+
NO = 0,
|
|
95
|
+
YES = 1
|
|
96
|
+
}
|
|
97
|
+
export type Registry = {
|
|
98
|
+
download: (imageStr: string, folder: string, preferredPlatform: Platform, cacheFolder?: string) => Promise<Manifest>;
|
|
99
|
+
upload: (imageStr: string, folder: string, doCrossMount: boolean, originalManifest: Manifest, originalRepository: string) => Promise<void>;
|
|
100
|
+
registryBaseUrl: string;
|
|
101
|
+
};
|
|
90
102
|
export {};
|
package/lib/types.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InsecureRegistrySupport = void 0;
|
|
4
|
+
var InsecureRegistrySupport;
|
|
5
|
+
(function (InsecureRegistrySupport) {
|
|
6
|
+
InsecureRegistrySupport[InsecureRegistrySupport["NO"] = 0] = "NO";
|
|
7
|
+
InsecureRegistrySupport[InsecureRegistrySupport["YES"] = 1] = "YES";
|
|
8
|
+
})(InsecureRegistrySupport || (exports.InsecureRegistrySupport = InsecureRegistrySupport = {}));
|
package/lib/utils.d.ts
CHANGED
|
@@ -4,3 +4,8 @@ export declare function omit<T>(obj: Record<string, T>, keys: string[]): Record<
|
|
|
4
4
|
export declare function getPreferredPlatform(platform?: string): Platform;
|
|
5
5
|
export declare function getManifestLayerType(manifest: Manifest): string;
|
|
6
6
|
export declare function getLayerTypeFileEnding(layer: Layer): ".tar.gz" | ".tar";
|
|
7
|
+
export declare function getHash(digest: string): string;
|
|
8
|
+
export declare function parseImage(imageStr: string): {
|
|
9
|
+
path: string;
|
|
10
|
+
tag: string;
|
|
11
|
+
};
|
package/lib/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getLayerTypeFileEnding = exports.getManifestLayerType = exports.getPreferredPlatform = exports.omit = exports.unique = void 0;
|
|
3
|
+
exports.parseImage = exports.getHash = exports.getLayerTypeFileEnding = exports.getManifestLayerType = exports.getPreferredPlatform = exports.omit = exports.unique = void 0;
|
|
4
4
|
const MIMETypes_1 = require("./MIMETypes");
|
|
5
5
|
function unique(vals) {
|
|
6
6
|
return [...new Set(vals)];
|
|
@@ -92,3 +92,14 @@ function getLayerTypeFileEnding(layer) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
exports.getLayerTypeFileEnding = getLayerTypeFileEnding;
|
|
95
|
+
function getHash(digest) {
|
|
96
|
+
return digest.split(":")[1];
|
|
97
|
+
}
|
|
98
|
+
exports.getHash = getHash;
|
|
99
|
+
function parseImage(imageStr) {
|
|
100
|
+
const ar = imageStr.split(":");
|
|
101
|
+
const tag = ar[1] || "latest";
|
|
102
|
+
const ipath = ar[0];
|
|
103
|
+
return { path: ipath, tag: tag };
|
|
104
|
+
}
|
|
105
|
+
exports.parseImage = parseImage;
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "
|
|
1
|
+
export declare const VERSION = "3.0.1";
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "containerify",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Build node.js docker images without docker",
|
|
5
5
|
"main": "./lib/cli.js",
|
|
6
6
|
"scripts": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"build": "tsc && chmod ugo+x lib/cli.js",
|
|
9
9
|
"lint": "eslint . --ext .ts --fix --ignore-path .gitignore",
|
|
10
10
|
"typecheck": "tsc --noEmit",
|
|
11
|
+
"watch": "tsc --watch",
|
|
11
12
|
"check": "npm run lint && npm run typecheck",
|
|
12
13
|
"dev": "tsc --watch",
|
|
13
14
|
"integrationTest": "cd tests/integration/ && ./test.sh",
|