@git.zone/tsdocker 1.3.0 → 1.4.1

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.
@@ -0,0 +1,462 @@
1
+ import * as plugins from './tsdocker.plugins.js';
2
+ import * as paths from './tsdocker.paths.js';
3
+ import { logger } from './tsdocker.logging.js';
4
+ import { DockerRegistry } from './classes.dockerregistry.js';
5
+ import type { IDockerfileOptions, ITsDockerConfig } from './interfaces/index.js';
6
+ import type { TsDockerManager } from './classes.tsdockermanager.js';
7
+
8
+ const smartshellInstance = new plugins.smartshell.Smartshell({
9
+ executor: 'bash',
10
+ });
11
+
12
+ /**
13
+ * Class Dockerfile represents a Dockerfile on disk
14
+ */
15
+ export class Dockerfile {
16
+ // STATIC METHODS
17
+
18
+ /**
19
+ * Creates instances of class Dockerfile for all Dockerfiles in cwd
20
+ */
21
+ public static async readDockerfiles(managerRef: TsDockerManager): Promise<Dockerfile[]> {
22
+ const entries = await plugins.smartfs.directory(paths.cwd).filter('Dockerfile*').list();
23
+ const fileTree = entries
24
+ .filter(entry => entry.isFile)
25
+ .map(entry => plugins.path.join(paths.cwd, entry.name));
26
+
27
+ const readDockerfilesArray: Dockerfile[] = [];
28
+ logger.log('info', `found ${fileTree.length} Dockerfiles:`);
29
+ console.log(fileTree);
30
+
31
+ for (const dockerfilePath of fileTree) {
32
+ const myDockerfile = new Dockerfile(managerRef, {
33
+ filePath: dockerfilePath,
34
+ read: true,
35
+ });
36
+ readDockerfilesArray.push(myDockerfile);
37
+ }
38
+
39
+ return readDockerfilesArray;
40
+ }
41
+
42
+ /**
43
+ * Sorts Dockerfiles into a build order based on dependencies (topological sort)
44
+ */
45
+ public static async sortDockerfiles(dockerfiles: Dockerfile[]): Promise<Dockerfile[]> {
46
+ logger.log('info', 'Sorting Dockerfiles based on dependencies...');
47
+
48
+ // Map from cleanTag to Dockerfile instance for quick lookup
49
+ const tagToDockerfile = new Map<string, Dockerfile>();
50
+ dockerfiles.forEach((dockerfile) => {
51
+ tagToDockerfile.set(dockerfile.cleanTag, dockerfile);
52
+ });
53
+
54
+ // Build the dependency graph
55
+ const graph = new Map<Dockerfile, Dockerfile[]>();
56
+ dockerfiles.forEach((dockerfile) => {
57
+ const dependencies: Dockerfile[] = [];
58
+ const baseImage = dockerfile.baseImage;
59
+
60
+ // Check if the baseImage is among the local Dockerfiles
61
+ if (tagToDockerfile.has(baseImage)) {
62
+ const baseDockerfile = tagToDockerfile.get(baseImage)!;
63
+ dependencies.push(baseDockerfile);
64
+ dockerfile.localBaseImageDependent = true;
65
+ dockerfile.localBaseDockerfile = baseDockerfile;
66
+ }
67
+
68
+ graph.set(dockerfile, dependencies);
69
+ });
70
+
71
+ // Perform topological sort
72
+ const sortedDockerfiles: Dockerfile[] = [];
73
+ const visited = new Set<Dockerfile>();
74
+ const tempMarked = new Set<Dockerfile>();
75
+
76
+ const visit = (dockerfile: Dockerfile) => {
77
+ if (tempMarked.has(dockerfile)) {
78
+ throw new Error(`Circular dependency detected involving ${dockerfile.cleanTag}`);
79
+ }
80
+ if (!visited.has(dockerfile)) {
81
+ tempMarked.add(dockerfile);
82
+ const dependencies = graph.get(dockerfile) || [];
83
+ dependencies.forEach((dep) => visit(dep));
84
+ tempMarked.delete(dockerfile);
85
+ visited.add(dockerfile);
86
+ sortedDockerfiles.push(dockerfile);
87
+ }
88
+ };
89
+
90
+ try {
91
+ dockerfiles.forEach((dockerfile) => {
92
+ if (!visited.has(dockerfile)) {
93
+ visit(dockerfile);
94
+ }
95
+ });
96
+ } catch (error) {
97
+ logger.log('error', (error as Error).message);
98
+ throw error;
99
+ }
100
+
101
+ // Log the sorted order
102
+ sortedDockerfiles.forEach((dockerfile, index) => {
103
+ logger.log(
104
+ 'info',
105
+ `Build order ${index + 1}: ${dockerfile.cleanTag} with base image ${dockerfile.baseImage}`
106
+ );
107
+ });
108
+
109
+ return sortedDockerfiles;
110
+ }
111
+
112
+ /**
113
+ * Maps local Dockerfiles dependencies to the corresponding Dockerfile class instances
114
+ */
115
+ public static async mapDockerfiles(sortedDockerfileArray: Dockerfile[]): Promise<Dockerfile[]> {
116
+ sortedDockerfileArray.forEach((dockerfileArg) => {
117
+ if (dockerfileArg.localBaseImageDependent) {
118
+ sortedDockerfileArray.forEach((dockfile2: Dockerfile) => {
119
+ if (dockfile2.cleanTag === dockerfileArg.baseImage) {
120
+ dockerfileArg.localBaseDockerfile = dockfile2;
121
+ }
122
+ });
123
+ }
124
+ });
125
+ return sortedDockerfileArray;
126
+ }
127
+
128
+ /**
129
+ * Builds the corresponding real docker image for each Dockerfile class instance
130
+ */
131
+ public static async buildDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> {
132
+ for (const dockerfileArg of sortedArrayArg) {
133
+ await dockerfileArg.build();
134
+ }
135
+ return sortedArrayArg;
136
+ }
137
+
138
+ /**
139
+ * Tests all Dockerfiles by calling Dockerfile.test()
140
+ */
141
+ public static async testDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> {
142
+ for (const dockerfileArg of sortedArrayArg) {
143
+ await dockerfileArg.test();
144
+ }
145
+ return sortedArrayArg;
146
+ }
147
+
148
+ /**
149
+ * Returns a version for a docker file
150
+ * Dockerfile_latest -> latest
151
+ * Dockerfile_v1.0.0 -> v1.0.0
152
+ * Dockerfile -> latest
153
+ */
154
+ public static dockerFileVersion(
155
+ dockerfileInstanceArg: Dockerfile,
156
+ dockerfileNameArg: string
157
+ ): string {
158
+ let versionString: string;
159
+ const versionRegex = /Dockerfile_(.+)$/;
160
+ const regexResultArray = versionRegex.exec(dockerfileNameArg);
161
+ if (regexResultArray && regexResultArray.length === 2) {
162
+ versionString = regexResultArray[1];
163
+ } else {
164
+ versionString = 'latest';
165
+ }
166
+
167
+ // Replace ##version## placeholder with actual package version if available
168
+ if (dockerfileInstanceArg.managerRef?.projectInfo?.npm?.version) {
169
+ versionString = versionString.replace(
170
+ '##version##',
171
+ dockerfileInstanceArg.managerRef.projectInfo.npm.version
172
+ );
173
+ }
174
+
175
+ return versionString;
176
+ }
177
+
178
+ /**
179
+ * Extracts the base image from a Dockerfile content
180
+ * Handles ARG substitution for variable base images
181
+ */
182
+ public static dockerBaseImage(dockerfileContentArg: string): string {
183
+ const lines = dockerfileContentArg.split(/\r?\n/);
184
+ const args: { [key: string]: string } = {};
185
+
186
+ for (const line of lines) {
187
+ const trimmedLine = line.trim();
188
+
189
+ // Skip empty lines and comments
190
+ if (trimmedLine === '' || trimmedLine.startsWith('#')) {
191
+ continue;
192
+ }
193
+
194
+ // Match ARG instructions
195
+ const argMatch = trimmedLine.match(/^ARG\s+([^\s=]+)(?:=(.*))?$/i);
196
+ if (argMatch) {
197
+ const argName = argMatch[1];
198
+ const argValue = argMatch[2] !== undefined ? argMatch[2] : process.env[argName] || '';
199
+ args[argName] = argValue;
200
+ continue;
201
+ }
202
+
203
+ // Match FROM instructions
204
+ const fromMatch = trimmedLine.match(/^FROM\s+(.+?)(?:\s+AS\s+[^\s]+)?$/i);
205
+ if (fromMatch) {
206
+ let baseImage = fromMatch[1].trim();
207
+
208
+ // Substitute variables in the base image name
209
+ baseImage = Dockerfile.substituteVariables(baseImage, args);
210
+
211
+ return baseImage;
212
+ }
213
+ }
214
+
215
+ throw new Error('No FROM instruction found in Dockerfile');
216
+ }
217
+
218
+ /**
219
+ * Substitutes variables in a string, supporting default values like ${VAR:-default}
220
+ */
221
+ private static substituteVariables(str: string, vars: { [key: string]: string }): string {
222
+ return str.replace(/\${([^}:]+)(:-([^}]+))?}/g, (_, varName, __, defaultValue) => {
223
+ if (vars[varName] !== undefined) {
224
+ return vars[varName];
225
+ } else if (defaultValue !== undefined) {
226
+ return defaultValue;
227
+ } else {
228
+ return '';
229
+ }
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Returns the docker tag string for a given registry and repo
235
+ */
236
+ public static getDockerTagString(
237
+ managerRef: TsDockerManager,
238
+ registryArg: string,
239
+ repoArg: string,
240
+ versionArg: string,
241
+ suffixArg?: string
242
+ ): string {
243
+ // Determine whether the repo should be mapped according to the registry
244
+ const config = managerRef.config;
245
+ const mappedRepo = config.registryRepoMap?.[registryArg];
246
+ const repo = mappedRepo || repoArg;
247
+
248
+ // Determine whether the version contains a suffix
249
+ let version = versionArg;
250
+ if (suffixArg) {
251
+ version = versionArg + '_' + suffixArg;
252
+ }
253
+
254
+ const tagString = `${registryArg}/${repo}:${version}`;
255
+ return tagString;
256
+ }
257
+
258
+ /**
259
+ * Gets build args from environment variable mapping
260
+ */
261
+ public static async getDockerBuildArgs(managerRef: TsDockerManager): Promise<string> {
262
+ logger.log('info', 'checking for env vars to be supplied to the docker build');
263
+ let buildArgsString: string = '';
264
+ const config = managerRef.config;
265
+
266
+ if (config.buildArgEnvMap) {
267
+ for (const dockerArgKey of Object.keys(config.buildArgEnvMap)) {
268
+ const dockerArgOuterEnvVar = config.buildArgEnvMap[dockerArgKey];
269
+ logger.log(
270
+ 'note',
271
+ `docker ARG "${dockerArgKey}" maps to outer env var "${dockerArgOuterEnvVar}"`
272
+ );
273
+ const targetValue = process.env[dockerArgOuterEnvVar];
274
+ if (targetValue) {
275
+ buildArgsString = `${buildArgsString} --build-arg ${dockerArgKey}="${targetValue}"`;
276
+ }
277
+ }
278
+ }
279
+ return buildArgsString;
280
+ }
281
+
282
+ // INSTANCE PROPERTIES
283
+ public managerRef: TsDockerManager;
284
+ public filePath!: string;
285
+ public repo: string;
286
+ public version: string;
287
+ public cleanTag: string;
288
+ public buildTag: string;
289
+ public pushTag!: string;
290
+ public containerName: string;
291
+ public content!: string;
292
+ public baseImage: string;
293
+ public localBaseImageDependent: boolean;
294
+ public localBaseDockerfile!: Dockerfile;
295
+
296
+ constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) {
297
+ this.managerRef = managerRefArg;
298
+ this.filePath = options.filePath!;
299
+
300
+ // Build repo name from project info or directory name
301
+ const projectInfo = this.managerRef.projectInfo;
302
+ if (projectInfo?.npm?.name) {
303
+ // Use package name, removing scope if present
304
+ const packageName = projectInfo.npm.name.replace(/^@[^/]+\//, '');
305
+ this.repo = packageName;
306
+ } else {
307
+ // Fallback to directory name
308
+ this.repo = plugins.path.basename(paths.cwd);
309
+ }
310
+
311
+ this.version = Dockerfile.dockerFileVersion(this, plugins.path.parse(this.filePath).base);
312
+ this.cleanTag = this.repo + ':' + this.version;
313
+ this.buildTag = this.cleanTag;
314
+ this.containerName = 'dockerfile-' + this.version;
315
+
316
+ if (options.filePath && options.read) {
317
+ const fs = require('fs');
318
+ this.content = fs.readFileSync(plugins.path.resolve(options.filePath), 'utf-8');
319
+ } else if (options.fileContents) {
320
+ this.content = options.fileContents;
321
+ }
322
+
323
+ this.baseImage = Dockerfile.dockerBaseImage(this.content);
324
+ this.localBaseImageDependent = false;
325
+ }
326
+
327
+ /**
328
+ * Builds the Dockerfile
329
+ */
330
+ public async build(): Promise<void> {
331
+ logger.log('info', 'now building Dockerfile for ' + this.cleanTag);
332
+ const buildArgsString = await Dockerfile.getDockerBuildArgs(this.managerRef);
333
+ const config = this.managerRef.config;
334
+
335
+ let buildCommand: string;
336
+
337
+ // Check if multi-platform build is needed
338
+ if (config.platforms && config.platforms.length > 1) {
339
+ // Multi-platform build using buildx
340
+ const platformString = config.platforms.join(',');
341
+ buildCommand = `docker buildx build --platform ${platformString} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
342
+
343
+ if (config.push) {
344
+ buildCommand += ' --push';
345
+ } else {
346
+ buildCommand += ' --load';
347
+ }
348
+ } else {
349
+ // Standard build
350
+ const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
351
+ buildCommand = `docker build --label="version=${versionLabel}" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
352
+ }
353
+
354
+ const result = await smartshellInstance.exec(buildCommand);
355
+ if (result.exitCode !== 0) {
356
+ logger.log('error', `Build failed for ${this.cleanTag}`);
357
+ console.log(result.stdout);
358
+ throw new Error(`Build failed for ${this.cleanTag}`);
359
+ }
360
+
361
+ logger.log('ok', `Built ${this.cleanTag}`);
362
+ }
363
+
364
+ /**
365
+ * Pushes the Dockerfile to a registry
366
+ */
367
+ public async push(dockerRegistryArg: DockerRegistry, versionSuffix?: string): Promise<void> {
368
+ this.pushTag = Dockerfile.getDockerTagString(
369
+ this.managerRef,
370
+ dockerRegistryArg.registryUrl,
371
+ this.repo,
372
+ this.version,
373
+ versionSuffix
374
+ );
375
+
376
+ await smartshellInstance.exec(`docker tag ${this.buildTag} ${this.pushTag}`);
377
+ const pushResult = await smartshellInstance.exec(`docker push ${this.pushTag}`);
378
+
379
+ if (pushResult.exitCode !== 0) {
380
+ logger.log('error', `Push failed for ${this.pushTag}`);
381
+ throw new Error(`Push failed for ${this.pushTag}`);
382
+ }
383
+
384
+ // Get image digest
385
+ const inspectResult = await smartshellInstance.exec(
386
+ `docker inspect --format="{{index .RepoDigests 0}}" ${this.pushTag}`
387
+ );
388
+
389
+ if (inspectResult.exitCode === 0 && inspectResult.stdout.includes('@')) {
390
+ const imageDigest = inspectResult.stdout.split('@')[1]?.trim();
391
+ console.log(`The image ${this.pushTag} has digest ${imageDigest}`);
392
+ }
393
+
394
+ logger.log('ok', `Pushed ${this.pushTag}`);
395
+ }
396
+
397
+ /**
398
+ * Pulls the Dockerfile from a registry
399
+ */
400
+ public async pull(registryArg: DockerRegistry, versionSuffixArg?: string): Promise<void> {
401
+ const pullTag = Dockerfile.getDockerTagString(
402
+ this.managerRef,
403
+ registryArg.registryUrl,
404
+ this.repo,
405
+ this.version,
406
+ versionSuffixArg
407
+ );
408
+
409
+ await smartshellInstance.exec(`docker pull ${pullTag}`);
410
+ await smartshellInstance.exec(`docker tag ${pullTag} ${this.buildTag}`);
411
+
412
+ logger.log('ok', `Pulled and tagged ${pullTag} as ${this.buildTag}`);
413
+ }
414
+
415
+ /**
416
+ * Tests the Dockerfile by running a test script if it exists
417
+ */
418
+ public async test(): Promise<void> {
419
+ const testDir = this.managerRef.config.testDir || plugins.path.join(paths.cwd, 'test');
420
+ const testFile = plugins.path.join(testDir, 'test_' + this.version + '.sh');
421
+
422
+ const fs = require('fs');
423
+ const testFileExists = fs.existsSync(testFile);
424
+
425
+ if (testFileExists) {
426
+ logger.log('info', `Running tests for ${this.cleanTag}`);
427
+
428
+ // Run tests in container
429
+ await smartshellInstance.exec(
430
+ `docker run --name tsdocker_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /tsdocker_test"`
431
+ );
432
+ await smartshellInstance.exec(`docker cp ${testFile} tsdocker_test_container:/tsdocker_test/test.sh`);
433
+ await smartshellInstance.exec(`docker commit tsdocker_test_container tsdocker_test_image`);
434
+
435
+ const testResult = await smartshellInstance.exec(
436
+ `docker run --entrypoint="bash" tsdocker_test_image -x /tsdocker_test/test.sh`
437
+ );
438
+
439
+ // Cleanup
440
+ await smartshellInstance.exec(`docker rm tsdocker_test_container`);
441
+ await smartshellInstance.exec(`docker rmi --force tsdocker_test_image`);
442
+
443
+ if (testResult.exitCode !== 0) {
444
+ throw new Error(`Tests failed for ${this.cleanTag}`);
445
+ }
446
+
447
+ logger.log('ok', `Tests passed for ${this.cleanTag}`);
448
+ } else {
449
+ logger.log('warn', `Skipping tests for ${this.cleanTag} because no test file was found at ${testFile}`);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Gets the ID of a built Docker image
455
+ */
456
+ public async getId(): Promise<string> {
457
+ const result = await smartshellInstance.exec(
458
+ 'docker inspect --type=image --format="{{.Id}}" ' + this.buildTag
459
+ );
460
+ return result.stdout.trim();
461
+ }
462
+ }
@@ -0,0 +1,91 @@
1
+ import * as plugins from './tsdocker.plugins.js';
2
+ import { logger } from './tsdocker.logging.js';
3
+ import type { IDockerRegistryOptions } from './interfaces/index.js';
4
+
5
+ const smartshellInstance = new plugins.smartshell.Smartshell({
6
+ executor: 'bash',
7
+ });
8
+
9
+ /**
10
+ * Represents a Docker registry with authentication capabilities
11
+ */
12
+ export class DockerRegistry {
13
+ public registryUrl: string;
14
+ public username: string;
15
+ public password: string;
16
+
17
+ constructor(optionsArg: IDockerRegistryOptions) {
18
+ this.registryUrl = optionsArg.registryUrl;
19
+ this.username = optionsArg.username;
20
+ this.password = optionsArg.password;
21
+ logger.log('info', `created DockerRegistry for ${this.registryUrl}`);
22
+ }
23
+
24
+ /**
25
+ * Creates a DockerRegistry instance from a pipe-delimited environment string
26
+ * Format: "registryUrl|username|password"
27
+ */
28
+ public static fromEnvString(envString: string): DockerRegistry {
29
+ const dockerRegexResultArray = envString.split('|');
30
+ if (dockerRegexResultArray.length !== 3) {
31
+ logger.log('error', 'malformed docker env var...');
32
+ throw new Error('malformed docker env var, expected format: registryUrl|username|password');
33
+ }
34
+ const registryUrl = dockerRegexResultArray[0].replace('https://', '').replace('http://', '');
35
+ const username = dockerRegexResultArray[1];
36
+ const password = dockerRegexResultArray[2];
37
+ return new DockerRegistry({
38
+ registryUrl: registryUrl,
39
+ username: username,
40
+ password: password,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Creates a DockerRegistry from environment variables
46
+ * Looks for DOCKER_REGISTRY, DOCKER_REGISTRY_USER, DOCKER_REGISTRY_PASSWORD
47
+ * Or for a specific registry: DOCKER_REGISTRY_<NAME>, etc.
48
+ */
49
+ public static fromEnv(registryName?: string): DockerRegistry | null {
50
+ const prefix = registryName ? `DOCKER_REGISTRY_${registryName.toUpperCase()}_` : 'DOCKER_REGISTRY_';
51
+
52
+ const registryUrl = process.env[`${prefix}URL`] || process.env['DOCKER_REGISTRY'];
53
+ const username = process.env[`${prefix}USER`] || process.env['DOCKER_REGISTRY_USER'];
54
+ const password = process.env[`${prefix}PASSWORD`] || process.env['DOCKER_REGISTRY_PASSWORD'];
55
+
56
+ if (!registryUrl || !username || !password) {
57
+ return null;
58
+ }
59
+
60
+ return new DockerRegistry({
61
+ registryUrl: registryUrl.replace('https://', '').replace('http://', ''),
62
+ username,
63
+ password,
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Logs in to the Docker registry
69
+ */
70
+ public async login(): Promise<void> {
71
+ if (this.registryUrl === 'docker.io') {
72
+ await smartshellInstance.exec(`docker login -u ${this.username} -p ${this.password}`);
73
+ logger.log('info', 'Logged in to standard docker hub');
74
+ } else {
75
+ await smartshellInstance.exec(`docker login -u ${this.username} -p ${this.password} ${this.registryUrl}`);
76
+ }
77
+ logger.log('ok', `docker authenticated for ${this.registryUrl}!`);
78
+ }
79
+
80
+ /**
81
+ * Logs out from the Docker registry
82
+ */
83
+ public async logout(): Promise<void> {
84
+ if (this.registryUrl === 'docker.io') {
85
+ await smartshellInstance.exec('docker logout');
86
+ } else {
87
+ await smartshellInstance.exec(`docker logout ${this.registryUrl}`);
88
+ }
89
+ logger.log('info', `logged out from ${this.registryUrl}`);
90
+ }
91
+ }
@@ -0,0 +1,83 @@
1
+ import * as plugins from './tsdocker.plugins.js';
2
+ import { logger } from './tsdocker.logging.js';
3
+ import { DockerRegistry } from './classes.dockerregistry.js';
4
+
5
+ /**
6
+ * Storage class for managing multiple Docker registries
7
+ */
8
+ export class RegistryStorage {
9
+ public objectMap = new plugins.lik.ObjectMap<DockerRegistry>();
10
+
11
+ constructor() {
12
+ // Nothing here
13
+ }
14
+
15
+ /**
16
+ * Adds a registry to the storage
17
+ */
18
+ public addRegistry(registryArg: DockerRegistry): void {
19
+ this.objectMap.add(registryArg);
20
+ }
21
+
22
+ /**
23
+ * Gets a registry by its URL
24
+ */
25
+ public getRegistryByUrl(registryUrlArg: string): DockerRegistry | undefined {
26
+ return this.objectMap.findSync((registryArg) => {
27
+ return registryArg.registryUrl === registryUrlArg;
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Gets all registries
33
+ */
34
+ public getAllRegistries(): DockerRegistry[] {
35
+ return this.objectMap.getArray();
36
+ }
37
+
38
+ /**
39
+ * Logs in to all registries
40
+ */
41
+ public async loginAll(): Promise<void> {
42
+ await this.objectMap.forEach(async (registryArg) => {
43
+ await registryArg.login();
44
+ });
45
+ logger.log('success', 'logged in successfully into all available DockerRegistries!');
46
+ }
47
+
48
+ /**
49
+ * Logs out from all registries
50
+ */
51
+ public async logoutAll(): Promise<void> {
52
+ await this.objectMap.forEach(async (registryArg) => {
53
+ await registryArg.logout();
54
+ });
55
+ logger.log('info', 'logged out from all DockerRegistries');
56
+ }
57
+
58
+ /**
59
+ * Loads registries from environment variables
60
+ * Looks for DOCKER_REGISTRY_1, DOCKER_REGISTRY_2, etc. (pipe-delimited format)
61
+ * Or individual registries like DOCKER_REGISTRY_GITLAB_URL, etc.
62
+ */
63
+ public loadFromEnv(): void {
64
+ // Check for numbered registry env vars (pipe-delimited format)
65
+ for (let i = 1; i <= 10; i++) {
66
+ const envVar = process.env[`DOCKER_REGISTRY_${i}`];
67
+ if (envVar) {
68
+ try {
69
+ const registry = DockerRegistry.fromEnvString(envVar);
70
+ this.addRegistry(registry);
71
+ } catch (err) {
72
+ logger.log('warn', `Failed to parse DOCKER_REGISTRY_${i}: ${(err as Error).message}`);
73
+ }
74
+ }
75
+ }
76
+
77
+ // Check for default registry
78
+ const defaultRegistry = DockerRegistry.fromEnv();
79
+ if (defaultRegistry) {
80
+ this.addRegistry(defaultRegistry);
81
+ }
82
+ }
83
+ }