Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infer registry from toimage #36

Merged
merged 10 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ This will take the `nginx:alpine` image, and copy the files from `./dist/` into
Usage: containerify [options]

Options:
--from <registry/image:tag> Optional: Shorthand to specify fromRegistry and fromImage in one argument
--to <registry/image:tag> Optional: Shorthand to specify toRegistry and toImage in one argument
--fromImage <name:tag> Required: Image name of base image - [path/]image:tag
--toImage <name:tag> Required: Image name of target image - [path/]image:tag
--folder <full path> Required: Base folder of node application (contains package.json)
--file <path> Optional: Name of configuration file (defaults to containerify.json if found on path)
--doCrossMount <true/false> Cross mount image layers from the base image (only works if fromImage and toImage are in the same registry) (default: false)
--doCrossMount Optional: Cross mount image layers from the base image (only works if fromImage and toImage are in the same registry) (default: false)
--fromRegistry <registry url> Optional: URL of registry to pull base image from - Default: https://registry-1.docker.io/v2/
--fromToken <token> Optional: Authentication token for from registry
--toRegistry <registry url> Optional: URL of registry to push base image to - Default: https://registry-1.docker.io/v2/
Expand Down
38 changes: 29 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as path from "path";
import * as fse from "fs-extra";
import * as fs from "fs";

import { createRegistry, createDockerRegistry } from "./registry";
import { DEFAULT_DOCKER_REGISTRY, createRegistry, processToken, parseFullImageUrl } from "./registry";
import appLayerCreator from "./appLayerCreator";
import dockerExporter from "./dockerExporter";
import tarExporter from "./tarExporter";
Expand All @@ -18,12 +18,14 @@ import { ensureEmptyDir } from "./fileutil";
import { VERSION } from "./version";

const possibleArgs = {
"--from <registry/image:tag>": "Optional: Shorthand to specify fromRegistry and fromImage in one argument",
"--to <registry/image:tag>": "Optional: Shorthand to specify toRegistry and toImage in one argument",
"--fromImage <name:tag>": "Required: Image name of base image - [path/]image:tag",
"--toImage <name:tag>": "Required: Image name of target image - [path/]image:tag",
"--folder <full path>": "Required: Base folder of node application (contains package.json)",
"--file <path>": "Optional: Name of configuration file (defaults to containerify.json if found on path)",
"--doCrossMount <true/false>":
"Cross mount image layers from the base image (only works if fromImage and toImage are in the same registry) (default: false)",
"--doCrossMount":
"Optional: Cross mount image layers from the base image (only works if fromImage and toImage are in the same registry) (default: false)",
"--fromRegistry <registry url>":
"Optional: URL of registry to pull base image from - Default: https://registry-1.docker.io/v2/",
"--fromToken <token>": "Optional: Authentication token for from registry",
Expand Down Expand Up @@ -181,13 +183,29 @@ function exitWithErrorIf(check: boolean, error: string) {
if (options.verbose) logger.enableDebug();

exitWithErrorIf(!!options.registry && !!options.fromRegistry, "Do not set both --registry and --fromRegistry");
exitWithErrorIf(!!options.from && !!options.fromRegistry, "Do not set both --from and --fromRegistry");
exitWithErrorIf(!!options.registry && !!options.from, "Do not set both --registry and --from");

exitWithErrorIf(!!options.registry && !!options.toRegistry, "Do not set both --registry and --toRegistry");
exitWithErrorIf(!!options.to && !!options.toRegistry, "Do not set both --toRegistry and --to");
exitWithErrorIf(!!options.registry && !!options.to, "Do not set both --registry and --to");
if (options.from) {
const { registry, image } = parseFullImageUrl(options.from);
options.fromRegistry = registry;
options.fromImage = image;
}
if (options.to) {
const { registry, image } = parseFullImageUrl(options.to);
options.toRegistry = registry;
options.toImage = image;
}

exitWithErrorIf(!!options.token && !!options.fromToken, "Do not set both --token and --fromToken");
exitWithErrorIf(!!options.token && !!options.toToken, "Do not set both --token and --toToken");

exitWithErrorIf(
!!options.doCrossMount && options.toRegistry != options.fromRegistry,
"Cross mounting only works if fromRegistry and toRegistry are the same",
`Cross mounting only works if fromRegistry and toRegistry are the same (${options.fromRegistry} != ${options.toRegistry})`,
);

if (options.setTimeStamp) {
Expand Down Expand Up @@ -263,10 +281,12 @@ async function run(options: Options) {
const fromdir = await ensureEmptyDir(path.join(tmpdir, "from"));
const todir = await ensureEmptyDir(path.join(tmpdir, "to"));
const allowInsecure = options.allowInsecureRegistries ? InsecureRegistrySupport.YES : InsecureRegistrySupport.NO;

const fromRegistry = options.fromRegistry
? createRegistry(options.fromRegistry, options.fromToken ?? "", allowInsecure)
: createDockerRegistry(allowInsecure, options.fromToken);
const fromRegistryUrl = options.fromRegistry ?? DEFAULT_DOCKER_REGISTRY;
const fromRegistry = createRegistry(
fromRegistryUrl,
await processToken(fromRegistryUrl, allowInsecure, options.fromImage, options.fromToken),
allowInsecure,
);
const originalManifest = await fromRegistry.download(
options.fromImage,
fromdir,
Expand All @@ -290,7 +310,7 @@ async function run(options: Options) {
if (options.toRegistry) {
const toRegistry = createRegistry(
options.toRegistry,
options.toToken ?? "",
await processToken(options.toRegistry, allowInsecure, options.toImage, options.toToken),
allowInsecure,
options.optimisticToRegistryCheck,
);
Expand Down
98 changes: 36 additions & 62 deletions src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Manifest,
PartialManifestConfig,
Platform,
Registry,
} from "./types";
import { DockerV2, OCI } from "./MIMETypes";
import { getLayerTypeFileEnding } from "./utils";
Expand Down Expand Up @@ -231,24 +232,6 @@ function uploadContent(
});
}

function prepareToken(token: string) {
if (token.startsWith("Basic ")) return token;
if (token.startsWith("ghp_")) return "Bearer " + Buffer.from(token).toString("base64");
return "Bearer " + token;
}

type Registry = {
download: (imageStr: string, folder: string, preferredPlatform: Platform, cacheFolder?: string) => Promise<Manifest>;
upload: (
imageStr: string,
folder: string,
doCrossMount: boolean,
originalManifest: Manifest,
originalRepository: string,
) => Promise<void>;
registryBaseUrl: string;
};

type Mount = { mount: string; from: string };
type UploadURL = { uploadUrl: string };
type UploadURLorMounted = UploadURL | { mountSuccess: true };
Expand All @@ -259,7 +242,7 @@ export function createRegistry(
allowInsecure: InsecureRegistrySupport,
optimisticToRegistryCheck = false,
): Registry {
const auth = prepareToken(token);
const auth = token;

async function exists(image: Image, layer: Layer) {
const url = `${registryBaseUrl}${image.path}/blobs/${layer.digest}`;
Expand All @@ -274,10 +257,10 @@ export function createRegistry(

async function getUploadUrl(
image: Image,
mountParamters: Mount | undefined = undefined,
mountParameters: Mount | undefined = undefined,
): Promise<UploadURLorMounted> {
return new Promise((resolve, reject) => {
const parameters = new URLSearchParams(mountParamters);
const parameters = new URLSearchParams(mountParameters);
const url = `${registryBaseUrl}${image.path}/blobs/uploads/${parameters.size > 0 ? "?" + parameters : ""}`;
const options: https.RequestOptions = URL.parse(url);
options.method = "POST";
Expand All @@ -297,11 +280,11 @@ export function createRegistry(
}
}
reject("Missing location for 202");
} else if (mountParamters && res.statusCode == 201) {
} else if (mountParameters && res.statusCode == 201) {
const returnedDigest = res.headers["docker-content-digest"];
if (returnedDigest && returnedDigest != mountParamters.mount) {
if (returnedDigest && returnedDigest != mountParameters.mount) {
reject(
`ERROR: Layer mounted with wrong digest: Expected ${mountParamters.mount} but got ${returnedDigest}`,
`ERROR: Layer mounted with wrong digest: Expected ${mountParameters.mount} but got ${returnedDigest}`,
);
}
resolve({ mountSuccess: true });
Expand Down Expand Up @@ -505,49 +488,40 @@ export function createRegistry(
};
}

export function createDockerRegistry(allowInsecure: InsecureRegistrySupport, auth?: string): Registry {
const registryBaseUrl = "https://registry-1.docker.io/v2/";
export const DEFAULT_DOCKER_REGISTRY = "https://registry-1.docker.io/v2/";

async function getToken(image: Image) {
export function parseFullImageUrl(imageStr: string): { registry: string; image: string } {
const [registry, ...rest] = imageStr.split("/");
if (registry == "docker.io") {
return {
registry: DEFAULT_DOCKER_REGISTRY,
image: rest.join("/"),
};
}
return {
registry: `https://${registry}/v2/`,
image: rest.join("/"),
};
}

export async function processToken(
registryBaseUrl: string,
allowInsecure: InsecureRegistrySupport,
imagePath: string,
token?: string,
): Promise<string> {
const { hostname } = URL.parse(registryBaseUrl);
const image = parseImage(imagePath);
if (hostname?.endsWith(".docker.io") && !token) {
const resp = await dlJson<{ token: string }>(
`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image.path}:pull`,
{},
allowInsecure,
);
return resp.token;
}

async function download(
imageStr: string,
folder: string,
platform: Platform,
cacheFolder?: string,
): Promise<Manifest> {
const image = parseImage(imageStr);
if (!auth) auth = await getToken(image);
return await createRegistry(registryBaseUrl, auth, allowInsecure).download(imageStr, folder, platform, cacheFolder);
}

async function upload(
imageStr: string,
folder: string,
doCrossMount: boolean,
originalManifest: Manifest,
originalRepository: string,
) {
if (!auth) throw new Error("Need auth token to upload to Docker");
await createRegistry(registryBaseUrl, auth, allowInsecure).upload(
imageStr,
folder,
doCrossMount,
originalManifest,
originalRepository,
);
return `Bearer ${resp.token}`;
}

return {
download: download,
upload: upload,
registryBaseUrl,
};
if (!token) throw new Error("Need auth token to upload to " + registryBaseUrl);
if (token.startsWith("Basic ")) return token;
if (token.startsWith("ghp_")) return "Bearer " + Buffer.from(token).toString("base64");
return "Bearer " + token;
}
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export type HistoryLine = {
};

export type Options = {
from?: string;
to?: string;
fromImage: string;
toImage: string;
folder: string;
Expand Down Expand Up @@ -105,3 +107,14 @@ export enum InsecureRegistrySupport {
NO,
YES,
}
export type Registry = {
download: (imageStr: string, folder: string, preferredPlatform: Platform, cacheFolder?: string) => Promise<Manifest>;
upload: (
imageStr: string,
folder: string,
doCrossMount: boolean,
originalManifest: Manifest,
originalRepository: string,
) => Promise<void>;
registryBaseUrl: string;
};
2 changes: 1 addition & 1 deletion tests/external-registries/github-ghcr-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ if [[ -z "$GITHUB_TOKEN" ]]; then
fi

printf "* Running containerify to pull from and push result to gchr.io ...\n"
../../lib/cli.js --verbose --fromImage docker-mirror/node:alpine --registry https://ghcr.io/v2/ --toImage eoftedal/containerify-integrationtest:latest --folder . --customContent customContent --setTimeStamp "2024-01-15T20:00:00.000Z" --token "$GITHUB_TOKEN"
../../lib/cli.js --verbose --doCrossMount --from ghcr.io/docker-mirror/node:alpine --to ghcr.io/eoftedal/containerify-integrationtest:latest --folder . --customContent customContent --setTimeStamp "2024-01-15T20:00:00.000Z" --token "$GITHUB_TOKEN"
6 changes: 5 additions & 1 deletion tests/localtest/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ docker pull node:alpine &> /dev/null

printf "* Pushing base image to local containerify test registry...\n"
docker tag node:alpine localhost:5443/node > /dev/null
echo -n $TESTPASSWORD | docker login -u $TESTUSER --password-stdin localhost:5443
docker push localhost:5443/node > /dev/null

printf "* Running containerify to pull from and push result to the local containerify test registry...\n"
../../lib/cli.js --fromImage node --doCrossMount true --registry https://localhost:5443/v2/ --toImage containerify-integration-test:localtest --folder ../integration/app --setTimeStamp "2023-03-07T12:53:10.471Z" --allowInsecureRegistries --token "Basic $BASICAUTH"
cd ../integration/app
npm install
cd ../../localtest
../../lib/cli.js --fromImage node --doCrossMount --registry https://localhost:5443/v2/ --toImage containerify-integration-test:localtest --folder ../integration/app --setTimeStamp "2023-03-07T12:53:10.471Z" --allowInsecureRegistries --token "Basic $BASICAUTH"


printf "\n* Pulling image from registry to local docker daemon...\n"
Expand Down
Loading