Skip to content

Commit

Permalink
Handle error case in CredentialProvider (#425)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristianrgreco committed Dec 23, 2022
1 parent 4530500 commit a57972d
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 9 deletions.
159 changes: 159 additions & 0 deletions src/registry-auth-locator/credential-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { CredentialProvider } from "./credential-provider";
import { ChildProcess, exec, spawn } from "child_process";
import { DockerConfig } from "./types";
import { Readable, Writable } from "stream";
import EventEmitter from "events";

jest.mock("child_process");
const mockExec = jest.mocked(exec, { shallow: true });
const mockSpawn = jest.mocked(spawn, { shallow: true });

describe("CredentialProvider", () => {
let credentialProvider: CredentialProvider;
let dockerConfig: DockerConfig;

beforeEach(() => {
credentialProvider = new TestCredentialProvider("name", "credentialProviderName");
dockerConfig = {};
});

it("should return the auth config for a registry", async () => {
mockExecReturns(JSON.stringify({ registry: "username" }));
mockSpawnReturns(
0,
JSON.stringify({
ServerURL: "registry",
Username: "username",
Secret: "secret",
})
);

const credentials = await credentialProvider.getAuthConfig("registry", dockerConfig);

expect(credentials).toEqual({
registryAddress: "registry",
username: "username",
password: "secret",
});
});

it("should return auth config when registry is a substring of registry in credentials", async () => {
mockExecReturns(JSON.stringify({ "registry.example.com": "username" }));
mockSpawnReturns(
0,
JSON.stringify({
ServerURL: "registry.example.com",
Username: "username",
Secret: "secret",
})
);

expect(await credentialProvider.getAuthConfig("registry", dockerConfig)).toEqual({
registryAddress: "registry.example.com",
username: "username",
password: "secret",
});
});

it("should return undefined when no auth config found for registry", async () => {
mockExecReturns(JSON.stringify({ registry2: "username" }));

const credentials = await credentialProvider.getAuthConfig("registry1", dockerConfig);

expect(credentials).toBeUndefined();
});

it("should return undefined when provider name not provided", async () => {
const credentialProvider = new TestCredentialProvider("name", undefined!);

expect(await credentialProvider.getAuthConfig("registry", dockerConfig)).toBeUndefined();
});

it("should throw when list credentials fails", async () => {
mockExecThrows();

await expect(() => credentialProvider.getAuthConfig("registry", dockerConfig)).rejects.toThrow(
"An error occurred listing credentials"
);
});

it("should throw when list credentials output cannot be parsed", async () => {
mockExecReturns("CANNOT_PARSE");

await expect(() => credentialProvider.getAuthConfig("registry", dockerConfig)).rejects.toThrow(
"Unexpected response from Docker credential provider LIST command"
);
});

it("should throw when get credentials fails", async () => {
mockExecReturns(JSON.stringify({ registry: "username" }));
mockSpawnReturns(
1,
JSON.stringify({
ServerURL: "registry",
Username: "username",
Secret: "secret",
})
);

await expect(() => credentialProvider.getAuthConfig("registry", dockerConfig)).rejects.toThrow(
"An error occurred getting a credential"
);
});

it("should throw when get credentials output cannot be parsed", async () => {
mockExecReturns(JSON.stringify({ registry: "username" }));
mockSpawnReturns(0, "CANNOT_PARSE");

await expect(() => credentialProvider.getAuthConfig("registry", dockerConfig)).rejects.toThrow(
"Unexpected response from Docker credential provider GET command"
);
});
});

function mockExecReturns(stdout: string) {
mockExec.mockImplementationOnce((command, callback) => {
// @ts-ignore
return callback(null, stdout);
});
}

function mockExecThrows() {
mockExec.mockImplementationOnce((command, callback) => {
// @ts-ignore
return callback("An error occurred");
});
}

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

sink.stdout = new Readable({
read() {
// no-op
},
});

sink.stdin = new Writable({
write() {
sink.stdout!.emit("data", stdout);
sink.emit("close", exitCode);
},
});

mockSpawn.mockReturnValueOnce(sink);
}

class TestCredentialProvider extends CredentialProvider {
constructor(private readonly name: string, private readonly credentialProviderName: string) {
super();
}

getCredentialProviderName(registry: string, dockerConfig: DockerConfig): string | undefined {
return this.credentialProviderName;
}

getName(): string {
return this.name;
}
}
19 changes: 10 additions & 9 deletions src/registry-auth-locator/credential-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,39 +40,40 @@ export abstract class CredentialProvider implements RegistryAuthLocator {
return new Promise((resolve, reject) => {
exec(`${providerName} list`, (err, stdout) => {
if (err) {
log.error("An error occurred listing credentials");
return reject(err);
log.error(`An error occurred listing credentials: ${err}`);
return reject(new Error("An error occurred listing credentials"));
}
try {
const response = JSON.parse(stdout);
resolve(response);
return resolve(response);
} catch (e) {
log.error(`Unexpected response from Docker credential provider LIST command: "${stdout}"`);
return reject(new Error("Unexpected response from Docker credential provider LIST command"));
}
});
});
}

private runCredentialProvider(registry: string, providerName: string): Promise<CredentialProviderGetResponse> {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
const sink = spawn(providerName, ["get"]);

const chunks: string[] = [];
sink.stdout.on("data", (chunk) => chunks.push(chunk));

sink.on("close", (code) => {
if (code === 0) {
log.debug(`Docker credential provider exited with code: ${code}`);
} else {
log.warn(`Docker credential provider exited with code: ${code}`);
if (code !== 0) {
log.error(`An error occurred getting a credential: ${code}`);
return reject(new Error("An error occurred getting a credential"));
}

const response = chunks.join("");
try {
const parsedResponse = JSON.parse(response);
resolve(parsedResponse);
return resolve(parsedResponse);
} 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

0 comments on commit a57972d

Please sign in to comment.