Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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: 4 additions & 0 deletions src/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,8 @@ export class Argv {
get childPipelineDepth (): number {
return this.map.get("childPipelineDepth");
}

get registry (): boolean {
return this.map.get("registry") ?? false;
}
}
12 changes: 12 additions & 0 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[
Commander.runCsv(parser, writeStreams, argv.listCsvAll);
} else if (argv.job.length > 0) {
assert(argv.stage === null, "You cannot use --stage when starting individual jobs");
if (argv.registry) {
await Utils.startDockerRegistry(argv);
}
generateGitIgnore(cwd, stateDir);
const time = process.hrtime();
if (argv.needs || argv.onlyNeeds) {
Expand All @@ -77,6 +80,9 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[
writeStreams.stderr(chalk`{grey pipeline finished} in {grey ${prettyHrtime(process.hrtime(time))}}\n`);
}
} else if (argv.stage) {
if (argv.registry) {
await Utils.startDockerRegistry(argv);
}
generateGitIgnore(cwd, stateDir);
const time = process.hrtime();
const pipelineIid = await state.getPipelineIid(cwd, stateDir);
Expand All @@ -85,6 +91,9 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[
await Commander.runJobsInStage(argv, parser, writeStreams);
writeStreams.stderr(chalk`{grey pipeline finished} in {grey ${prettyHrtime(process.hrtime(time))}}\n`);
} else {
if (argv.registry) {
await Utils.startDockerRegistry(argv);
}
generateGitIgnore(cwd, stateDir);
const time = process.hrtime();
await state.incrementPipelineIid(cwd, stateDir);
Expand All @@ -96,5 +105,8 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[
}
writeStreams.flush();

if (argv.registry) {
await Utils.stopDockerRegistry(argv.containerExecutable);
}
return cleanupJobResources(jobs);
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ process.on("SIGUSR2", async () => await cleanupJobResources(jobs));
default: true,
description: "Enables color",
})
.option("registry", {
type: "boolean",
requiresArg: false,
description: "Start a local docker registry and configure gitlab-ci-local containers to use that by default",
})
.completion("completion", false, (current: string, yargsArgv: any, completionFilter: any, done: (completions: string[]) => any) => {
try {
if (current.startsWith("-")) {
Expand Down
13 changes: 12 additions & 1 deletion src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export class Job {
predefinedVariables["CI_NODE_INDEX"] = `${opt.nodeIndex}`;
}
predefinedVariables["CI_NODE_TOTAL"] = `${opt.nodesTotal}`;
predefinedVariables["CI_REGISTRY"] = `local-registry.${this.gitData.remote.host}`;
predefinedVariables["CI_REGISTRY"] = predefinedVariables["CI_REGISTRY"] = this.argv.registry ? Utils.gclRegistryPrefix : `local-registry.${this.gitData.remote.host}`;
predefinedVariables["CI_REGISTRY_IMAGE"] = `$CI_REGISTRY/${predefinedVariables["CI_PROJECT_PATH"].toLowerCase()}`;
return predefinedVariables;
}
Expand Down Expand Up @@ -829,6 +829,11 @@ export class Job {
});
}

if (this.argv.registry) {
expanded["CI_REGISTRY_USER"] = expanded["CI_REGISTRY_USER"] ?? `${Utils.gclRegistryPrefix}.user`;
expanded["CI_REGISTRY_PASSWORD"] = expanded["CI_REGISTRY_PASSWORD"] ?? `${Utils.gclRegistryPrefix}.password`;
}

this.refreshLongRunningSilentTimeout(writeStreams);

if (imageName && !this._containerId) {
Expand Down Expand Up @@ -893,6 +898,12 @@ export class Job {
dockerCmd += `--network ${this._serviceNetworkId} --network-alias build `;
}

if (this.argv.registry) {
dockerCmd += `--network ${Utils.gclRegistryPrefix}.net `;
dockerCmd += `--volume ${Utils.gclRegistryPrefix}.certs:/etc/containers/certs.d:ro `;
dockerCmd += `--volume ${Utils.gclRegistryPrefix}.certs:/etc/docker/certs.d:ro `;
}

dockerCmd += `--volume ${buildVolumeName}:${this.ciProjectDir} `;
dockerCmd += `--volume ${tmpVolumeName}:${this.fileVariablesDir} `;
dockerCmd += `--workdir ${this.ciProjectDir} `;
Expand Down
77 changes: 77 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,4 +434,81 @@ export class Utils {
// https://dev.to/babak/exhaustive-type-checking-with-typescript-4l3f
throw new Error(`Unhandled case ${param}`);
}

static async dockerVolumeFileExists (containerExecutable: string, path: string, volume: string): Promise<boolean> {
try {
await Utils.spawn([containerExecutable, "run", "--rm", "-v", `${volume}:/mnt/vol`, "alpine", "ls", `/mnt/vol/${path}`]);
return true;
} catch {
return false;
}
}

static gclRegistryPrefix: string = "registry.gcl.local";
static async startDockerRegistry (argv: Argv): Promise<void> {
const gclRegistryCertVol = `${this.gclRegistryPrefix}.certs`;
const gclRegistryDataVol = `${this.gclRegistryPrefix}.data`;
const gclRegistryNet = `${this.gclRegistryPrefix}.net`;

// create cert volume
try {
await Utils.spawn(`${argv.containerExecutable} volume create ${gclRegistryCertVol}`.split(" "));
} catch (err) {
if (err instanceof Error && !err.message.endsWith("already exists"))
throw err;
}

// create self-signed cert/key files for https support
if (!await this.dockerVolumeFileExists(argv.containerExecutable, `${this.gclRegistryPrefix}.crt`, gclRegistryCertVol)) {
const opensslArgs = [
"req", "-newkey", "rsa:4096", "-nodes", "-sha256",
"-keyout", `/certs/${this.gclRegistryPrefix}.key`,
"-x509", "-days", "365",
"-out", `/certs/${this.gclRegistryPrefix}.crt`,
"-subj", `/CN=${this.gclRegistryPrefix}`,
"-addext", `subjectAltName=DNS:${this.gclRegistryPrefix}`,
];
const generateCertsInPlace = [
argv.containerExecutable, "run", "--rm", "-v", `${gclRegistryCertVol}:/certs`, "--entrypoint", "sh", "alpine/openssl", "-c",
[
"openssl", ...opensslArgs,
"&&", "mkdir", "-p", `/certs/${this.gclRegistryPrefix}`,
"&&", "cp", `/certs/${this.gclRegistryPrefix}.crt`, `/certs/${this.gclRegistryPrefix}/ca.crt`,
].join(" "),
];
await Utils.spawn(generateCertsInPlace);
}

// create data volume
try {
await Utils.spawn([argv.containerExecutable, "volume", "create", gclRegistryDataVol]);
} catch (err) {
if (err instanceof Error && !err.message.endsWith("already exists"))
throw err;
}

// create network
try {
await Utils.spawn([argv.containerExecutable, "network", "create", gclRegistryNet]);
} catch (err) {
if (err instanceof Error && !err.message.includes("already exists"))
throw err;
}

await Utils.spawn([argv.containerExecutable, "rm", "-f", this.gclRegistryPrefix]);
await Utils.spawn([
argv.containerExecutable, "run", "-d", "--name", this.gclRegistryPrefix,
"--network", gclRegistryNet,
"--volume", `${gclRegistryDataVol}:/var/lib/registry`,
"--volume", `${gclRegistryCertVol}:/certs:ro`,
"-e", "REGISTRY_HTTP_ADDR=0.0.0.0:443",
"-e", `REGISTRY_HTTP_TLS_CERTIFICATE=/certs/${this.gclRegistryPrefix}.crt`,
"-e", `REGISTRY_HTTP_TLS_KEY=/certs/${this.gclRegistryPrefix}.key`,
"registry",
]);
}

static async stopDockerRegistry (containerExecutable: string): Promise<void> {
await Utils.spawn([containerExecutable, "rm", "-f", this.gclRegistryPrefix]);
}
}
17 changes: 17 additions & 0 deletions tests/test-cases/local-registry/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
registry-variables:
image: alpine:latest
script:
- echo "CI_REGISTRY=$CI_REGISTRY"
- echo "CI_REGISTRY_USER=$CI_REGISTRY_USER"
- echo "CI_REGISTRY_PASSWORD=$CI_REGISTRY_PASSWORD"

registry-login-docker:
image: docker:dind
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY

registry-login-oci:
image: quay.io/podman/stable
script:
- echo "$CI_REGISTRY_PASSWORD" | podman login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
54 changes: 54 additions & 0 deletions tests/test-cases/local-registry/integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {WriteStreamsMock} from "../../../src/write-streams.js";
import {handler} from "../../../src/handler.js";
import {Utils} from "../../../src/utils.js";
import chalk from "chalk";

test("local-registry ci variables", async () => {
const writeStreams = new WriteStreamsMock;
await handler({
cwd: "tests/test-cases/local-registry",
job: ["registry-variables"],
registry: true,
}, writeStreams);

const expected = [
chalk`{blueBright registry-variables} {greenBright >} CI_REGISTRY=${Utils.gclRegistryPrefix}`,
chalk`{blueBright registry-variables} {greenBright >} CI_REGISTRY_USER=${Utils.gclRegistryPrefix}.user`,
chalk`{blueBright registry-variables} {greenBright >} CI_REGISTRY_PASSWORD=${Utils.gclRegistryPrefix}.password`,
];

expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
});

test("local-registry login <docker>", async () => {
const writeStreams = new WriteStreamsMock();
await handler({
cwd: "tests/test-cases/local-registry",
job: ["registry-login-docker"],
registry: true,
}, writeStreams);


const expected = [
chalk`{blueBright registry-login-docker} {greenBright >} Login Succeeded`,
];

expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
});

test("local-registry login <oci>", async () => {
const writeStreams = new WriteStreamsMock();
await handler({
cwd: "tests/test-cases/local-registry",
job: ["registry-login-oci"],
registry: true,
privileged: true,
}, writeStreams);


const expected = [
chalk`{blueBright registry-login-oci} {greenBright >} Login Succeeded!`,
];

expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
});