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
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer adding the dev dependency to the project root, just like all the E2E test dependencies. Doing so can reduce the number dependencies user needs to download.

"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