containerify 2.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/LICENSE.md ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2018 Erlend Oftedal
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # containerify
2
+
3
+ containerify allows you to build node.js docker images without docker, allowing you to build a node.js docker image _from within a docker container_. This means you can build the container image on Kubernetes or Openshift - or locally in a docker container for more hermetic builds.
4
+
5
+ It will pull an image you specify from a given registry, add the node.js application from a given folder, and push the result to a(nother) given registry.
6
+
7
+ ## How to install
8
+
9
+ ```
10
+ npm install -g containerify
11
+ ```
12
+
13
+ ## How to use
14
+
15
+ This will pull the `node:13-slim` image from Docker hub, build the image by adding the application in `src/`, and push the result to the given registry, and set time timestamp of files in the created layers and configs to the current timestamp of the latest git commit.
16
+
17
+ ```
18
+ containerify --fromImage node:13-slim --folder src/ --toImage myapp:latest --toRegistry https://registry.example.com/v2/ --setTimeStamp=$(git show -s --format="%aI" HEAD)
19
+ ```
20
+
21
+ ### Command line options
22
+
23
+ ```
24
+ Usage: containerify [options]
25
+
26
+ Options:
27
+ --fromImage <name:tag> Required: Image name of base image - [path/]image:tag
28
+ --toImage <name:tag> Required: Image name of target image - [path/]image:tag
29
+ --folder <full path> Required: Base folder of node application (contains package.json)
30
+ --fromRegistry <registry url> Optional: URL of registry to pull base image from - Default: https://registry-1.docker.io/v2/
31
+ --fromToken <token> Optional: Authentication token for from registry
32
+ --toRegistry <registry url> Optional: URL of registry to push base image to - Default: https://registry-1.docker.io/v2/
33
+ --toToken <token> Optional: Authentication token for target registry
34
+ --toTar <path> Optional: Export to tar file
35
+ --registry <path> Optional: Convenience argument for setting both from and to registry
36
+ --platform <platform> Optional: Preferred platform, e.g. linux/amd64 or arm64
37
+ --token <path> Optional: Convenience argument for setting token for both from and to registry
38
+ --user <user> Optional: User account to run process in container - default: 1000
39
+ --workdir <directory> Optional: Workdir where node app will be added and run from - default: /app
40
+ --entrypoint <entrypoint> Optional: Entrypoint when starting container - default: npm start
41
+ --labels <labels> Optional: Comma-separated list of key value pairs to use as labels
42
+ --label <label> Optional: Single label (name=value). This option can be used multiple times. Wrap in double quotes if value has spaces or other characters that can cause arugment parsing issues.
43
+ --envs <envs> Optional: Comma-separated list of key value paris to use av environment variables.
44
+ --env <env> Optional: Single environment variable (name=value). This option can be used multiple times.
45
+ --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
46
+ --verbose Verbose logging
47
+ --allowInsecureRegistries Allow insecure registries (with self-signed/untrusted cert)
48
+ --customContent <dirs/files> Optional: Skip normal node_modules and applayer and include specified root folder files/directories instead
49
+ --extraContent <dirs/files> Optional: Add specific content. Specify as local-path:absolute-container-path,local-path2:absolute-container-path2 etc
50
+ --layerOwner <gid:uid> Optional: Set specific gid and uid on files in the added layers
51
+ --buildFolder <path> Optional: Use a specific build folder when creating the image
52
+ -h, --help output usage information
53
+ ```
54
+
55
+ ## Detailed info
56
+
57
+ Everything in the specified folder (`--folder`) is currently added to the image. It adds one layer with `package.json`, `package-lock.json` and `node_modules` and then a separate layer with the rest.
58
+
59
+ You may want to prune dev-dependencies and remove any unwanted files before running `containerify`.
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DockerV2 = exports.OCI = void 0;
4
+ exports.OCI = {
5
+ index: 'application/vnd.oci.image.index.v1+json',
6
+ manifest: 'application/vnd.oci.image.manifest.v1+json',
7
+ layer: {
8
+ tar: 'application/vnd.oci.image.layer.v1.tar',
9
+ gzip: 'application/vnd.oci.image.layer.v1.tar+gzip'
10
+ },
11
+ config: 'application/vnd.oci.image.config.v1+json'
12
+ };
13
+ exports.DockerV2 = {
14
+ index: 'application/vnd.docker.distribution.manifest.list.v2+json',
15
+ manifest: 'application/vnd.docker.distribution.manifest.v2+json',
16
+ layer: {
17
+ tar: 'application/vnd.docker.image.rootfs.diff.tar',
18
+ gzip: 'application/vnd.docker.image.rootfs.diff.tar.gzip',
19
+ },
20
+ config: 'application/vnd.docker.container.image.v1+json'
21
+ };
@@ -0,0 +1,239 @@
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
+ const tar = require("tar");
13
+ const fs_1 = require("fs");
14
+ const fse = require("fs-extra");
15
+ const fss = require("fs");
16
+ const path = require("path");
17
+ const crypto = require("crypto");
18
+ const minizlib_1 = require("minizlib");
19
+ const fileutil = require("./fileutil");
20
+ const logger_1 = require("./logger");
21
+ const utils_1 = require("./utils");
22
+ const version_1 = require("./version");
23
+ const depLayerPossibles = ["package.json", "package-lock.json", "node_modules"];
24
+ const ignore = [".git", ".gitignore", ".npmrc", ".DS_Store", "npm-debug.log", ".svn", ".hg", "CVS"];
25
+ function statCache(layerOwner) {
26
+ if (!layerOwner)
27
+ return null;
28
+ // We use the stat cache to overwrite uid and gid in image.
29
+ // A bit hacky
30
+ const statCacheMap = new Map();
31
+ const a = layerOwner.split(":");
32
+ const gid = parseInt(a[0]);
33
+ const uid = parseInt(a[1]);
34
+ return {
35
+ get: function (name) {
36
+ if (statCacheMap.has(name))
37
+ return statCacheMap.get(name);
38
+ const stat = fss.statSync(name);
39
+ stat.uid = uid;
40
+ stat.gid = gid;
41
+ stat.atime = new Date(0);
42
+ stat.mtime = new Date(0);
43
+ stat.ctime = new Date(0);
44
+ stat.birthtime = new Date(0);
45
+ stat.atimeMs = 0;
46
+ stat.mtimeMs = 0;
47
+ stat.ctimeMs = 0;
48
+ stat.birthtimeMs = 0;
49
+ statCacheMap.set(name, stat);
50
+ return stat;
51
+ },
52
+ set: function (name, stat) {
53
+ statCacheMap.set(name, stat);
54
+ },
55
+ has: function () {
56
+ return true;
57
+ },
58
+ };
59
+ }
60
+ const tarDefaultConfig = {
61
+ preservePaths: false,
62
+ follow: true,
63
+ };
64
+ function calculateHashOfBuffer(buf) {
65
+ const hash = crypto.createHash("sha256");
66
+ hash.update(buf);
67
+ return hash.digest("hex");
68
+ }
69
+ function calculateHash(path) {
70
+ return new Promise((resolve, reject) => {
71
+ const hash = crypto.createHash("sha256");
72
+ const stream = fss.createReadStream(path);
73
+ stream.on("error", (err) => reject(err));
74
+ stream.on("data", (chunk) => hash.update(chunk));
75
+ stream.on("end", () => resolve(hash.digest("hex")));
76
+ });
77
+ }
78
+ function copySync(src, dest) {
79
+ const copyOptions = { overwrite: true, dereference: true };
80
+ const destFolder = dest.substring(0, dest.lastIndexOf("/"));
81
+ logger_1.default.debug("Copying " + src + " to " + dest);
82
+ fse.ensureDirSync(destFolder);
83
+ fse.copySync(src, dest, copyOptions);
84
+ }
85
+ function addEmptyLayer(config, options, operation, action) {
86
+ logger_1.default.info(`Applying ${operation}`);
87
+ config.history.push({
88
+ created: options.setTimeStamp || new Date().toISOString(),
89
+ created_by: "/bin/sh -c #(nop) " + operation,
90
+ empty_layer: true,
91
+ });
92
+ action(config);
93
+ }
94
+ function getHashOfUncompressed(file) {
95
+ return __awaiter(this, void 0, void 0, function* () {
96
+ return new Promise((resolve, reject) => {
97
+ const hash = crypto.createHash("sha256");
98
+ const gunzip = new minizlib_1.Gunzip({});
99
+ gunzip.on("data", (chunk) => hash.update(chunk));
100
+ gunzip.on("end", () => resolve(hash.digest("hex")));
101
+ gunzip.on("error", (err) => reject(err));
102
+ fss
103
+ .createReadStream(file)
104
+ .pipe(gunzip)
105
+ .on("error", (err) => reject(err));
106
+ });
107
+ });
108
+ }
109
+ function addDataLayer(tmpdir, todir, options, config, manifest, files, comment) {
110
+ return __awaiter(this, void 0, void 0, function* () {
111
+ logger_1.default.info("Adding layer for " + comment + " ...");
112
+ const buildDir = yield fileutil.ensureEmptyDir(path.join(tmpdir, "build"));
113
+ files.map((f) => {
114
+ if (Array.isArray(f)) {
115
+ copySync(path.join(options.folder, f[0]), path.join(buildDir, f[1]));
116
+ }
117
+ else {
118
+ copySync(path.join(options.folder, f), path.join(buildDir, options.workdir, f));
119
+ }
120
+ });
121
+ const layerFile = path.join(todir, "layer.tar.gz");
122
+ if (options.layerOwner)
123
+ logger_1.default.info("Setting file ownership to: " + options.layerOwner);
124
+ const filesToTar = fss.readdirSync(buildDir);
125
+ if (filesToTar.length == 0) {
126
+ throw new Error("No files found for layer: " +
127
+ comment +
128
+ (comment == "dependencies" ? ". Did you forget to run npm install?" : ""));
129
+ }
130
+ yield tar.c(Object.assign({}, tarDefaultConfig, Object.assign({ statCache: statCache(options.layerOwner), portable: !options.layerOwner, prefix: "/", cwd: buildDir, file: layerFile, gzip: true, noMtime: !options.setTimeStamp }, (options.setTimeStamp ? { mtime: new Date(options.setTimeStamp) } : {}))), filesToTar);
131
+ const fhash = yield calculateHash(layerFile);
132
+ const finalName = path.join(todir, fhash + ".tar.gz");
133
+ yield fse.move(layerFile, finalName);
134
+ manifest.layers.push({
135
+ mediaType: (0, utils_1.getManifestLayerType)(manifest),
136
+ size: yield fileutil.sizeOf(finalName),
137
+ digest: "sha256:" + fhash,
138
+ });
139
+ const dhash = yield getHashOfUncompressed(finalName);
140
+ config.rootfs.diff_ids.push("sha256:" + dhash);
141
+ config.history.push({
142
+ created: options.setTimeStamp || new Date().toISOString(),
143
+ created_by: `containerify:${version_1.VERSION}`,
144
+ comment: comment,
145
+ });
146
+ });
147
+ }
148
+ function copyLayers(fromdir, todir, layers) {
149
+ return __awaiter(this, void 0, void 0, function* () {
150
+ yield Promise.all(layers.map((layer) => __awaiter(this, void 0, void 0, function* () {
151
+ const file = layer.digest.split(":")[1] + (0, utils_1.getLayerTypeFileEnding)(layer);
152
+ yield fse.copy(path.join(fromdir, file), path.join(todir, file));
153
+ })));
154
+ });
155
+ }
156
+ function parseCommandLineToParts(entrypoint) {
157
+ return entrypoint
158
+ .split('"')
159
+ .map((p, i) => {
160
+ if (i % 2 == 1)
161
+ return [p];
162
+ return p.split(" ");
163
+ })
164
+ .reduce((a, b) => a.concat(b), [])
165
+ .filter((a) => a != "");
166
+ }
167
+ function addAppLayers(options, config, todir, manifest, tmpdir) {
168
+ return __awaiter(this, void 0, void 0, function* () {
169
+ if (options.customContent) {
170
+ yield addEnvsLayer(options, config);
171
+ yield addLabelsLayer(options, config);
172
+ yield addDataLayer(tmpdir, todir, options, config, manifest, options.customContent, "custom");
173
+ }
174
+ else {
175
+ addEmptyLayer(config, options, `WORKDIR ${options.workdir}`, (config) => (config.config.WorkingDir = options.workdir));
176
+ const entrypoint = parseCommandLineToParts(options.entrypoint);
177
+ addEmptyLayer(config, options, `ENTRYPOINT ${JSON.stringify(entrypoint)}`, (config) => (config.config.Entrypoint = entrypoint));
178
+ addEmptyLayer(config, options, `USER ${options.user}`, (config) => {
179
+ config.config.User = options.user;
180
+ config.container_config.User = options.user;
181
+ });
182
+ yield addEnvsLayer(options, config);
183
+ yield addLabelsLayer(options, config);
184
+ const appFiles = (yield fs_1.promises.readdir(options.folder)).filter((l) => !ignore.includes(l));
185
+ const depLayerContent = appFiles.filter((l) => depLayerPossibles.includes(l));
186
+ const appLayerContent = appFiles.filter((l) => !depLayerPossibles.includes(l));
187
+ yield addDataLayer(tmpdir, todir, options, config, manifest, depLayerContent, "dependencies");
188
+ yield addDataLayer(tmpdir, todir, options, config, manifest, appLayerContent, "app");
189
+ }
190
+ if (options.extraContent) {
191
+ for (const i in options.extraContent) {
192
+ yield addDataLayer(tmpdir, todir, options, config, manifest, [options.extraContent[i]], "extra");
193
+ }
194
+ }
195
+ });
196
+ }
197
+ function addLabelsLayer(options, config) {
198
+ return __awaiter(this, void 0, void 0, function* () {
199
+ if (Object.keys(options.labels).length > 0) {
200
+ addEmptyLayer(config, options, `LABELS ${JSON.stringify(options.labels)}`, (config) => {
201
+ config.config.Labels = options.labels;
202
+ config.container_config.Labels = options.labels;
203
+ });
204
+ }
205
+ });
206
+ }
207
+ function addEnvsLayer(options, config) {
208
+ return __awaiter(this, void 0, void 0, function* () {
209
+ if (options.envs.length > 0) {
210
+ addEmptyLayer(config, options, `ENV ${JSON.stringify(options.envs)}`, (config) => {
211
+ // Keep old environment variables
212
+ config.config.Env = (0, utils_1.unique)([...config.config.Env, ...options.envs]);
213
+ config.container_config.Env = (0, utils_1.unique)([...config.config.Env, ...options.envs]);
214
+ });
215
+ }
216
+ });
217
+ }
218
+ function addLayers(tmpdir, fromdir, todir, options) {
219
+ return __awaiter(this, void 0, void 0, function* () {
220
+ logger_1.default.info("Parsing image ...");
221
+ const manifest = yield fse.readJson(path.join(fromdir, "manifest.json"));
222
+ const config = yield fse.readJson(path.join(fromdir, "config.json"));
223
+ config.container_config = config.container_config || {};
224
+ logger_1.default.info("Adding new layers...");
225
+ yield copyLayers(fromdir, todir, manifest.layers);
226
+ yield addAppLayers(options, config, todir, manifest, tmpdir);
227
+ logger_1.default.info("Writing final image...");
228
+ const configContent = Buffer.from(JSON.stringify(config));
229
+ const configHash = calculateHashOfBuffer(configContent);
230
+ const configFile = path.join(todir, configHash + ".json");
231
+ yield fs_1.promises.writeFile(configFile, configContent);
232
+ manifest.config.digest = "sha256:" + configHash;
233
+ manifest.config.size = yield fileutil.sizeOf(configFile);
234
+ yield fs_1.promises.writeFile(path.join(todir, "manifest.json"), JSON.stringify(manifest));
235
+ });
236
+ }
237
+ exports.default = {
238
+ addLayers,
239
+ };
package/lib/cli.js ADDED
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const os = require("os");
15
+ const commander_1 = require("commander");
16
+ const path = require("path");
17
+ const fse = require("fs-extra");
18
+ const fs = require("fs");
19
+ const registry_1 = require("./registry");
20
+ const appLayerCreator_1 = require("./appLayerCreator");
21
+ const tarExporter_1 = require("./tarExporter");
22
+ const logger_1 = require("./logger");
23
+ const utils_1 = require("./utils");
24
+ const fileutil_1 = require("./fileutil");
25
+ const version_1 = require("./version");
26
+ const possibleArgs = {
27
+ "--fromImage <name:tag>": "Required: Image name of base image - [path/]image:tag",
28
+ "--toImage <name:tag>": "Required: Image name of target image - [path/]image:tag",
29
+ "--folder <full path>": "Required: Base folder of node application (contains package.json)",
30
+ "--file <path>": "Optional: Name of configuration file (defaults to containerify.json if found on path)",
31
+ "--fromRegistry <registry url>": "Optional: URL of registry to pull base image from - Default: https://registry-1.docker.io/v2/",
32
+ "--fromToken <token>": "Optional: Authentication token for from registry",
33
+ "--toRegistry <registry url>": "Optional: URL of registry to push base image to - Default: https://registry-1.docker.io/v2/",
34
+ "--toToken <token>": "Optional: Authentication token for target registry",
35
+ "--toTar <path>": "Optional: Export to tar file",
36
+ "--registry <path>": "Optional: Convenience argument for setting both from and to registry",
37
+ "--platform <platform>": "Optional: Preferred platform, e.g. linux/amd64 or arm64",
38
+ "--token <path>": "Optional: Convenience argument for setting token for both from and to registry",
39
+ "--user <user>": "Optional: User account to run process in container - default: 1000",
40
+ "--workdir <directory>": "Optional: Workdir where node app will be added and run from - default: /app",
41
+ "--entrypoint <entrypoint>": "Optional: Entrypoint when starting container - default: npm start",
42
+ "--labels <labels>": "Optional: Comma-separated list of key value pairs to use as labels",
43
+ "--label <label>": "Optional: Single label (name=value). This option can be used multiple times.",
44
+ "--envs <envs>": "Optional: Comma-separated list of key value pairs to use av environment variables.",
45
+ "--env <env>": "Optional: Single environment variable (name=value). This option can be used multiple times.",
46
+ "--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",
47
+ "--verbose": "Verbose logging",
48
+ "--allowInsecureRegistries": "Allow insecure registries (with self-signed/untrusted cert)",
49
+ "--customContent <dirs/files>": "Optional: Skip normal node_modules and applayer and include specified root folder files/directories instead",
50
+ "--extraContent <dirs/files>": "Optional: Add specific content. Specify as local-path:absolute-container-path,local-path2:absolute-container-path2 etc",
51
+ "--layerOwner <gid:uid>": "Optional: Set specific gid and uid on files in the added layers",
52
+ "--buildFolder <path>": "Optional: Use a specific build folder when creating the image",
53
+ "--version": "Get containerify version",
54
+ };
55
+ function setKeyValue(target, keyValue) {
56
+ const [k, v] = keyValue.split("=", 2);
57
+ target[k.trim()] = v.trim();
58
+ }
59
+ const cliLabels = {};
60
+ commander_1.program.on("option:label", (ops) => {
61
+ setKeyValue(cliLabels, ops);
62
+ });
63
+ const cliEnv = {};
64
+ commander_1.program.on("option:env", (ops) => {
65
+ setKeyValue(cliEnv, ops);
66
+ });
67
+ const cliOptions = Object.entries(possibleArgs)
68
+ .reduce((program, [k, v]) => {
69
+ program.option(k, v);
70
+ return program;
71
+ }, commander_1.program)
72
+ .parse()
73
+ .opts();
74
+ if (cliOptions.version) {
75
+ console.log(`containerify v${version_1.VERSION}`);
76
+ process.exit(0);
77
+ }
78
+ const keys = Object.keys(possibleArgs).map((k) => k.split(" ")[0].replace("--", ""));
79
+ const defaultOptions = {
80
+ workdir: "/app",
81
+ user: "1000",
82
+ entrypoint: "npm start",
83
+ };
84
+ if (cliOptions.file && !fs.existsSync(cliOptions.file)) {
85
+ logger_1.default.error(`Config file '${cliOptions.file}' not found`);
86
+ process.exit(1);
87
+ }
88
+ if (!cliOptions.file && fs.existsSync(`${cliOptions.folder}/containerify.json`)) {
89
+ cliOptions.file = "containerify.json";
90
+ }
91
+ const configFromFile = cliOptions.file ? JSON.parse(fs.readFileSync(cliOptions.file, "utf-8")) : {};
92
+ Object.keys(configFromFile).forEach((k) => {
93
+ if (!keys.includes(k)) {
94
+ logger_1.default.error(`Unknown option in config-file '${cliOptions.file}': ${k}`);
95
+ process.exit(1);
96
+ }
97
+ });
98
+ const labelsOpt = {};
99
+ (_b = (_a = cliOptions.labels) === null || _a === void 0 ? void 0 : _a.split(",")) === null || _b === void 0 ? void 0 : _b.forEach((x) => setKeyValue(labelsOpt, x));
100
+ Object.keys(labelsOpt)
101
+ .filter((l) => Object.keys(cliLabels).includes(l))
102
+ .forEach((l) => {
103
+ exitWithErrorIf(true, `Label ${l} specified both with --labels and --label`);
104
+ });
105
+ const labels = Object.assign(Object.assign(Object.assign({}, configFromFile.labels), labelsOpt), cliLabels); //Let cli arguments override file
106
+ const envOpt = {};
107
+ (_d = (_c = cliOptions.envs) === null || _c === void 0 ? void 0 : _c.split(",")) === null || _d === void 0 ? void 0 : _d.forEach((x) => setKeyValue(envOpt, x));
108
+ Object.keys(envOpt)
109
+ .filter((l) => Object.keys(cliEnv).includes(l))
110
+ .forEach((l) => {
111
+ exitWithErrorIf(true, `Env ${l} specified both with --envs and --env`);
112
+ });
113
+ const envs = Object.assign(Object.assign(Object.assign({}, configFromFile.envs), envOpt), cliEnv); //Let cli arguments overide file
114
+ const cliParams = (0, utils_1.omit)(cliOptions, [
115
+ "label",
116
+ "labels",
117
+ "env",
118
+ "envs",
119
+ "customContent",
120
+ "extraContent",
121
+ ]);
122
+ cliParams.customContent = (_e = cliOptions.customContent) === null || _e === void 0 ? void 0 : _e.split(",");
123
+ cliParams.extraContent = (_f = cliOptions.extraContent) === null || _f === void 0 ? void 0 : _f.split(",").map((x) => x.split(":"));
124
+ const options = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultOptions), configFromFile), cliParams), { labels, envs: Object.entries(envs).map(([k, v]) => `${k}=${v}`) });
125
+ function exitWithErrorIf(check, error) {
126
+ if (check) {
127
+ logger_1.default.error("ERROR: " + error);
128
+ commander_1.program.help({ error: true });
129
+ }
130
+ }
131
+ if (options.verbose)
132
+ logger_1.default.enableDebug();
133
+ if (options.allowInsecureRegistries)
134
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
135
+ exitWithErrorIf(!!options.registry && !!options.fromRegistry, "Do not set both --registry and --fromRegistry");
136
+ exitWithErrorIf(!!options.registry && !!options.toRegistry, "Do not set both --registry and --toRegistry");
137
+ exitWithErrorIf(!!options.token && !!options.fromToken, "Do not set both --token and --fromToken");
138
+ exitWithErrorIf(!!options.token && !!options.toToken, "Do not set both --token and --toToken");
139
+ if (options.setTimeStamp) {
140
+ try {
141
+ options.setTimeStamp = new Date(options.setTimeStamp).toISOString();
142
+ }
143
+ catch (e) {
144
+ exitWithErrorIf(true, "Failed to parse date: " + e);
145
+ }
146
+ logger_1.default.info("Setting all dates to: " + options.setTimeStamp);
147
+ }
148
+ if (options.layerOwner) {
149
+ if (!options.layerOwner.match("^[0-9]+:[0-9]+$")) {
150
+ exitWithErrorIf(true, "layerOwner should be on format <number>:<number> (e.g. 1000:1000) but was: " + options.layerOwner);
151
+ }
152
+ }
153
+ if (options.registry) {
154
+ options.fromRegistry = options.registry;
155
+ options.toRegistry = options.registry;
156
+ }
157
+ if (options.token) {
158
+ options.fromToken = options.token;
159
+ options.toToken = options.token;
160
+ }
161
+ exitWithErrorIf(!options.folder, "--folder must be specified");
162
+ exitWithErrorIf(!options.fromImage, "--fromImage must be specified");
163
+ exitWithErrorIf(!options.toImage, "--toImage must be specified");
164
+ exitWithErrorIf(!options.toRegistry && !options.toTar, "Must specify either --toTar or --toRegistry");
165
+ exitWithErrorIf(!options.toRegistry && !options.toToken && !options.toTar, "A token must be given when uploading to docker hub");
166
+ if (options.toRegistry && !options.toRegistry.endsWith("/"))
167
+ options.toRegistry += "/";
168
+ if (options.fromRegistry && !options.fromRegistry.endsWith("/"))
169
+ options.fromRegistry += "/";
170
+ if (!options.fromRegistry && !((_j = (_h = (_g = options.fromImage) === null || _g === void 0 ? void 0 : _g.split(":")) === null || _h === void 0 ? void 0 : _h[0]) === null || _j === void 0 ? void 0 : _j.includes("/"))) {
171
+ options.fromImage = "library/" + options.fromImage;
172
+ }
173
+ if (options.customContent) {
174
+ options.customContent.forEach((p) => {
175
+ exitWithErrorIf(!fs.existsSync(p), "Could not find " + p + " in the base folder " + options.folder);
176
+ });
177
+ }
178
+ if (options.extraContent) {
179
+ options.extraContent.forEach((p) => {
180
+ exitWithErrorIf(p.length != 2, "Invalid extraContent - use comma between files/dirs, and : to separate local path and container path");
181
+ exitWithErrorIf(!fs.existsSync(options.folder + p[0]), "Could not find `" + p[0] + "` in the folder " + options.folder);
182
+ });
183
+ }
184
+ function run(options) {
185
+ var _a, _b;
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ if (!(yield fse.pathExists(options.folder)))
188
+ throw new Error("No such folder: " + options.folder);
189
+ const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "containerify-"));
190
+ logger_1.default.debug("Using " + tmpdir);
191
+ const fromdir = yield (0, fileutil_1.ensureEmptyDir)(path.join(tmpdir, "from"));
192
+ const todir = yield (0, fileutil_1.ensureEmptyDir)(path.join(tmpdir, "to"));
193
+ const fromRegistry = options.fromRegistry
194
+ ? (0, registry_1.createRegistry)(options.fromRegistry, (_a = options.fromToken) !== null && _a !== void 0 ? _a : "")
195
+ : (0, registry_1.createDockerRegistry)(options.fromToken);
196
+ yield fromRegistry.download(options.fromImage, fromdir, (0, utils_1.getPreferredPlatform)(options.platform));
197
+ yield appLayerCreator_1.default.addLayers(tmpdir, fromdir, todir, options);
198
+ if (options.toTar) {
199
+ yield tarExporter_1.default.saveToTar(todir, tmpdir, options.toTar, [options.toImage], options);
200
+ }
201
+ if (options.toRegistry) {
202
+ const toRegistry = (0, registry_1.createRegistry)(options.toRegistry, (_b = options.toToken) !== null && _b !== void 0 ? _b : "");
203
+ yield toRegistry.upload(options.toImage, todir);
204
+ }
205
+ logger_1.default.debug("Deleting " + tmpdir + " ...");
206
+ yield fse.remove(tmpdir);
207
+ logger_1.default.debug("Done");
208
+ });
209
+ }
210
+ logger_1.default.debug("Running with config:", options);
211
+ run(options)
212
+ .then(() => {
213
+ logger_1.default.info("Done!");
214
+ process.exit(0);
215
+ })
216
+ .catch((error) => {
217
+ logger_1.default.error(error);
218
+ process.exit(1);
219
+ });
@@ -0,0 +1,34 @@
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.ensureEmptyDir = exports.sizeOf = void 0;
13
+ const fs_1 = require("fs");
14
+ const fse = require("fs-extra");
15
+ const fss = require("fs");
16
+ function sizeOf(file) {
17
+ return __awaiter(this, void 0, void 0, function* () {
18
+ return (yield fs_1.promises.lstat(file)).size;
19
+ });
20
+ }
21
+ exports.sizeOf = sizeOf;
22
+ function ensureEmptyDir(path) {
23
+ return __awaiter(this, void 0, void 0, function* () {
24
+ if (fss.existsSync(path))
25
+ yield fse.remove(path);
26
+ yield fs_1.promises.mkdir(path);
27
+ return path;
28
+ });
29
+ }
30
+ exports.ensureEmptyDir = ensureEmptyDir;
31
+ exports.default = {
32
+ sizeOf,
33
+ ensureEmptyDir,
34
+ };
package/lib/logger.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /* eslint-disable no-console */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ let debugEnabled = false;
5
+ function timeString() {
6
+ return new Date().toISOString();
7
+ }
8
+ function dolog(logger, parts) {
9
+ logger.apply(console, [timeString()].concat(parts));
10
+ }
11
+ function log(level, msg) {
12
+ if (level == "error")
13
+ return dolog(console.error, ["ERROR"].concat(msg));
14
+ if (level == "info")
15
+ return dolog(console.log, msg);
16
+ if (level == "debug" && debugEnabled)
17
+ return dolog(console.log, ["DEBUG"].concat(msg));
18
+ }
19
+ const logger = {
20
+ enableDebug: () => (debugEnabled = true),
21
+ info: function (...msg) {
22
+ log("info", msg);
23
+ },
24
+ error: function (...msg) {
25
+ log("error", msg);
26
+ },
27
+ debug: function (...msg) {
28
+ log("debug", msg);
29
+ },
30
+ };
31
+ exports.default = logger;
@@ -0,0 +1,335 @@
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.createDockerRegistry = exports.createRegistry = void 0;
13
+ const https = require("https");
14
+ const http = require("http");
15
+ const URL = require("url");
16
+ const fs_1 = require("fs");
17
+ const path = require("path");
18
+ const fse = require("fs-extra");
19
+ const fss = require("fs");
20
+ const fileutil = require("./fileutil");
21
+ const logger_1 = require("./logger");
22
+ const MIMETypes_1 = require("./MIMETypes");
23
+ 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) {
72
+ return new Promise((resolve, reject) => {
73
+ followRedirects(uri, headers, (result) => {
74
+ var _a;
75
+ if ("error" in result)
76
+ return reject(result.error);
77
+ const { res } = result;
78
+ logger_1.default.debug(res.statusCode, res.statusMessage, res.headers["content-type"], res.headers["content-length"]);
79
+ if (!isOk((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0))
80
+ return reject(toError(res));
81
+ res.pipe(fss.createWriteStream(file)).on("finish", () => {
82
+ logger_1.default.debug("Done " + file + " - " + res.headers["content-length"] + " bytes ");
83
+ resolve();
84
+ });
85
+ });
86
+ });
87
+ }
88
+ function followRedirects(uri, headers, cb, count = 0) {
89
+ logger_1.default.debug("rc", uri);
90
+ const options = Object.assign({}, URL.parse(uri));
91
+ options.headers = headers;
92
+ options.method = "GET";
93
+ request(options, (res) => {
94
+ var _a;
95
+ if (redirectCodes.includes((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0)) {
96
+ if (count > 10)
97
+ return cb({ error: "Too many redirects for " + uri });
98
+ const location = res.headers.location;
99
+ if (!location)
100
+ return cb({ error: "Redirect, but missing location header" });
101
+ return followRedirects(location, headers, cb, count + 1);
102
+ }
103
+ cb({ res });
104
+ }).end();
105
+ }
106
+ function buildHeaders(accept, auth) {
107
+ const headers = { accept: accept };
108
+ if (auth)
109
+ headers.authorization = auth;
110
+ return headers;
111
+ }
112
+ function headOk(url, headers) {
113
+ return new Promise((resolve, reject) => {
114
+ logger_1.default.debug(`HEAD ${url}`);
115
+ const options = URL.parse(url);
116
+ options.headers = headers;
117
+ options.method = "HEAD";
118
+ request(options, (res) => {
119
+ logger_1.default.debug(`HEAD ${url}`, res.statusCode);
120
+ if (res.statusCode == 404)
121
+ return resolve(false);
122
+ if (res.statusCode == 200)
123
+ return resolve(true);
124
+ reject(toError(res));
125
+ }).end();
126
+ });
127
+ }
128
+ function uploadContent(uploadUrl, file, fileConfig, auth) {
129
+ return new Promise((resolve, reject) => {
130
+ logger_1.default.debug("Uploading: ", file);
131
+ let url = uploadUrl;
132
+ if (fileConfig.digest)
133
+ url += (url.indexOf("?") == -1 ? "?" : "&") + "digest=" + fileConfig.digest;
134
+ const options = URL.parse(url);
135
+ options.method = "PUT";
136
+ options.headers = {
137
+ authorization: auth,
138
+ "content-length": fileConfig.size,
139
+ "content-type": fileConfig.mediaType,
140
+ };
141
+ logger_1.default.debug("POST", url);
142
+ const req = request(options, (res) => {
143
+ var _a;
144
+ logger_1.default.debug(res.statusCode, res.statusMessage, res.headers["content-type"], res.headers["content-length"]);
145
+ if ([200, 201, 202, 203].includes((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0)) {
146
+ resolve();
147
+ }
148
+ else {
149
+ const data = [];
150
+ res.on("data", (d) => data.push(d.toString()));
151
+ res.on("end", () => {
152
+ reject(`Error uploading to ${uploadUrl}. Got ${res.statusCode} ${res.statusMessage}:\n${data.join("")}`);
153
+ });
154
+ }
155
+ });
156
+ fss.createReadStream(file).pipe(req);
157
+ });
158
+ }
159
+ function createRegistry(registryBaseUrl, token) {
160
+ const auth = token.startsWith("Basic ") ? token : "Bearer " + token;
161
+ function exists(image, layer) {
162
+ return __awaiter(this, void 0, void 0, function* () {
163
+ const url = `${registryBaseUrl}${image.path}/blobs/${layer.digest}`;
164
+ return yield headOk(url, buildHeaders(layer.mediaType, auth));
165
+ });
166
+ }
167
+ function uploadLayerContent(uploadUrl, layer, dir) {
168
+ return __awaiter(this, void 0, void 0, function* () {
169
+ logger_1.default.info(layer.digest);
170
+ const file = path.join(dir, getHash(layer.digest) + (0, utils_1.getLayerTypeFileEnding)(layer));
171
+ yield uploadContent(uploadUrl, file, layer, auth);
172
+ });
173
+ }
174
+ function getUploadUrl(image) {
175
+ return __awaiter(this, void 0, void 0, function* () {
176
+ return new Promise((resolve, reject) => {
177
+ const url = `${registryBaseUrl}${image.path}/blobs/uploads/`;
178
+ const options = URL.parse(url);
179
+ options.method = "POST";
180
+ options.headers = { authorization: auth };
181
+ request(options, (res) => {
182
+ logger_1.default.debug("POST", `${url}`, res.statusCode);
183
+ if (res.statusCode == 202) {
184
+ const { location } = res.headers;
185
+ if (location)
186
+ resolve(location);
187
+ reject("Missing location for 202");
188
+ }
189
+ else {
190
+ const data = [];
191
+ res
192
+ .on("data", (c) => data.push(c.toString()))
193
+ .on("end", () => {
194
+ reject(`Error getting upload URL from ${url}. Got ${res.statusCode} ${res.statusMessage}:\n${data.join("")}`);
195
+ });
196
+ }
197
+ }).end();
198
+ });
199
+ });
200
+ }
201
+ function dlManifest(image, preferredPlatform) {
202
+ return __awaiter(this, void 0, void 0, function* () {
203
+ // Accept both manifests and index/manifest lists
204
+ 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));
205
+ // We've received an OCI Index or Docker Manifest List and need to find which manifest we want
206
+ if (res.mediaType === MIMETypes_1.OCI.index || res.mediaType === MIMETypes_1.DockerV2.index) {
207
+ const availableManifests = res.manifests;
208
+ const adequateManifest = pickManifest(availableManifests, preferredPlatform);
209
+ return dlManifest(Object.assign(Object.assign({}, image), { tag: adequateManifest.digest }), preferredPlatform);
210
+ }
211
+ return res;
212
+ });
213
+ }
214
+ function pickManifest(manifests, preferredPlatform) {
215
+ const matchingArchitectures = new Set();
216
+ const matchingOSes = new Set();
217
+ // Find sets of matching architecture and os
218
+ for (const manifest of manifests) {
219
+ if (manifest.platform.architecture === preferredPlatform.architecture) {
220
+ matchingArchitectures.add(manifest);
221
+ }
222
+ if (manifest.platform.os === preferredPlatform.os) {
223
+ matchingOSes.add(manifest);
224
+ }
225
+ }
226
+ // If the intersection of matching architectures and OS is one we've found our optimal match
227
+ const intersection = new Set([...matchingArchitectures].filter((x) => matchingOSes.has(x)));
228
+ if (intersection.size == 1) {
229
+ return intersection.values().next().value;
230
+ }
231
+ // If we don't have a perfect match we give a warning and try the first matching architecture
232
+ if (matchingArchitectures.size >= 1) {
233
+ const matchingArch = matchingArchitectures.values().next().value;
234
+ logger_1.default.info(`[WARN] Preferred OS '${preferredPlatform.os}' not available.`);
235
+ logger_1.default.info("[WARN] Using closest available manifest:", JSON.stringify(matchingArch.platform));
236
+ return matchingArch;
237
+ }
238
+ // If there's no image matching the wanted architecture we bail
239
+ logger_1.default.error(`No image matching requested architecture: '${preferredPlatform.architecture}'`);
240
+ logger_1.default.error("Available platforms:", JSON.stringify(manifests.map((m) => m.platform)));
241
+ throw new Error("No image matching requested architecture");
242
+ }
243
+ function dlConfig(image, config) {
244
+ return __awaiter(this, void 0, void 0, function* () {
245
+ return yield dlJson(`${registryBaseUrl}${image.path}/blobs/${config.digest}`, buildHeaders("*/*", auth));
246
+ });
247
+ }
248
+ function dlLayer(image, layer, folder) {
249
+ return __awaiter(this, void 0, void 0, function* () {
250
+ logger_1.default.info(layer.digest);
251
+ const file = getHash(layer.digest) + (0, utils_1.getLayerTypeFileEnding)(layer);
252
+ yield dlToFile(`${registryBaseUrl}${image.path}/blobs/${layer.digest}`, path.join(folder, file), buildHeaders(layer.mediaType, auth));
253
+ return file;
254
+ });
255
+ }
256
+ function upload(imageStr, folder) {
257
+ return __awaiter(this, void 0, void 0, function* () {
258
+ const image = parseImage(imageStr);
259
+ const manifestFile = path.join(folder, "manifest.json");
260
+ const manifest = (yield fse.readJson(manifestFile));
261
+ logger_1.default.info("Checking layer status...");
262
+ const layerStatus = yield Promise.all(manifest.layers.map((l) => __awaiter(this, void 0, void 0, function* () {
263
+ return { layer: l, exists: yield exists(image, l) };
264
+ })));
265
+ const layersForUpload = layerStatus.filter((l) => !l.exists);
266
+ logger_1.default.debug("Needs upload:", layersForUpload.map((l) => l.layer.digest));
267
+ logger_1.default.info("Uploading layers...");
268
+ yield Promise.all(layersForUpload.map((l) => __awaiter(this, void 0, void 0, function* () {
269
+ const url = yield getUploadUrl(image);
270
+ yield uploadLayerContent(url, l.layer, folder);
271
+ })));
272
+ logger_1.default.info("Uploading config...");
273
+ const configUploadUrl = yield getUploadUrl(image);
274
+ const configFile = path.join(folder, getHash(manifest.config.digest) + ".json");
275
+ yield uploadContent(configUploadUrl, configFile, manifest.config, auth);
276
+ logger_1.default.info("Uploading manifest...");
277
+ const manifestSize = yield fileutil.sizeOf(manifestFile);
278
+ yield uploadContent(`${registryBaseUrl}${image.path}/manifests/${image.tag}`, manifestFile, { mediaType: manifest.mediaType, size: manifestSize }, auth);
279
+ });
280
+ }
281
+ function download(imageStr, folder, preferredPlatform) {
282
+ return __awaiter(this, void 0, void 0, function* () {
283
+ const image = parseImage(imageStr);
284
+ logger_1.default.info("Downloading manifest...");
285
+ const manifest = yield dlManifest(image, preferredPlatform);
286
+ yield fs_1.promises.writeFile(path.join(folder, "manifest.json"), JSON.stringify(manifest));
287
+ logger_1.default.info("Downloading config...");
288
+ const config = yield dlConfig(image, manifest.config);
289
+ if (config.architecture != preferredPlatform.architecture) {
290
+ logger_1.default.info(`[WARN] Image architecture (${config.architecture}) does not match preferred architecture (${preferredPlatform.architecture}).`);
291
+ }
292
+ if (config.os != preferredPlatform.os) {
293
+ logger_1.default.info(`[WARN] Image OS (${config.os}) does not match preferred OS (${preferredPlatform.os}).`);
294
+ }
295
+ yield fs_1.promises.writeFile(path.join(folder, "config.json"), JSON.stringify(config));
296
+ logger_1.default.info("Downloading layers...");
297
+ yield Promise.all(manifest.layers.map((layer) => dlLayer(image, layer, folder)));
298
+ logger_1.default.info("Image downloaded.");
299
+ });
300
+ }
301
+ return {
302
+ download: download,
303
+ upload: upload,
304
+ };
305
+ }
306
+ exports.createRegistry = createRegistry;
307
+ function createDockerRegistry(auth) {
308
+ const registryBaseUrl = "https://registry-1.docker.io/v2/";
309
+ function getToken(image) {
310
+ return __awaiter(this, void 0, void 0, function* () {
311
+ const resp = yield dlJson(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image.path}:pull`, {});
312
+ return resp.token;
313
+ });
314
+ }
315
+ function download(imageStr, folder, platform) {
316
+ return __awaiter(this, void 0, void 0, function* () {
317
+ const image = parseImage(imageStr);
318
+ if (!auth)
319
+ auth = yield getToken(image);
320
+ yield createRegistry(registryBaseUrl, auth).download(imageStr, folder, platform);
321
+ });
322
+ }
323
+ function upload(imageStr, folder) {
324
+ return __awaiter(this, void 0, void 0, function* () {
325
+ if (!auth)
326
+ throw new Error("Need auth token to upload to Docker");
327
+ yield createRegistry(registryBaseUrl, auth).upload(imageStr, folder);
328
+ });
329
+ }
330
+ return {
331
+ download: download,
332
+ upload: upload,
333
+ };
334
+ }
335
+ exports.createDockerRegistry = createDockerRegistry;
@@ -0,0 +1,55 @@
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
+ const tar = require("tar");
13
+ const fs_1 = require("fs");
14
+ const fse = require("fs-extra");
15
+ const path = require("path");
16
+ const logger_1 = require("./logger");
17
+ const tarDefaultConfig = {
18
+ preservePaths: false,
19
+ portable: true,
20
+ follow: true,
21
+ };
22
+ function saveToTar(fromdir, tmpdir, toPath, repoTags, options) {
23
+ return __awaiter(this, void 0, void 0, function* () {
24
+ logger_1.default.info("Creating " + toPath + " ...");
25
+ const targetFolder = path.dirname(toPath);
26
+ yield fs_1.promises.access(targetFolder).catch(() => __awaiter(this, void 0, void 0, function* () { return yield fs_1.promises.mkdir(targetFolder, { recursive: true }); }));
27
+ const manifestFile = path.join(fromdir, "manifest.json");
28
+ const manifest = (yield fse.readJson(manifestFile));
29
+ const configFile = path.join(fromdir, manifest.config.digest.split(":")[1] + ".json");
30
+ const config = yield fse.readJson(configFile);
31
+ const tardir = path.join(tmpdir, "totar");
32
+ yield fs_1.promises.mkdir(tardir);
33
+ const layers = yield Promise.all(manifest.layers
34
+ .map((x) => x.digest.split(":")[1])
35
+ .map((x) => __awaiter(this, void 0, void 0, function* () {
36
+ const fn = x + ((yield fse.pathExists(path.join(fromdir, x + ".tar.gz"))) ? ".tar.gz" : ".tar");
37
+ yield fse.copy(path.join(fromdir, fn), path.join(tardir, fn));
38
+ return fn;
39
+ })));
40
+ const simpleManifest = [
41
+ {
42
+ config: "config.json",
43
+ repoTags: repoTags,
44
+ layers: layers,
45
+ },
46
+ ];
47
+ yield fs_1.promises.writeFile(path.join(tardir, "manifest.json"), JSON.stringify(simpleManifest));
48
+ yield fs_1.promises.writeFile(path.join(tardir, "config.json"), JSON.stringify(config));
49
+ yield tar.c(Object.assign(Object.assign({}, tarDefaultConfig), Object.assign({ cwd: tardir, file: toPath, noMtime: !options.setTimeStamp }, (options.setTimeStamp ? { mtime: new Date(options.setTimeStamp) } : {}))), ["config.json", "manifest.json"].concat(layers));
50
+ logger_1.default.info("Finished " + toPath);
51
+ });
52
+ }
53
+ exports.default = {
54
+ saveToTar,
55
+ };
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/lib/utils.js ADDED
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLayerTypeFileEnding = exports.getManifestLayerType = exports.getPreferredPlatform = exports.omit = exports.unique = void 0;
4
+ const MIMETypes_1 = require("./MIMETypes");
5
+ function unique(vals) {
6
+ return [...new Set(vals)];
7
+ }
8
+ exports.unique = unique;
9
+ function omit(obj, keys) {
10
+ return Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k)));
11
+ }
12
+ exports.omit = omit;
13
+ function getPreferredPlatform(platform) {
14
+ var _a, _b;
15
+ // We assume the input is similar to docker which accepts `<os>/<arch>` and `<arch>`
16
+ let os = process.platform;
17
+ let arch = process.arch;
18
+ if (platform != undefined) {
19
+ const input = platform.split("/");
20
+ if (input.length == 1) {
21
+ arch = input[0];
22
+ }
23
+ else if (input.length == 2) {
24
+ os = input[0];
25
+ arch = input[1];
26
+ }
27
+ else {
28
+ throw new Error(`Invalid platform ${platform}. Should be on format <os>/<arch> or <arch>`);
29
+ }
30
+ }
31
+ // Mapping from Node's process.platform and Golang's `$GOOS` to Golang's `$GOOS`
32
+ // Incomplete, but cover the most common OS-types
33
+ // https://nodejs.org/api/process.html#processplatform
34
+ // https://go.dev/doc/install/source#environment
35
+ const OS_MAPPING = {
36
+ aix: "aix",
37
+ darwin: "darwin",
38
+ freebsd: "freebsd",
39
+ linux: "linux",
40
+ openbsd: "openbsd",
41
+ sunos: "solaris",
42
+ solaris: "solaris",
43
+ win32: "windows",
44
+ windows: "windows",
45
+ };
46
+ const targetOS = (_a = Object.entries(OS_MAPPING).find(([k]) => k == os)) === null || _a === void 0 ? void 0 : _a[1];
47
+ if (targetOS == undefined) {
48
+ throw new Error(`Platform ${os} not supported. Supported platforms are '${Object.keys(OS_MAPPING)}`);
49
+ }
50
+ // Mapping from Node's `process.arch` and Golang's `$GOARCH` to Golang's `$GOARCH` (incomplete)
51
+ // Incomplete, but cover the most common architectures
52
+ // https://nodejs.org/api/process.html#processarch
53
+ // https://go.dev/doc/install/source#environment
54
+ const ARCH_MAPPING = {
55
+ ia32: "386",
56
+ "386": "386",
57
+ x64: "amd64",
58
+ amd64: "amd64",
59
+ arm: "arm",
60
+ arm64: "arm64",
61
+ };
62
+ const targetArch = (_b = Object.entries(ARCH_MAPPING).find(([k]) => k == arch)) === null || _b === void 0 ? void 0 : _b[1];
63
+ if (targetArch == undefined) {
64
+ throw new Error(`Architecture ${arch} not supported. Supported architectures are '${Object.keys(ARCH_MAPPING)}'.`);
65
+ }
66
+ return {
67
+ os: targetOS,
68
+ architecture: targetArch,
69
+ };
70
+ }
71
+ exports.getPreferredPlatform = getPreferredPlatform;
72
+ function getManifestLayerType(manifest) {
73
+ if (manifest.mediaType === MIMETypes_1.OCI.manifest) {
74
+ return MIMETypes_1.OCI.layer.gzip;
75
+ }
76
+ if (manifest.mediaType === MIMETypes_1.DockerV2.manifest) {
77
+ return MIMETypes_1.DockerV2.layer.gzip;
78
+ }
79
+ throw new Error(`${manifest.mediaType} not recognized.`);
80
+ }
81
+ exports.getManifestLayerType = getManifestLayerType;
82
+ function getLayerTypeFileEnding(layer) {
83
+ switch (layer.mediaType) {
84
+ case MIMETypes_1.OCI.layer.gzip:
85
+ case MIMETypes_1.DockerV2.layer.gzip:
86
+ return ".tar.gz";
87
+ case MIMETypes_1.OCI.layer.tar:
88
+ case MIMETypes_1.DockerV2.layer.tar:
89
+ return ".tar";
90
+ default:
91
+ throw new Error(`Layer mediaType ${layer.mediaType} not known.`);
92
+ }
93
+ }
94
+ exports.getLayerTypeFileEnding = getLayerTypeFileEnding;
package/lib/version.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ exports.VERSION = "2.0.0";
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "containerify",
3
+ "version": "2.0.0",
4
+ "description": "Build node.js docker images without docker",
5
+ "main": "./lib/cli.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "prebuild": "node -p \"'export const VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts",
9
+ "build": "tsc && chmod ugo+x lib/cli.js",
10
+ "lint": "eslint . --ext .ts --fix --ignore-path .gitignore",
11
+ "typecheck": "tsc --noEmit",
12
+ "check": "npm run lint && npm run typecheck",
13
+ "dev": "tsc --watch",
14
+ "integrationTest": "cd tests/integration/ && ./test.sh",
15
+ "registryTest": "cd tests/localtest/ && ./test.sh"
16
+ },
17
+ "bin": {
18
+ "containerify": "./lib/cli.js"
19
+ },
20
+ "author": "Erlend Oftedal <erlend@oftedal.no>",
21
+ "license": "Apache-2.0",
22
+ "dependencies": {
23
+ "commander": "^10.0.0",
24
+ "fs-extra": "^11.1.0",
25
+ "tar": "^6.1.13"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/eoftedal/doqr.git"
30
+ },
31
+ "files": [
32
+ "lib/*.js",
33
+ "bin/*"
34
+ ],
35
+ "keywords": [
36
+ "docker",
37
+ "container",
38
+ "image"
39
+ ],
40
+ "devDependencies": {
41
+ "@types/fs-extra": "^11.0.1",
42
+ "@types/minizlib": "^2.1.4",
43
+ "@types/node": "^18.11.19",
44
+ "@types/tar": "^6.1.3",
45
+ "@typescript-eslint/eslint-plugin": "^5.51.0",
46
+ "@typescript-eslint/parser": "^5.51.0",
47
+ "eslint": "^8.33.0",
48
+ "eslint-config-prettier": "^8.6.0",
49
+ "prettier": "^2.8.3",
50
+ "typescript": "^4.9.5"
51
+ }
52
+ }