From 8528246bc391dd79e7e6ae5967ed01f0a549456e Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Thu, 2 Oct 2025 10:11:54 +0100 Subject: [PATCH] Return empty registry credentials when none found --- .../auth/credential-provider.test.ts | 35 +++++++++++++++---- .../auth/credential-provider.ts | 27 +++++++------- .../container-runtime/auth/get-auth-config.ts | 2 +- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts b/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts index 9a2c553bf..113b85f19 100644 --- a/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts +++ b/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts @@ -21,7 +21,7 @@ describe.sequential("CredentialProvider", () => { }); it("should return the auth config for a registry", async () => { - mockSpawnReturns( + mockSpawnEmitsData( 0, JSON.stringify({ ServerURL: "registry", @@ -40,7 +40,7 @@ describe.sequential("CredentialProvider", () => { }); it("should default to the registry url when the server url is not returned", async () => { - mockSpawnReturns( + mockSpawnEmitsData( 0, JSON.stringify({ Username: "username", @@ -61,8 +61,8 @@ describe.sequential("CredentialProvider", () => { expect(await credentialProvider.getAuthConfig("registry", containerRuntimeConfig)).toBeUndefined(); }); - it("should throw when get credentials fails", async () => { - mockSpawnReturns( + it("should return undefined when get credentials fails because we lookup optimistically", async () => { + mockSpawnEmitsData( 1, JSON.stringify({ ServerURL: "registry", @@ -71,13 +71,19 @@ describe.sequential("CredentialProvider", () => { }) ); + expect(await credentialProvider.getAuthConfig("registry", containerRuntimeConfig)).toBeUndefined(); + }); + + it("should throw when credential provider emits error", async () => { + mockSpawnEmitsError("ERROR"); + await expect(() => credentialProvider.getAuthConfig("registry", containerRuntimeConfig)).rejects.toThrow( - "An error occurred getting a credential" + "Error from Docker credential provider: Error: ERROR" ); }); it("should throw when get credentials output cannot be parsed", async () => { - mockSpawnReturns(0, "CANNOT_PARSE"); + mockSpawnEmitsData(0, "CANNOT_PARSE"); await expect(() => credentialProvider.getAuthConfig("registry", containerRuntimeConfig)).rejects.toThrow( "Unexpected response from Docker credential provider GET command" @@ -85,7 +91,7 @@ describe.sequential("CredentialProvider", () => { }); }); -function mockSpawnReturns(exitCode: number, stdout: string) { +function mockSpawnEmitsData(exitCode: number, stdout: string) { const sink = new EventEmitter() as ChildProcess; sink.stdout = new Readable({ @@ -104,6 +110,21 @@ function mockSpawnReturns(exitCode: number, stdout: string) { mockSpawn.mockReturnValueOnce(sink); } +function mockSpawnEmitsError(message: string) { + const sink = new EventEmitter() as ChildProcess; + + sink.kill = () => true; + sink.stdout = new Readable({ read() {} }); + sink.stdin = new Writable({ + write(_chunk, _enc, cb) { + sink.emit("error", new Error(message)); + cb?.(); + }, + }); + + mockSpawn.mockReturnValueOnce(sink); +} + class TestCredentialProvider extends CredentialProvider { constructor( private readonly name: string, diff --git a/packages/testcontainers/src/container-runtime/auth/credential-provider.ts b/packages/testcontainers/src/container-runtime/auth/credential-provider.ts index 991baeece..145d6a9fb 100644 --- a/packages/testcontainers/src/container-runtime/auth/credential-provider.ts +++ b/packages/testcontainers/src/container-runtime/auth/credential-provider.ts @@ -1,7 +1,7 @@ import { spawn } from "child_process"; import { log } from "../../common"; import { RegistryAuthLocator } from "./registry-auth-locator"; -import { AuthConfig, ContainerRuntimeConfig, CredentialProviderGetResponse } from "./types"; +import { AuthConfig, ContainerRuntimeConfig } from "./types"; export abstract class CredentialProvider implements RegistryAuthLocator { abstract getName(): string; @@ -17,32 +17,35 @@ export abstract class CredentialProvider implements RegistryAuthLocator { const programName = `docker-credential-${credentialProviderName}`; log.debug(`Executing Docker credential provider "${programName}"`); - const response = await this.runCredentialProvider(registry, programName); - - return { - username: response.Username, - password: response.Secret, - registryAddress: response.ServerURL ?? registry, - }; + return await this.runCredentialProvider(registry, programName); } - private runCredentialProvider(registry: string, providerName: string): Promise { + private runCredentialProvider(registry: string, providerName: string): Promise { return new Promise((resolve, reject) => { const sink = spawn(providerName, ["get"]); const chunks: string[] = []; sink.stdout.on("data", (chunk) => chunks.push(chunk)); + sink.on("error", (err) => { + log.error(`Error from Docker credential provider: ${err}`); + sink.kill("SIGKILL"); + reject(new Error(`Error from Docker credential provider: ${err}`)); + }); + sink.on("close", (code) => { if (code !== 0) { - log.error(`An error occurred getting a credential: ${code}`); - return reject(new Error("An error occurred getting a credential")); + return resolve(undefined); } const response = chunks.join(""); try { const parsedResponse = JSON.parse(response); - return resolve(parsedResponse); + return resolve({ + username: parsedResponse.Username, + password: parsedResponse.Secret, + registryAddress: parsedResponse.ServerURL ?? registry, + }); } catch (e) { log.error(`Unexpected response from Docker credential provider GET command: "${response}"`); return reject(new Error("Unexpected response from Docker credential provider GET command")); diff --git a/packages/testcontainers/src/container-runtime/auth/get-auth-config.ts b/packages/testcontainers/src/container-runtime/auth/get-auth-config.ts index b89ca56b6..511e1d534 100644 --- a/packages/testcontainers/src/container-runtime/auth/get-auth-config.ts +++ b/packages/testcontainers/src/container-runtime/auth/get-auth-config.ts @@ -55,7 +55,7 @@ export const getAuthConfig = async (registry: string): Promise