Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe.sequential("CredentialProvider", () => {
});

it("should return the auth config for a registry", async () => {
mockSpawnReturns(
mockSpawnEmitsData(
0,
JSON.stringify({
ServerURL: "registry",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -71,21 +71,27 @@ 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"
);
});
});

function mockSpawnReturns(exitCode: number, stdout: string) {
function mockSpawnEmitsData(exitCode: number, stdout: string) {
const sink = new EventEmitter() as ChildProcess;

sink.stdout = new Readable({
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<CredentialProviderGetResponse> {
private runCredentialProvider(registry: string, providerName: string): Promise<AuthConfig | undefined> {
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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const getAuthConfig = async (registry: string): Promise<AuthConfig | unde
}
}

log.debug(`No registry auth locator found for registry "${registry}"`);
log.debug(`No auth config found for registry "${registry}"`);
authsCache.set(registry, undefined);
return undefined;
};