Skip to content

Commit 987e19e

Browse files
committed
feat: option to start local docker registry
1 parent bf655d1 commit 987e19e

File tree

7 files changed

+177
-0
lines changed

7 files changed

+177
-0
lines changed

src/argv.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,8 @@ export class Argv {
337337
get childPipelineDepth (): number {
338338
return this.map.get("childPipelineDepth");
339339
}
340+
341+
get registry (): boolean {
342+
return this.map.get("registry") ?? false;
343+
}
340344
}

src/handler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[
6464
Commander.runCsv(parser, writeStreams, argv.listCsvAll);
6565
} else if (argv.job.length > 0) {
6666
assert(argv.stage === null, "You cannot use --stage when starting individual jobs");
67+
if (argv.registry) {
68+
await Utils.startDockerRegistry(argv);
69+
}
6770
generateGitIgnore(cwd, stateDir);
6871
const time = process.hrtime();
6972
if (argv.needs || argv.onlyNeeds) {
@@ -77,6 +80,9 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[
7780
writeStreams.stderr(chalk`{grey pipeline finished} in {grey ${prettyHrtime(process.hrtime(time))}}\n`);
7881
}
7982
} else if (argv.stage) {
83+
if (argv.registry) {
84+
await Utils.startDockerRegistry(argv);
85+
}
8086
generateGitIgnore(cwd, stateDir);
8187
const time = process.hrtime();
8288
const pipelineIid = await state.getPipelineIid(cwd, stateDir);
@@ -85,6 +91,9 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[
8591
await Commander.runJobsInStage(argv, parser, writeStreams);
8692
writeStreams.stderr(chalk`{grey pipeline finished} in {grey ${prettyHrtime(process.hrtime(time))}}\n`);
8793
} else {
94+
if (argv.registry) {
95+
await Utils.startDockerRegistry(argv);
96+
}
8897
generateGitIgnore(cwd, stateDir);
8998
const time = process.hrtime();
9099
await state.incrementPipelineIid(cwd, stateDir);
@@ -96,5 +105,8 @@ export async function handler (args: any, writeStreams: WriteStreams, jobs: Job[
96105
}
97106
writeStreams.flush();
98107

108+
if (argv.registry) {
109+
await Utils.stopDockerRegistry(argv.containerExecutable);
110+
}
99111
return cleanupJobResources(jobs);
100112
}

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,11 @@ process.on("SIGUSR2", async () => await cleanupJobResources(jobs));
308308
default: true,
309309
description: "Enables color",
310310
})
311+
.option("registry", {
312+
type: "boolean",
313+
requiresArg: false,
314+
description: "Start a local docker registry and configure gitlab-ci-local containers to use that by default",
315+
})
311316
.completion("completion", false, (current: string, yargsArgv: any, completionFilter: any, done: (completions: string[]) => any) => {
312317
try {
313318
if (current.startsWith("-")) {

src/job.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,15 @@ export class Job {
893893
dockerCmd += `--network ${this._serviceNetworkId} --network-alias build `;
894894
}
895895

896+
if (this.argv.registry) {
897+
dockerCmd += `--network ${Utils.gclRegistryPrefix}.net `;
898+
dockerCmd += `--volume ${Utils.gclRegistryPrefix}.certs:/etc/containers/certs.d:ro `;
899+
dockerCmd += `--volume ${Utils.gclRegistryPrefix}.certs:/etc/docker/certs.d:ro `;
900+
expanded["CI_REGISTRY"] = Utils.gclRegistryPrefix;
901+
expanded["CI_REGISTRY_USER"] = expanded["CI_REGISTRY_USER"] ?? `${Utils.gclRegistryPrefix}.user`;
902+
expanded["CI_REGISTRY_PASSWORD"] = expanded["CI_REGISTRY_PASSWORD"] ?? `${Utils.gclRegistryPrefix}.password`;
903+
}
904+
896905
dockerCmd += `--volume ${buildVolumeName}:${this.ciProjectDir} `;
897906
dockerCmd += `--volume ${tmpVolumeName}:${this.fileVariablesDir} `;
898907
dockerCmd += `--workdir ${this.ciProjectDir} `;

src/utils.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,4 +434,82 @@ export class Utils {
434434
// https://dev.to/babak/exhaustive-type-checking-with-typescript-4l3f
435435
throw new Error(`Unhandled case ${param}`);
436436
}
437+
438+
static async dockerVolumeFileExists (containerExecutable: string, path: string, volume: string): Promise<boolean> {
439+
try {
440+
await Utils.spawn([containerExecutable, "run", "--rm", "-v", `${volume}:/mnt/vol`, "alpine", "ls", `/mnt/vol/${path}`]);
441+
return true;
442+
} catch {
443+
return false;
444+
}
445+
}
446+
447+
static gclRegistryPrefix: string = "registry.gcl.local";
448+
static async startDockerRegistry (argv: Argv): Promise<void> {
449+
const gclRegistryCertVol = `${this.gclRegistryPrefix}.certs`;
450+
const gclRegistryDataVol = `${this.gclRegistryPrefix}.data`;
451+
const gclRegistryNet = `${this.gclRegistryPrefix}.net`;
452+
453+
// create cert volume
454+
try {
455+
await Utils.spawn(`${argv.containerExecutable} volume create ${gclRegistryCertVol}`.split(" "));
456+
} catch (err) {
457+
if (err instanceof Error && "exitCode" in err && err.exitCode !== 125)
458+
throw err;
459+
}
460+
461+
// create self-signed cert/key files for https support
462+
if (!await this.dockerVolumeFileExists(argv.containerExecutable, `${this.gclRegistryPrefix}.crt`, gclRegistryCertVol)) {
463+
const opensslArgs = [
464+
"req", "-newkey", "rsa:4096", "-nodes", "-sha256",
465+
"-keyout", `/certs/${this.gclRegistryPrefix}.key`,
466+
"-x509", "-days", "365",
467+
"-out", `/certs/${this.gclRegistryPrefix}.crt`,
468+
"-subj", `/CN=${this.gclRegistryPrefix}`,
469+
"-addext", `subjectAltName=DNS:${this.gclRegistryPrefix}`
470+
];
471+
const generateCertsInPlace = [
472+
argv.containerExecutable, "run", "--rm", "-v", `${gclRegistryCertVol}:/certs`, "--entrypoint", "sh", "alpine/openssl", "-c",
473+
[
474+
"openssl", ...opensslArgs,
475+
"&&", "mkdir", "-p", `/certs/${this.gclRegistryPrefix}`,
476+
"&&", "cp", `/certs/${this.gclRegistryPrefix}.crt`, `/certs/${this.gclRegistryPrefix}/ca.crt`,
477+
].join(" ")
478+
];
479+
await Utils.spawn(generateCertsInPlace);
480+
}
481+
482+
// create data volume
483+
try {
484+
await Utils.spawn([argv.containerExecutable, "volume", "create", gclRegistryDataVol]);
485+
} catch (err) {
486+
// rethrow error if not 'already exists' (exitCode 125)
487+
if (err instanceof Error && "exitCode" in err && err.exitCode !== 125)
488+
throw err;
489+
}
490+
491+
// create network
492+
try {
493+
await Utils.spawn([argv.containerExecutable, "network", "create", gclRegistryNet]);
494+
} catch (err) {
495+
if (err instanceof Error && "exitCode" in err && err.exitCode !== 125)
496+
throw err;
497+
}
498+
499+
await Utils.spawn([argv.containerExecutable, "rm", "-f", this.gclRegistryPrefix]);
500+
await Utils.spawn([
501+
argv.containerExecutable, "run", "-d", "--name", this.gclRegistryPrefix,
502+
"--network", gclRegistryNet,
503+
"--volume", `${gclRegistryDataVol}:/var/lib/registry`,
504+
"--volume", `${gclRegistryCertVol}:/certs:ro`,
505+
"-e", "REGISTRY_HTTP_ADDR=0.0.0.0:443",
506+
"-e", `REGISTRY_HTTP_TLS_CERTIFICATE=/certs/${this.gclRegistryPrefix}.crt`,
507+
"-e", `REGISTRY_HTTP_TLS_KEY=/certs/${this.gclRegistryPrefix}.key`,
508+
"registry"
509+
]);
510+
}
511+
512+
static async stopDockerRegistry (containerExecutable: string): Promise<void> {
513+
await Utils.spawn([containerExecutable, "rm", "-f", this.gclRegistryPrefix]);
514+
}
437515
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
registry-variables:
2+
image: alpine:latest
3+
script:
4+
- echo "CI_REGISTRY=$CI_REGISTRY"
5+
- echo "CI_REGISTRY_USER=$CI_REGISTRY_USER"
6+
- echo "CI_REGISTRY_PASSWORD=$CI_REGISTRY_PASSWORD"
7+
8+
registry-login-docker:
9+
image: docker:dind
10+
script:
11+
- echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
12+
13+
registry-login-oci:
14+
image: quay.io/podman/stable
15+
script:
16+
- echo "$CI_REGISTRY_PASSWORD" | podman login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {WriteStreamsMock} from "../../../src/write-streams.js";
2+
import {handler} from "../../../src/handler.js";
3+
import {Utils} from "../../../src/utils.js";
4+
import chalk from "chalk";
5+
6+
test("local-registry ci variables", async () => {
7+
const writeStreams = new WriteStreamsMock;
8+
await handler({
9+
cwd: "tests/test-cases/local-registry",
10+
job: ["registry-variables"],
11+
registry: true
12+
}, writeStreams);
13+
14+
const expected = [
15+
chalk`{blueBright registry-variables} {greenBright >} CI_REGISTRY=${Utils.gclRegistryPrefix}`,
16+
chalk`{blueBright registry-variables} {greenBright >} CI_REGISTRY_USER=${Utils.gclRegistryPrefix}.user`,
17+
chalk`{blueBright registry-variables} {greenBright >} CI_REGISTRY_PASSWORD=${Utils.gclRegistryPrefix}.password`,
18+
];
19+
20+
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
21+
});
22+
23+
test("local-registry login <docker>", async () => {
24+
const writeStreams = new WriteStreamsMock();
25+
await handler({
26+
cwd: "tests/test-cases/local-registry",
27+
job: ["registry-login-docker"],
28+
registry: true
29+
}, writeStreams);
30+
31+
32+
const expected = [
33+
chalk`{blueBright registry-login-docker} {greenBright >} Login Succeeded`,
34+
];
35+
36+
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
37+
});
38+
39+
test("local-registry login <oci>", async () => {
40+
const writeStreams = new WriteStreamsMock();
41+
await handler({
42+
cwd: "tests/test-cases/local-registry",
43+
job: ["registry-login-oci"],
44+
registry: true
45+
}, writeStreams);
46+
47+
48+
const expected = [
49+
chalk`{blueBright registry-login-oci} {greenBright >} Login Succeeded!`,
50+
];
51+
52+
expect(writeStreams.stdoutLines).toEqual(expect.arrayContaining(expected));
53+
});

0 commit comments

Comments
 (0)