containerify 2.6.0 → 3.0.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/README.md CHANGED
@@ -21,15 +21,31 @@ containerify --fromImage node:13-slim --folder src/ --toImage myapp:latest --toR
21
21
  ### customContent - Adding compiled code to non-node container
22
22
 
23
23
  If you want to build a non-node container (e.g. add compiled frontend code to an nginx container), you can use `--customContent`. When doing this
24
- the normal `node_modules` etc layers will not be added, and workdir, user and entrypoint will not be overridden (allthough they can be explicitely modified
25
- if needed).
24
+ the normal `node_modules` etc layers will not be added. By default it does _NOT_ modify then entrypoint, user or workdir, so the base image settings are still used when running. You can still override with `--entrypoint` etc. if needed.
26
25
 
27
26
  ```
28
- npm run build
29
- containerify --fromImage nginx:alpine --folder . --toImage frontend:latest --customContent dist:/var/www/html --toRegistry https://registry.example.com/v2/
27
+ npm run build # or some other build command
28
+ containerify --fromImage nginx:alpine --folder . --toImage frontend:latest --customContent dist:/usr/share/nginx/html --toRegistry https://registry.example.com/v2/
30
29
  ```
31
30
 
32
- This will take nginx:alpine and copy the files in `./dist/` into `/var/www/html`.
31
+ This will take the `nginx:alpine` image, and copy the files from `./dist/` into `/usr/share/nginx/html`.
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>`
33
49
 
34
50
  ### Command line options
35
51
 
@@ -37,30 +53,35 @@ This will take nginx:alpine and copy the files in `./dist/` into `/var/www/html`
37
53
  Usage: containerify [options]
38
54
 
39
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
40
58
  --fromImage <name:tag> Required: Image name of base image - [path/]image:tag
41
59
  --toImage <name:tag> Required: Image name of target image - [path/]image:tag
42
60
  --folder <full path> Required: Base folder of node application (contains package.json)
43
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)
44
63
  --fromRegistry <registry url> Optional: URL of registry to pull base image from - Default: https://registry-1.docker.io/v2/
45
64
  --fromToken <token> Optional: Authentication token for from registry
46
65
  --toRegistry <registry url> Optional: URL of registry to push base image to - Default: https://registry-1.docker.io/v2/
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,17 +21,22 @@ 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/",
39
+ "--optimisticToRegistryCheck": "Treat redirects as layer existing in remote registry. Potentially unsafe, but can save bandwidth.",
35
40
  "--toToken <token>": "Optional: Authentication token for target registry",
36
41
  "--toTar <path>": "Optional: Export to tar file",
37
42
  "--toDocker": "Optional: Export to local docker registry",
@@ -84,6 +89,7 @@ const defaultOptions = {
84
89
  workdir: "/app",
85
90
  user: "1000",
86
91
  entrypoint: "npm start",
92
+ doCrossMount: false,
87
93
  };
88
94
  if (cliOptions.file && !fs.existsSync(cliOptions.file)) {
89
95
  logger_1.default.error(`Config file '${cliOptions.file}' not found`);
@@ -145,12 +151,25 @@ function exitWithErrorIf(check, error) {
145
151
  }
146
152
  if (options.verbose)
147
153
  logger_1.default.enableDebug();
148
- if (options.allowInsecureRegistries)
149
- process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
150
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");
151
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
+ }
152
170
  exitWithErrorIf(!!options.token && !!options.fromToken, "Do not set both --token and --fromToken");
153
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})`);
154
173
  if (options.setTimeStamp) {
155
174
  try {
156
175
  options.setTimeStamp = new Date(options.setTimeStamp).toISOString();
@@ -206,7 +225,7 @@ Object.keys(options.extraContent).forEach((k) => {
206
225
  exitWithErrorIf(!fs.existsSync(options.folder + k), "Could not find `" + k + "` in the folder " + options.folder);
207
226
  });
208
227
  function run(options) {
209
- var _a, _b;
228
+ var _a;
210
229
  return __awaiter(this, void 0, void 0, function* () {
211
230
  if (!(yield fse.pathExists(options.folder)))
212
231
  throw new Error("No such folder: " + options.folder);
@@ -214,10 +233,10 @@ function run(options) {
214
233
  logger_1.default.debug("Using " + tmpdir);
215
234
  const fromdir = yield (0, fileutil_1.ensureEmptyDir)(path.join(tmpdir, "from"));
216
235
  const todir = yield (0, fileutil_1.ensureEmptyDir)(path.join(tmpdir, "to"));
217
- const fromRegistry = options.fromRegistry
218
- ? (0, registry_1.createRegistry)(options.fromRegistry, (_a = options.fromToken) !== null && _a !== void 0 ? _a : "")
219
- : (0, registry_1.createDockerRegistry)(options.fromToken);
220
- 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 = (0, registry_1.createRegistry)(fromRegistryUrl, yield (0, registry_1.processToken)(fromRegistryUrl, allowInsecure, options.fromImage, options.fromToken), allowInsecure);
239
+ const originalManifest = yield fromRegistry.download(options.fromImage, fromdir, (0, utils_1.getPreferredPlatform)(options.platform), options.layerCacheFolder);
221
240
  yield appLayerCreator_1.default.addLayers(tmpdir, fromdir, todir, options);
222
241
  if (options.toDocker) {
223
242
  if (!(yield dockerExporter_1.default.isAvailable())) {
@@ -231,8 +250,8 @@ function run(options) {
231
250
  yield tarExporter_1.default.saveToTar(todir, tmpdir, options.toTar, [options.toImage], options);
232
251
  }
233
252
  if (options.toRegistry) {
234
- const toRegistry = (0, registry_1.createRegistry)(options.toRegistry, (_b = options.toToken) !== null && _b !== void 0 ? _b : "");
235
- yield toRegistry.upload(options.toImage, todir);
253
+ const toRegistry = (0, registry_1.createRegistry)(options.toRegistry, yield (0, registry_1.processToken)(options.toRegistry, allowInsecure, options.toImage, options.toToken), allowInsecure, options.optimisticToRegistryCheck);
254
+ yield toRegistry.upload(options.toImage, todir, options.doCrossMount, originalManifest, options.fromImage);
236
255
  }
237
256
  logger_1.default.debug("Deleting " + tmpdir + " ...");
238
257
  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,8 @@
1
- import { Platform } from "./types";
2
- export declare function createRegistry(registryBaseUrl: string, token: string): {
3
- download: (imageStr: string, folder: string, preferredPlatform: Platform, cacheFolder?: string) => Promise<void>;
4
- upload: (imageStr: string, folder: string) => Promise<void>;
5
- };
6
- export declare function createDockerRegistry(auth?: string): {
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 processToken(registryBaseUrl: string, allowInsecure: InsecureRegistrySupport, imagePath: string, token?: string): Promise<string>;
3
+ export declare function createRegistry(registryBaseUrl: string, auth: string, allowInsecure: InsecureRegistrySupport, optimisticToRegistryCheck?: boolean): Registry;
4
+ export declare const DEFAULT_DOCKER_REGISTRY = "https://registry-1.docker.io/v2/";
5
+ export declare function parseFullImageUrl(imageStr: string): {
6
+ registry: string;
7
+ image: string;
9
8
  };
package/lib/registry.js CHANGED
@@ -9,66 +9,18 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.createDockerRegistry = exports.createRegistry = void 0;
13
- const https = require("https");
14
- const http = require("http");
12
+ exports.parseFullImageUrl = exports.DEFAULT_DOCKER_REGISTRY = exports.createRegistry = exports.processToken = void 0;
15
13
  const URL = require("url");
14
+ const fss = require("fs");
16
15
  const fs_1 = require("fs");
17
16
  const path = require("path");
18
17
  const fse = require("fs-extra");
19
- const fss = require("fs");
20
18
  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 redirectCodes = [307, 303, 302];
25
- function request(options, callback) {
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,128 +56,163 @@ function dlToFile(uri, file, headers, cacheFolder, skipCache = false) {
104
56
  });
105
57
  });
106
58
  }
107
- function followRedirects(uri, headers, cb, count = 0) {
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) {
59
+ function checkIfLayerExists(url, headers, allowInsecure, optimisticCheck = false, depth = 0) {
60
+ if (depth >= 5) {
61
+ logger_1.default.info("Followed five redirects, assuming layer does not exist");
62
+ return new Promise((resolve) => resolve(false));
63
+ }
132
64
  return new Promise((resolve, reject) => {
133
65
  logger_1.default.debug(`HEAD ${url}`);
134
- const options = URL.parse(url);
135
- options.headers = headers;
136
- options.method = "HEAD";
137
- request(options, (res) => {
66
+ const options = (0, httpRequest_1.createHttpOptions)("HEAD", url, headers);
67
+ (0, httpRequest_1.request)(options, allowInsecure, (res) => {
138
68
  logger_1.default.debug(`HEAD ${url}`, res.statusCode);
139
- if (res.statusCode == 404)
140
- return resolve(false);
141
- if (res.statusCode == 200)
142
- return resolve(true);
143
- reject(toError(res));
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)
76
+ return resolve(true);
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
+ });
144
92
  }).end();
145
93
  });
146
94
  }
147
- function uploadContent(uploadUrl, file, fileConfig, auth) {
95
+ function uploadContent(uploadUrl, file, fileConfig, allowInsecure, auth, contentType = "application/octet-stream") {
148
96
  return new Promise((resolve, reject) => {
149
97
  logger_1.default.debug("Uploading: ", file);
150
98
  let url = uploadUrl;
151
99
  if (fileConfig.digest)
152
100
  url += (url.indexOf("?") == -1 ? "?" : "&") + "digest=" + fileConfig.digest;
153
- const options = URL.parse(url);
154
- options.method = "PUT";
155
- options.headers = {
101
+ const options = (0, httpRequest_1.createHttpOptions)("PUT", url, {
156
102
  authorization: auth,
157
103
  "content-length": fileConfig.size,
158
- "content-type": fileConfig.mediaType,
159
- };
160
- logger_1.default.debug("POST", url);
161
- 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) => {
162
108
  var _a;
163
109
  logger_1.default.debug(res.statusCode, res.statusMessage, res.headers["content-type"], res.headers["content-length"]);
164
110
  if ([200, 201, 202, 203].includes((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0)) {
165
111
  resolve();
166
112
  }
167
113
  else {
168
- const data = [];
169
- res.on("data", (d) => data.push(d.toString()));
170
- res.on("end", () => {
171
- 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()}`);
172
116
  });
173
117
  }
174
118
  });
175
- fss.createReadStream(file).pipe(req);
119
+ fss
120
+ .createReadStream(file)
121
+ .pipe(req)
122
+ .on("error", (e) => {
123
+ reject("Error reading file for upload: " + e);
124
+ });
176
125
  });
177
126
  }
178
- function createRegistry(registryBaseUrl, token) {
179
- const auth = token.startsWith("Basic ") ? token : "Bearer " + token;
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
+ throw new Error("Needs auth token to upload to " + registryBaseUrl);
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
+ exports.processToken = processToken;
152
+ function createRegistry(registryBaseUrl, auth, allowInsecure, optimisticToRegistryCheck = false) {
180
153
  function exists(image, layer) {
181
154
  return __awaiter(this, void 0, void 0, function* () {
182
155
  const url = `${registryBaseUrl}${image.path}/blobs/${layer.digest}`;
183
- return yield headOk(url, buildHeaders(layer.mediaType, auth));
156
+ return yield checkIfLayerExists(url, (0, httpRequest_1.buildHeaders)(layer.mediaType, auth), allowInsecure, optimisticToRegistryCheck, 0);
184
157
  });
185
158
  }
186
159
  function uploadLayerContent(uploadUrl, layer, dir) {
187
160
  return __awaiter(this, void 0, void 0, function* () {
188
161
  logger_1.default.info(layer.digest);
189
- const file = path.join(dir, getHash(layer.digest) + (0, utils_1.getLayerTypeFileEnding)(layer));
190
- yield uploadContent(uploadUrl, file, layer, auth);
162
+ const file = path.join(dir, (0, utils_1.getHash)(layer.digest) + (0, utils_1.getLayerTypeFileEnding)(layer));
163
+ yield uploadContent(uploadUrl, file, layer, allowInsecure, auth);
191
164
  });
192
165
  }
193
- function getUploadUrl(image) {
166
+ function getUploadUrl(image, mountParameters = undefined) {
194
167
  return __awaiter(this, void 0, void 0, function* () {
195
168
  return new Promise((resolve, reject) => {
196
- const url = `${registryBaseUrl}${image.path}/blobs/uploads/`;
169
+ const parameters = new URLSearchParams(mountParameters);
170
+ const url = `${registryBaseUrl}${image.path}/blobs/uploads/${parameters.size > 0 ? "?" + parameters : ""}`;
197
171
  const options = URL.parse(url);
198
172
  options.method = "POST";
199
173
  options.headers = { authorization: auth };
200
- request(options, (res) => {
174
+ (0, httpRequest_1.request)(options, allowInsecure, (res) => {
201
175
  logger_1.default.debug("POST", `${url}`, res.statusCode);
202
176
  if (res.statusCode == 202) {
203
177
  const { location } = res.headers;
204
- if (location)
205
- resolve(location);
178
+ if (location) {
179
+ if (location.startsWith("http")) {
180
+ resolve({ uploadUrl: location });
181
+ }
182
+ else {
183
+ const regURL = URL.parse(registryBaseUrl);
184
+ resolve({
185
+ uploadUrl: `${regURL.protocol}//${regURL.hostname}${regURL.port ? ":" + regURL.port : ""}${location}`,
186
+ });
187
+ }
188
+ }
206
189
  reject("Missing location for 202");
207
190
  }
191
+ else if (mountParameters && res.statusCode == 201) {
192
+ const returnedDigest = res.headers["docker-content-digest"];
193
+ if (returnedDigest && returnedDigest != mountParameters.mount) {
194
+ reject(`ERROR: Layer mounted with wrong digest: Expected ${mountParameters.mount} but got ${returnedDigest}`);
195
+ }
196
+ resolve({ mountSuccess: true });
197
+ }
208
198
  else {
209
- const data = [];
210
- res
211
- .on("data", (c) => data.push(c.toString()))
212
- .on("end", () => {
213
- reject(`Error getting upload URL from ${url}. Got ${res.statusCode} ${res.statusMessage}:\n${data.join("")}`);
199
+ (0, httpRequest_1.waitForResponseEnd)(res, (data) => {
200
+ reject(`Error getting upload URL from ${url}. Got ${res.statusCode} ${res.statusMessage}:\n${data.toString()}`);
214
201
  });
215
202
  }
216
203
  }).end();
217
204
  });
218
205
  });
219
206
  }
220
- function dlManifest(image, preferredPlatform) {
207
+ function dlManifest(image, preferredPlatform, allowInsecure) {
221
208
  return __awaiter(this, void 0, void 0, function* () {
222
209
  // Accept both manifests and index/manifest lists
223
- const res = yield dlJson(`${registryBaseUrl}${image.path}/manifests/${image.tag}`, buildHeaders(`${MIMETypes_1.OCI.index}, ${MIMETypes_1.OCI.manifest}, ${MIMETypes_1.DockerV2.index}, ${MIMETypes_1.DockerV2.manifest}`, auth));
210
+ 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}`, auth), allowInsecure);
224
211
  // We've received an OCI Index or Docker Manifest List and need to find which manifest we want
225
212
  if (res.mediaType === MIMETypes_1.OCI.index || res.mediaType === MIMETypes_1.DockerV2.index) {
226
213
  const availableManifests = res.manifests;
227
214
  const adequateManifest = pickManifest(availableManifests, preferredPlatform);
228
- return dlManifest(Object.assign(Object.assign({}, image), { tag: adequateManifest.digest }), preferredPlatform);
215
+ return dlManifest(Object.assign(Object.assign({}, image), { tag: adequateManifest.digest }), preferredPlatform, allowInsecure);
229
216
  }
230
217
  return res;
231
218
  });
@@ -259,21 +246,21 @@ function createRegistry(registryBaseUrl, token) {
259
246
  logger_1.default.error("Available platforms:", JSON.stringify(manifests.map((m) => m.platform)));
260
247
  throw new Error("No image matching requested architecture");
261
248
  }
262
- function dlConfig(image, config) {
249
+ function dlConfig(image, config, allowInsecure) {
263
250
  return __awaiter(this, void 0, void 0, function* () {
264
- return yield dlJson(`${registryBaseUrl}${image.path}/blobs/${config.digest}`, buildHeaders("*/*", auth));
251
+ return yield (0, httpRequest_1.dlJson)(`${registryBaseUrl}${image.path}/blobs/${config.digest}`, (0, httpRequest_1.buildHeaders)("*/*", auth), allowInsecure);
265
252
  });
266
253
  }
267
- function dlLayer(image, layer, folder, cacheFolder) {
254
+ function dlLayer(image, layer, folder, allowInsecure, cacheFolder) {
268
255
  return __awaiter(this, void 0, void 0, function* () {
269
- const file = getHash(layer.digest) + (0, utils_1.getLayerTypeFileEnding)(layer);
270
- yield dlToFile(`${registryBaseUrl}${image.path}/blobs/${layer.digest}`, path.join(folder, file), buildHeaders(layer.mediaType, auth), cacheFolder);
256
+ const file = (0, utils_1.getHash)(layer.digest) + (0, utils_1.getLayerTypeFileEnding)(layer);
257
+ yield dlToFile(`${registryBaseUrl}${image.path}/blobs/${layer.digest}`, path.join(folder, file), (0, httpRequest_1.buildHeaders)(layer.mediaType, auth), allowInsecure, cacheFolder);
271
258
  return file;
272
259
  });
273
260
  }
274
- function upload(imageStr, folder) {
261
+ function upload(imageStr, folder, doCrossMount, originalManifest, originalRepository) {
275
262
  return __awaiter(this, void 0, void 0, function* () {
276
- const image = parseImage(imageStr);
263
+ const image = (0, utils_1.parseImage)(imageStr);
277
264
  const manifestFile = path.join(folder, "manifest.json");
278
265
  const manifest = (yield fse.readJson(manifestFile));
279
266
  logger_1.default.info("Checking layer status...");
@@ -284,26 +271,40 @@ function createRegistry(registryBaseUrl, token) {
284
271
  logger_1.default.debug("Needs upload:", layersForUpload.map((l) => l.layer.digest));
285
272
  logger_1.default.info("Uploading layers...");
286
273
  yield Promise.all(layersForUpload.map((l) => __awaiter(this, void 0, void 0, function* () {
287
- const url = yield getUploadUrl(image);
288
- yield uploadLayerContent(url, l.layer, folder);
274
+ if (doCrossMount && originalManifest.layers.find((x) => x.digest == l.layer.digest)) {
275
+ const mount = yield getUploadUrl(image, { mount: l.layer.digest, from: originalRepository });
276
+ if ("mountSuccess" in mount) {
277
+ logger_1.default.info(`Cross mounted layer ${l.layer.digest} from '${originalRepository}'`);
278
+ return;
279
+ }
280
+ yield uploadLayerContent(mount.uploadUrl, l.layer, folder);
281
+ }
282
+ else {
283
+ const url = yield getUploadUrl(image);
284
+ if ("mountSuccess" in url)
285
+ throw new Error("Mounting not supported for this upload");
286
+ yield uploadLayerContent(url.uploadUrl, l.layer, folder);
287
+ }
289
288
  })));
290
289
  logger_1.default.info("Uploading config...");
291
290
  const configUploadUrl = yield getUploadUrl(image);
292
- const configFile = path.join(folder, getHash(manifest.config.digest) + ".json");
293
- yield uploadContent(configUploadUrl, configFile, manifest.config, auth);
291
+ if ("mountSuccess" in configUploadUrl)
292
+ throw new Error("Mounting not supported for config upload");
293
+ const configFile = path.join(folder, (0, utils_1.getHash)(manifest.config.digest) + ".json");
294
+ yield uploadContent(configUploadUrl.uploadUrl, configFile, manifest.config, allowInsecure, auth);
294
295
  logger_1.default.info("Uploading manifest...");
295
296
  const manifestSize = yield fileutil.sizeOf(manifestFile);
296
- yield uploadContent(`${registryBaseUrl}${image.path}/manifests/${image.tag}`, manifestFile, { mediaType: manifest.mediaType, size: manifestSize }, auth);
297
+ yield uploadContent(`${registryBaseUrl}${image.path}/manifests/${image.tag}`, manifestFile, { mediaType: manifest.mediaType, size: manifestSize }, allowInsecure, auth, manifest.mediaType);
297
298
  });
298
299
  }
299
300
  function download(imageStr, folder, preferredPlatform, cacheFolder) {
300
301
  return __awaiter(this, void 0, void 0, function* () {
301
- const image = parseImage(imageStr);
302
+ const image = (0, utils_1.parseImage)(imageStr);
302
303
  logger_1.default.info("Downloading manifest...");
303
- const manifest = yield dlManifest(image, preferredPlatform);
304
+ const manifest = yield dlManifest(image, preferredPlatform, allowInsecure);
304
305
  yield fs_1.promises.writeFile(path.join(folder, "manifest.json"), JSON.stringify(manifest));
305
306
  logger_1.default.info("Downloading config...");
306
- const config = yield dlConfig(image, manifest.config);
307
+ const config = yield dlConfig(image, manifest.config, allowInsecure);
307
308
  if (config.architecture != preferredPlatform.architecture) {
308
309
  logger_1.default.info(`[WARN] Image architecture (${config.architecture}) does not match preferred architecture (${preferredPlatform.architecture}).`);
309
310
  }
@@ -312,42 +313,30 @@ function createRegistry(registryBaseUrl, token) {
312
313
  }
313
314
  yield fs_1.promises.writeFile(path.join(folder, "config.json"), JSON.stringify(config));
314
315
  logger_1.default.info("Downloading layers...");
315
- yield Promise.all(manifest.layers.map((layer) => dlLayer(image, layer, folder, cacheFolder)));
316
+ yield Promise.all(manifest.layers.map((layer) => dlLayer(image, layer, folder, allowInsecure, cacheFolder)));
316
317
  logger_1.default.info("Image downloaded.");
318
+ return manifest;
317
319
  });
318
320
  }
319
321
  return {
320
322
  download: download,
321
323
  upload: upload,
324
+ registryBaseUrl,
322
325
  };
323
326
  }
324
327
  exports.createRegistry = createRegistry;
325
- function createDockerRegistry(auth) {
326
- const registryBaseUrl = "https://registry-1.docker.io/v2/";
327
- function getToken(image) {
328
- return __awaiter(this, void 0, void 0, function* () {
329
- const resp = yield dlJson(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image.path}:pull`, {});
330
- return resp.token;
331
- });
332
- }
333
- function download(imageStr, folder, platform, cacheFolder) {
334
- return __awaiter(this, void 0, void 0, function* () {
335
- const image = parseImage(imageStr);
336
- if (!auth)
337
- auth = yield getToken(image);
338
- yield createRegistry(registryBaseUrl, auth).download(imageStr, folder, platform, cacheFolder);
339
- });
340
- }
341
- function upload(imageStr, folder) {
342
- return __awaiter(this, void 0, void 0, function* () {
343
- if (!auth)
344
- throw new Error("Need auth token to upload to Docker");
345
- yield createRegistry(registryBaseUrl, auth).upload(imageStr, folder);
346
- });
328
+ exports.DEFAULT_DOCKER_REGISTRY = "https://registry-1.docker.io/v2/";
329
+ function parseFullImageUrl(imageStr) {
330
+ const [registry, ...rest] = imageStr.split("/");
331
+ if (registry == "docker.io") {
332
+ return {
333
+ registry: exports.DEFAULT_DOCKER_REGISTRY,
334
+ image: rest.join("/"),
335
+ };
347
336
  }
348
337
  return {
349
- download: download,
350
- upload: upload,
338
+ registry: `https://${registry}/v2/`,
339
+ image: rest.join("/"),
351
340
  };
352
341
  }
353
- exports.createDockerRegistry = createDockerRegistry;
342
+ 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,8 @@ export type Options = {
61
63
  fromRegistry?: string;
62
64
  fromToken?: string;
63
65
  toRegistry?: string;
66
+ doCrossMount: boolean;
67
+ optimisticToRegistryCheck?: boolean;
64
68
  toToken?: string;
65
69
  toTar?: string;
66
70
  toDocker?: boolean;
@@ -86,4 +90,13 @@ export type Options = {
86
90
  entrypoint?: string;
87
91
  };
88
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
+ };
89
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 = "2.6.0";
1
+ export declare const VERSION = "3.0.0";
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = "2.6.0";
4
+ exports.VERSION = "3.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "containerify",
3
- "version": "2.6.0",
3
+ "version": "3.0.0",
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",