diff --git a/packages/testcontainers/src/container-runtime/auth/auths.ts b/packages/testcontainers/src/container-runtime/auth/auths.ts index 079dc6bfd..52831c3f1 100644 --- a/packages/testcontainers/src/container-runtime/auth/auths.ts +++ b/packages/testcontainers/src/container-runtime/auth/auths.ts @@ -1,6 +1,6 @@ import { RegistryAuthLocator } from "./registry-auth-locator"; import { registryMatches } from "./registry-matches"; -import { Auth, AuthConfig, ContainerRuntimeConfig } from "./types"; +import { Auth, AuthConfig, ContainerRuntimeConfig, UsernamePasswordAuthConfig } from "./types"; export class Auths implements RegistryAuthLocator { public getName(): string { @@ -13,7 +13,7 @@ export class Auths implements RegistryAuthLocator { return undefined; } - const authConfig: Partial = { registryAddress: registry }; + const authConfig: Partial = { registryAddress: registry }; if (auth.email) { authConfig.email = auth.email; 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 113b85f19..5119393e1 100644 --- a/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts +++ b/packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts @@ -39,6 +39,24 @@ describe.sequential("CredentialProvider", () => { }); }); + it("should return the auth config for a registry using an identity token", async () => { + mockSpawnEmitsData( + 0, + JSON.stringify({ + ServerURL: "registry", + Username: "", + Secret: "dGVzdAo=", + }) + ); + + const credentials = await credentialProvider.getAuthConfig("registry", containerRuntimeConfig); + + expect(credentials).toEqual({ + registryAddress: "registry", + identityToken: "dGVzdAo=", + }); + }); + it("should default to the registry url when the server url is not returned", async () => { mockSpawnEmitsData( 0, diff --git a/packages/testcontainers/src/container-runtime/auth/credential-provider.ts b/packages/testcontainers/src/container-runtime/auth/credential-provider.ts index 145d6a9fb..5aae3626f 100644 --- a/packages/testcontainers/src/container-runtime/auth/credential-provider.ts +++ b/packages/testcontainers/src/container-runtime/auth/credential-provider.ts @@ -1,7 +1,13 @@ import { spawn } from "child_process"; import { log } from "../../common"; import { RegistryAuthLocator } from "./registry-auth-locator"; -import { AuthConfig, ContainerRuntimeConfig } from "./types"; +import { + AuthConfig, + ContainerRuntimeConfig, + CredentialProviderGetResponse, + IdentityTokenAuthConfig, + UsernamePasswordAuthConfig, +} from "./types"; export abstract class CredentialProvider implements RegistryAuthLocator { abstract getName(): string; @@ -40,12 +46,14 @@ export abstract class CredentialProvider implements RegistryAuthLocator { const response = chunks.join(""); try { - const parsedResponse = JSON.parse(response); - return resolve({ - username: parsedResponse.Username, - password: parsedResponse.Secret, - registryAddress: parsedResponse.ServerURL ?? registry, - }); + const credentialProviderResponse = JSON.parse(response) as CredentialProviderGetResponse; + + const authConfig = + credentialProviderResponse.Username === "" + ? this.parseIdentityTokenConfig(registry, credentialProviderResponse) + : this.parseUsernamePasswordConfig(registry, credentialProviderResponse); + + return resolve(authConfig); } 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")); @@ -56,4 +64,22 @@ export abstract class CredentialProvider implements RegistryAuthLocator { sink.stdin.end(); }); } + + private parseUsernamePasswordConfig( + registry: string, + config: CredentialProviderGetResponse + ): UsernamePasswordAuthConfig { + return { + username: config.Username, + password: config.Secret, + registryAddress: config.ServerURL ?? registry, + }; + } + + private parseIdentityTokenConfig(registry: string, config: CredentialProviderGetResponse): IdentityTokenAuthConfig { + return { + registryAddress: config.ServerURL ?? registry, + identityToken: config.Secret, + }; + } } diff --git a/packages/testcontainers/src/container-runtime/auth/types.ts b/packages/testcontainers/src/container-runtime/auth/types.ts index 1bb9fda4b..625fdf7ab 100644 --- a/packages/testcontainers/src/container-runtime/auth/types.ts +++ b/packages/testcontainers/src/container-runtime/auth/types.ts @@ -1,13 +1,9 @@ export type CredentialProviderGetResponse = { - ServerURL: string; + ServerURL?: string; Username: string; Secret: string; }; -export type CredentialProviderListResponse = { - [registry: string]: string; -}; - export type Auth = { auth?: string; email?: string; @@ -15,18 +11,18 @@ export type Auth = { password?: string; }; -export type AuthConfig = { +export type AuthConfig = UsernamePasswordAuthConfig | IdentityTokenAuthConfig; + +export type UsernamePasswordAuthConfig = { + registryAddress: string; username: string; password: string; - registryAddress: string; email?: string; }; -export type RegistryConfig = { - [registryAddress: string]: { - username: string; - password: string; - }; +export type IdentityTokenAuthConfig = { + registryAddress: string; + identityToken: string; }; export type ContainerRuntimeConfig = { diff --git a/packages/testcontainers/src/generic-container/generic-container-builder.ts b/packages/testcontainers/src/generic-container/generic-container-builder.ts index a084108dd..675aa907b 100644 --- a/packages/testcontainers/src/generic-container/generic-container-builder.ts +++ b/packages/testcontainers/src/generic-container/generic-container-builder.ts @@ -2,8 +2,9 @@ import type { ImageBuildOptions } from "dockerode"; import path from "path"; import { log, RandomUuid, Uuid } from "../common"; import { getAuthConfig, getContainerRuntimeClient, ImageName } from "../container-runtime"; +import { AuthConfig } from "../container-runtime/auth/types"; import { getReaper } from "../reaper/reaper"; -import { AuthConfig, BuildArgs, RegistryConfig } from "../types"; +import { BuildArgs, RegistryConfig } from "../types"; import { getDockerfileImages } from "../utils/dockerfile-parser"; import { createLabels, LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; import { ImagePullPolicy, PullPolicy } from "../utils/pull-policy"; @@ -81,6 +82,7 @@ export class GenericContainerBuilder { dockerfile: this.dockerfileName, buildargs: this.buildArgs, nocache: !this.cache, + // @ts-expect-error Dockerode types don't yet include identityToken registryconfig: registryConfig, labels, target: this.target, @@ -115,14 +117,7 @@ export class GenericContainerBuilder { ); return authConfigs - .map((authConfig) => { - return { - [authConfig.registryAddress]: { - username: authConfig.username, - password: authConfig.password, - }, - }; - }) + .map((authConfig) => ({ [authConfig.registryAddress]: authConfig })) .reduce((prev, next) => ({ ...prev, ...next }), {} as RegistryConfig); } } diff --git a/packages/testcontainers/src/types.ts b/packages/testcontainers/src/types.ts index df93bdc02..0ab163683 100644 --- a/packages/testcontainers/src/types.ts +++ b/packages/testcontainers/src/types.ts @@ -1,4 +1,5 @@ import { Readable } from "stream"; +import { AuthConfig } from "./container-runtime/auth/types"; import { ContainerCommitOptions } from "./container-runtime/clients/container/types"; export type InspectResult = { @@ -71,18 +72,8 @@ export type Labels = { [key: string]: string }; export type HostPortBindings = Array<{ hostIp: string; hostPort: number }>; export type Ports = { [containerPortWithProtocol: string]: HostPortBindings }; -export type AuthConfig = { - username: string; - password: string; - registryAddress: string; - email?: string; -}; - export type RegistryConfig = { - [registryAddress: string]: { - username: string; - password: string; - }; + [registryAddress: string]: AuthConfig; }; export type BuildArgs = { [key in string]: string };