Skip to content

Commit 3f1cbb5

Browse files
Add support for identity token authentication (#1179)
1 parent c5c54ad commit 3f1cbb5

File tree

6 files changed

+67
-41
lines changed

6 files changed

+67
-41
lines changed

packages/testcontainers/src/container-runtime/auth/auths.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { RegistryAuthLocator } from "./registry-auth-locator";
22
import { registryMatches } from "./registry-matches";
3-
import { Auth, AuthConfig, ContainerRuntimeConfig } from "./types";
3+
import { Auth, AuthConfig, ContainerRuntimeConfig, UsernamePasswordAuthConfig } from "./types";
44

55
export class Auths implements RegistryAuthLocator {
66
public getName(): string {
@@ -13,7 +13,7 @@ export class Auths implements RegistryAuthLocator {
1313
return undefined;
1414
}
1515

16-
const authConfig: Partial<AuthConfig> = { registryAddress: registry };
16+
const authConfig: Partial<UsernamePasswordAuthConfig> = { registryAddress: registry };
1717

1818
if (auth.email) {
1919
authConfig.email = auth.email;

packages/testcontainers/src/container-runtime/auth/credential-provider.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ describe.sequential("CredentialProvider", () => {
3939
});
4040
});
4141

42+
it("should return the auth config for a registry using an identity token", async () => {
43+
mockSpawnEmitsData(
44+
0,
45+
JSON.stringify({
46+
ServerURL: "registry",
47+
Username: "<token>",
48+
Secret: "dGVzdAo=",
49+
})
50+
);
51+
52+
const credentials = await credentialProvider.getAuthConfig("registry", containerRuntimeConfig);
53+
54+
expect(credentials).toEqual({
55+
registryAddress: "registry",
56+
identityToken: "dGVzdAo=",
57+
});
58+
});
59+
4260
it("should default to the registry url when the server url is not returned", async () => {
4361
mockSpawnEmitsData(
4462
0,

packages/testcontainers/src/container-runtime/auth/credential-provider.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { spawn } from "child_process";
22
import { log } from "../../common";
33
import { RegistryAuthLocator } from "./registry-auth-locator";
4-
import { AuthConfig, ContainerRuntimeConfig } from "./types";
4+
import {
5+
AuthConfig,
6+
ContainerRuntimeConfig,
7+
CredentialProviderGetResponse,
8+
IdentityTokenAuthConfig,
9+
UsernamePasswordAuthConfig,
10+
} from "./types";
511

612
export abstract class CredentialProvider implements RegistryAuthLocator {
713
abstract getName(): string;
@@ -40,12 +46,14 @@ export abstract class CredentialProvider implements RegistryAuthLocator {
4046

4147
const response = chunks.join("");
4248
try {
43-
const parsedResponse = JSON.parse(response);
44-
return resolve({
45-
username: parsedResponse.Username,
46-
password: parsedResponse.Secret,
47-
registryAddress: parsedResponse.ServerURL ?? registry,
48-
});
49+
const credentialProviderResponse = JSON.parse(response) as CredentialProviderGetResponse;
50+
51+
const authConfig =
52+
credentialProviderResponse.Username === "<token>"
53+
? this.parseIdentityTokenConfig(registry, credentialProviderResponse)
54+
: this.parseUsernamePasswordConfig(registry, credentialProviderResponse);
55+
56+
return resolve(authConfig);
4957
} catch (e) {
5058
log.error(`Unexpected response from Docker credential provider GET command: "${response}"`);
5159
return reject(new Error("Unexpected response from Docker credential provider GET command"));
@@ -56,4 +64,22 @@ export abstract class CredentialProvider implements RegistryAuthLocator {
5664
sink.stdin.end();
5765
});
5866
}
67+
68+
private parseUsernamePasswordConfig(
69+
registry: string,
70+
config: CredentialProviderGetResponse
71+
): UsernamePasswordAuthConfig {
72+
return {
73+
username: config.Username,
74+
password: config.Secret,
75+
registryAddress: config.ServerURL ?? registry,
76+
};
77+
}
78+
79+
private parseIdentityTokenConfig(registry: string, config: CredentialProviderGetResponse): IdentityTokenAuthConfig {
80+
return {
81+
registryAddress: config.ServerURL ?? registry,
82+
identityToken: config.Secret,
83+
};
84+
}
5985
}

packages/testcontainers/src/container-runtime/auth/types.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,28 @@
11
export type CredentialProviderGetResponse = {
2-
ServerURL: string;
2+
ServerURL?: string;
33
Username: string;
44
Secret: string;
55
};
66

7-
export type CredentialProviderListResponse = {
8-
[registry: string]: string;
9-
};
10-
117
export type Auth = {
128
auth?: string;
139
email?: string;
1410
username?: string;
1511
password?: string;
1612
};
1713

18-
export type AuthConfig = {
14+
export type AuthConfig = UsernamePasswordAuthConfig | IdentityTokenAuthConfig;
15+
16+
export type UsernamePasswordAuthConfig = {
17+
registryAddress: string;
1918
username: string;
2019
password: string;
21-
registryAddress: string;
2220
email?: string;
2321
};
2422

25-
export type RegistryConfig = {
26-
[registryAddress: string]: {
27-
username: string;
28-
password: string;
29-
};
23+
export type IdentityTokenAuthConfig = {
24+
registryAddress: string;
25+
identityToken: string;
3026
};
3127

3228
export type ContainerRuntimeConfig = {

packages/testcontainers/src/generic-container/generic-container-builder.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { ImageBuildOptions } from "dockerode";
22
import path from "path";
33
import { log, RandomUuid, Uuid } from "../common";
44
import { getAuthConfig, getContainerRuntimeClient, ImageName } from "../container-runtime";
5+
import { AuthConfig } from "../container-runtime/auth/types";
56
import { getReaper } from "../reaper/reaper";
6-
import { AuthConfig, BuildArgs, RegistryConfig } from "../types";
7+
import { BuildArgs, RegistryConfig } from "../types";
78
import { getDockerfileImages } from "../utils/dockerfile-parser";
89
import { createLabels, LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels";
910
import { ImagePullPolicy, PullPolicy } from "../utils/pull-policy";
@@ -81,6 +82,7 @@ export class GenericContainerBuilder {
8182
dockerfile: this.dockerfileName,
8283
buildargs: this.buildArgs,
8384
nocache: !this.cache,
85+
// @ts-expect-error Dockerode types don't yet include identityToken
8486
registryconfig: registryConfig,
8587
labels,
8688
target: this.target,
@@ -115,14 +117,7 @@ export class GenericContainerBuilder {
115117
);
116118

117119
return authConfigs
118-
.map((authConfig) => {
119-
return {
120-
[authConfig.registryAddress]: {
121-
username: authConfig.username,
122-
password: authConfig.password,
123-
},
124-
};
125-
})
120+
.map((authConfig) => ({ [authConfig.registryAddress]: authConfig }))
126121
.reduce((prev, next) => ({ ...prev, ...next }), {} as RegistryConfig);
127122
}
128123
}

packages/testcontainers/src/types.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Readable } from "stream";
2+
import { AuthConfig } from "./container-runtime/auth/types";
23
import { ContainerCommitOptions } from "./container-runtime/clients/container/types";
34

45
export type InspectResult = {
@@ -71,18 +72,8 @@ export type Labels = { [key: string]: string };
7172
export type HostPortBindings = Array<{ hostIp: string; hostPort: number }>;
7273
export type Ports = { [containerPortWithProtocol: string]: HostPortBindings };
7374

74-
export type AuthConfig = {
75-
username: string;
76-
password: string;
77-
registryAddress: string;
78-
email?: string;
79-
};
80-
8175
export type RegistryConfig = {
82-
[registryAddress: string]: {
83-
username: string;
84-
password: string;
85-
};
76+
[registryAddress: string]: AuthConfig;
8677
};
8778

8879
export type BuildArgs = { [key in string]: string };

0 commit comments

Comments
 (0)