Skip to content

Commit

Permalink
feat(credential-provider-imds): update httpGet to accept options.meth…
Browse files Browse the repository at this point in the history
…od (#1353)
  • Loading branch information
trivikr authored Jul 10, 2020
1 parent 089585d commit db2651c
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 161 deletions.
1 change: 1 addition & 0 deletions packages/credential-provider-imds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@types/jest": "^26.0.4",
"@types/node": "^10.0.0",
"jest": "^26.1.0",
"nock": "^13.0.2",
"typescript": "~3.8.3"
},
"types": "./dist/cjs/index.d.ts"
Expand Down
54 changes: 29 additions & 25 deletions packages/credential-provider-imds/src/fromContainerMetadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import {
ENV_CMDS_RELATIVE_URI,
fromContainerMetadata
} from "./fromContainerMetadata";
import { httpGet } from "./remoteProvider/httpGet";
import { httpRequest } from "./remoteProvider/httpRequest";
import {
fromImdsCredentials,
ImdsCredentials
} from "./remoteProvider/ImdsCredentials";

const mockHttpGet = <any>httpGet;
jest.mock("./remoteProvider/httpGet", () => ({ httpGet: jest.fn() }));
const mockHttpRequest = <any>httpRequest;
jest.mock("./remoteProvider/httpRequest", () => ({ httpRequest: jest.fn() }));

const relativeUri = process.env[ENV_CMDS_RELATIVE_URI];
const fullUri = process.env[ENV_CMDS_FULL_URI];
const authToken = process.env[ENV_CMDS_AUTH_TOKEN];

beforeEach(() => {
mockHttpGet.mockReset();
mockHttpRequest.mockReset();
delete process.env[ENV_CMDS_RELATIVE_URI];
delete process.env[ENV_CMDS_FULL_URI];
delete process.env[ENV_CMDS_AUTH_TOKEN];
Expand Down Expand Up @@ -53,12 +53,12 @@ describe("fromContainerMetadata", () => {
const token = "Basic abcd";
process.env[ENV_CMDS_FULL_URI] = "http://localhost:8080/path";
process.env[ENV_CMDS_AUTH_TOKEN] = token;
mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds)));

await fromContainerMetadata()();

expect(mockHttpGet.mock.calls.length).toBe(1);
const [options = {}] = mockHttpGet.mock.calls[0];
expect(mockHttpRequest.mock.calls.length).toBe(1);
const [options = {}] = mockHttpRequest.mock.calls[0];
expect(options.headers).toMatchObject({
Authorization: token
});
Expand All @@ -70,7 +70,7 @@ describe("fromContainerMetadata", () => {
});

it("should resolve credentials by fetching them from the container metadata service", async () => {
mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds)));

expect(await fromContainerMetadata()()).toEqual(
fromImdsCredentials(creds)
Expand All @@ -80,38 +80,42 @@ describe("fromContainerMetadata", () => {
it("should retry the fetching operation up to maxRetries times", async () => {
const maxRetries = 5;
for (let i = 0; i < maxRetries - 1; i++) {
mockHttpGet.mockReturnValueOnce(Promise.reject("No!"));
mockHttpRequest.mockReturnValueOnce(Promise.reject("No!"));
}
mockHttpGet.mockReturnValueOnce(Promise.resolve(JSON.stringify(creds)));
mockHttpRequest.mockReturnValueOnce(
Promise.resolve(JSON.stringify(creds))
);

expect(await fromContainerMetadata({ maxRetries })()).toEqual(
fromImdsCredentials(creds)
);
expect(mockHttpGet.mock.calls.length).toEqual(maxRetries);
expect(mockHttpRequest.mock.calls.length).toEqual(maxRetries);
});

it("should retry responses that receive invalid response values", async () => {
for (let key of Object.keys(creds)) {
const invalidCreds: any = { ...creds };
delete invalidCreds[key];
mockHttpGet.mockReturnValueOnce(
mockHttpRequest.mockReturnValueOnce(
Promise.resolve(JSON.stringify(invalidCreds))
);
}
mockHttpGet.mockReturnValueOnce(Promise.resolve(JSON.stringify(creds)));
mockHttpRequest.mockReturnValueOnce(
Promise.resolve(JSON.stringify(creds))
);

await fromContainerMetadata({ maxRetries: 100 })();
expect(mockHttpGet.mock.calls.length).toEqual(
expect(mockHttpRequest.mock.calls.length).toEqual(
Object.keys(creds).length + 1
);
});

it("should pass relevant configuration to httpGet", async () => {
it("should pass relevant configuration to httpRequest", async () => {
const timeout = Math.ceil(Math.random() * 1000);
mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
await fromContainerMetadata({ timeout })();
expect(mockHttpGet.mock.calls.length).toEqual(1);
expect(mockHttpGet.mock.calls[0][0]).toEqual({
expect(mockHttpRequest.mock.calls.length).toEqual(1);
expect(mockHttpRequest.mock.calls[0][0]).toEqual({
hostname: "169.254.170.2",
path: process.env[ENV_CMDS_RELATIVE_URI],
timeout
Expand All @@ -120,20 +124,20 @@ describe("fromContainerMetadata", () => {
});

describe(ENV_CMDS_FULL_URI, () => {
it("should pass relevant configuration to httpGet", async () => {
it("should pass relevant configuration to httpRequest", async () => {
process.env[ENV_CMDS_FULL_URI] = "http://localhost:8080/path";

const timeout = Math.ceil(Math.random() * 1000);
mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
await fromContainerMetadata({ timeout })();
expect(mockHttpGet.mock.calls.length).toEqual(1);
expect(mockHttpRequest.mock.calls.length).toEqual(1);
const {
protocol,
hostname,
path,
port,
timeout: actualTimeout
} = mockHttpGet.mock.calls[0][0];
} = mockHttpRequest.mock.calls[0][0];
expect(protocol).toBe("http:");
expect(hostname).toBe("localhost");
expect(path).toBe("/path");
Expand All @@ -146,10 +150,10 @@ describe("fromContainerMetadata", () => {
process.env[ENV_CMDS_FULL_URI] = "http://localhost:8080/path";

const timeout = Math.ceil(Math.random() * 1000);
mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
await fromContainerMetadata({ timeout })();
expect(mockHttpGet.mock.calls.length).toEqual(1);
expect(mockHttpGet.mock.calls[0][0]).toEqual({
expect(mockHttpRequest.mock.calls.length).toEqual(1);
expect(mockHttpRequest.mock.calls[0][0]).toEqual({
hostname: "169.254.170.2",
path: "foo",
timeout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
RemoteProviderInit,
providerConfigFromInit
} from "./remoteProvider/RemoteProviderInit";
import { httpGet } from "./remoteProvider/httpGet";
import { httpRequest } from "./remoteProvider/httpRequest";
import {
fromImdsCredentials,
isImdsCredentials
Expand Down Expand Up @@ -53,7 +53,7 @@ function requestFromEcsImds(
options.headers = headers;
}

return httpGet({
return httpRequest({
...options,
timeout
}).then(buffer => buffer.toString());
Expand Down
46 changes: 23 additions & 23 deletions packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fromInstanceMetadata } from "./fromInstanceMetadata";
import { httpGet } from "./remoteProvider/httpGet";
import { httpRequest } from "./remoteProvider/httpRequest";
import {
fromImdsCredentials,
isImdsCredentials
Expand All @@ -8,7 +8,7 @@ import { providerConfigFromInit } from "./remoteProvider/RemoteProviderInit";
import { retry } from "./remoteProvider/retry";
import { ProviderError } from "@aws-sdk/property-provider";

jest.mock("./remoteProvider/httpGet");
jest.mock("./remoteProvider/httpRequest");
jest.mock("./remoteProvider/ImdsCredentials");
jest.mock("./remoteProvider/retry");
jest.mock("./remoteProvider/RemoteProviderInit");
Expand All @@ -18,7 +18,7 @@ describe("fromInstanceMetadata", () => {
const mockMaxRetries = 3;
const mockProfile = "foo";

const mockHttpGetOptions = {
const mockHttpRequestOptions = {
host: "169.254.169.254",
path: "/latest/meta-data/iam/security-credentials/",
timeout: mockTimeout
Expand Down Expand Up @@ -51,36 +51,36 @@ describe("fromInstanceMetadata", () => {
});

it("gets profile name from IMDS, and passes profile name to fetch credentials", async () => {
(httpGet as jest.Mock)
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

(retry as jest.Mock).mockImplementation((fn: any) => fn());
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);

await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
expect(httpGet).toHaveBeenCalledTimes(2);
expect(httpGet).toHaveBeenNthCalledWith(1, mockHttpGetOptions);
expect(httpGet).toHaveBeenNthCalledWith(2, {
...mockHttpGetOptions,
path: `${mockHttpGetOptions.path}${mockProfile}`
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenNthCalledWith(1, mockHttpRequestOptions);
expect(httpRequest).toHaveBeenNthCalledWith(2, {
...mockHttpRequestOptions,
path: `${mockHttpRequestOptions.path}${mockProfile}`
});
});

it("trims profile returned name from IMDS", async () => {
(httpGet as jest.Mock)
(httpRequest as jest.Mock)
.mockResolvedValueOnce(" " + mockProfile + " ")
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

(retry as jest.Mock).mockImplementation((fn: any) => fn());
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);

await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
expect(httpGet).toHaveBeenCalledTimes(2);
expect(httpGet).toHaveBeenNthCalledWith(1, mockHttpGetOptions);
expect(httpGet).toHaveBeenNthCalledWith(2, {
...mockHttpGetOptions,
path: `${mockHttpGetOptions.path}${mockProfile}`
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenNthCalledWith(1, mockHttpRequestOptions);
expect(httpRequest).toHaveBeenNthCalledWith(2, {
...mockHttpRequestOptions,
path: `${mockHttpRequestOptions.path}${mockProfile}`
});
});

Expand Down Expand Up @@ -117,7 +117,7 @@ describe("fromInstanceMetadata", () => {
});

it("throws ProviderError if credentials returned are incorrect", async () => {
(httpGet as jest.Mock)
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));

Expand All @@ -130,37 +130,37 @@ describe("fromInstanceMetadata", () => {
)
);
expect(retry).toHaveBeenCalledTimes(2);
expect(httpGet).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(isImdsCredentials).toHaveBeenCalledTimes(1);
expect(isImdsCredentials).toHaveBeenCalledWith(mockImdsCreds);
expect(fromImdsCredentials).not.toHaveBeenCalled();
});

it("throws Error if requestFromEc2Imds for profile fails", async () => {
const mockError = new Error("profile not found");
(httpGet as jest.Mock).mockRejectedValueOnce(mockError);
(httpRequest as jest.Mock).mockRejectedValueOnce(mockError);
(retry as jest.Mock).mockImplementation((fn: any) => fn());

await expect(fromInstanceMetadata()()).rejects.toEqual(mockError);
expect(retry).toHaveBeenCalledTimes(1);
expect(httpGet).toHaveBeenCalledTimes(1);
expect(httpRequest).toHaveBeenCalledTimes(1);
});

it("throws Error if requestFromEc2Imds for credentials fails", async () => {
const mockError = new Error("creds not found");
(httpGet as jest.Mock)
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockProfile)
.mockRejectedValueOnce(mockError);
(retry as jest.Mock).mockImplementation((fn: any) => fn());

await expect(fromInstanceMetadata()()).rejects.toEqual(mockError);
expect(retry).toHaveBeenCalledTimes(2);
expect(httpGet).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(fromImdsCredentials).not.toHaveBeenCalled();
});

it("throws SyntaxError if requestFromEc2Imds returns unparseable creds", async () => {
(httpGet as jest.Mock)
(httpRequest as jest.Mock)
.mockResolvedValueOnce(mockProfile)
.mockResolvedValueOnce(".");
(retry as jest.Mock).mockImplementation((fn: any) => fn());
Expand All @@ -169,7 +169,7 @@ describe("fromInstanceMetadata", () => {
new SyntaxError("Unexpected token . in JSON at position 0")
);
expect(retry).toHaveBeenCalledTimes(2);
expect(httpGet).toHaveBeenCalledTimes(2);
expect(httpRequest).toHaveBeenCalledTimes(2);
expect(fromImdsCredentials).not.toHaveBeenCalled();
});
});
33 changes: 15 additions & 18 deletions packages/credential-provider-imds/src/fromInstanceMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import {
RemoteProviderInit,
providerConfigFromInit
} from "./remoteProvider/RemoteProviderInit";
import { httpGet } from "./remoteProvider/httpGet";
import { httpRequest } from "./remoteProvider/httpRequest";
import {
fromImdsCredentials,
isImdsCredentials
} from "./remoteProvider/ImdsCredentials";
import { retry } from "./remoteProvider/retry";
import { ProviderError } from "@aws-sdk/property-provider";

const IMDS_IP = "169.254.169.254";
const IMDS_PATH = "/latest/meta-data/iam/security-credentials/";

/**
* Creates a credential provider that will source credentials from the EC2
* Instance Metadata Service
Expand All @@ -22,14 +25,23 @@ export const fromInstanceMetadata = (
return async () => {
const profile = (
await retry<string>(
async () => await requestFromEc2Imds(timeout),
async () =>
(
await httpRequest({ host: IMDS_IP, path: IMDS_PATH, timeout })
).toString(),
maxRetries
)
).trim();

return retry(async () => {
const credsResponse = JSON.parse(
await requestFromEc2Imds(timeout, profile)
(
await httpRequest({
host: IMDS_IP,
path: IMDS_PATH + profile,
timeout
})
).toString()
);
if (!isImdsCredentials(credsResponse)) {
throw new ProviderError(
Expand All @@ -41,18 +53,3 @@ export const fromInstanceMetadata = (
}, maxRetries);
};
};

const IMDS_IP = "169.254.169.254";
const IMDS_PATH = "latest/meta-data/iam/security-credentials";

const requestFromEc2Imds = async (
timeout: number,
path?: string
): Promise<string> => {
const buffer = await httpGet({
host: IMDS_IP,
path: `/${IMDS_PATH}/${path ? path : ""}`,
timeout
});
return buffer.toString();
};
Loading

0 comments on commit db2651c

Please sign in to comment.