Skip to content

Commit d7fa8a7

Browse files
authored
test(credential-provider-imds): cases for fromInstanceMetadata (#1318)
1 parent 8b1afc6 commit d7fa8a7

File tree

2 files changed

+160
-81
lines changed

2 files changed

+160
-81
lines changed

Diff for: packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts

+149-74
Original file line numberDiff line numberDiff line change
@@ -2,99 +2,174 @@ import { fromInstanceMetadata } from "./fromInstanceMetadata";
22
import { httpGet } from "./remoteProvider/httpGet";
33
import {
44
fromImdsCredentials,
5-
ImdsCredentials
5+
isImdsCredentials
66
} from "./remoteProvider/ImdsCredentials";
7+
import { providerConfigFromInit } from "./remoteProvider/RemoteProviderInit";
8+
import { retry } from "./remoteProvider/retry";
9+
import { ProviderError } from "@aws-sdk/property-provider";
710

8-
const mockHttpGet = <any>httpGet;
9-
jest.mock("./remoteProvider/httpGet", () => ({ httpGet: jest.fn() }));
10-
11-
beforeEach(() => {
12-
mockHttpGet.mockReset();
13-
});
11+
jest.mock("./remoteProvider/httpGet");
12+
jest.mock("./remoteProvider/ImdsCredentials");
13+
jest.mock("./remoteProvider/retry");
14+
jest.mock("./remoteProvider/RemoteProviderInit");
1415

1516
describe("fromInstanceMetadata", () => {
16-
const creds: ImdsCredentials = Object.freeze({
17+
const mockTimeout = 1000;
18+
const mockMaxRetries = 3;
19+
const mockProfile = "foo";
20+
21+
const mockHttpGetOptions = {
22+
host: "169.254.169.254",
23+
path: "/latest/meta-data/iam/security-credentials/",
24+
timeout: mockTimeout
25+
};
26+
27+
const mockImdsCreds = Object.freeze({
1728
AccessKeyId: "foo",
1829
SecretAccessKey: "bar",
1930
Token: "baz",
2031
Expiration: new Date().toISOString()
2132
});
2233

23-
it("should resolve credentials by fetching them from the container metadata service", async () => {
24-
mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
25-
expect(await fromInstanceMetadata()()).toEqual(fromImdsCredentials(creds));
34+
const mockCreds = Object.freeze({
35+
accessKeyId: mockImdsCreds.AccessKeyId,
36+
secretAccessKey: mockImdsCreds.SecretAccessKey,
37+
sessionToken: mockImdsCreds.Token,
38+
expiration: new Date(mockImdsCreds.Expiration)
2639
});
2740

28-
it("should retry the fetching operation up to maxRetries times", async () => {
29-
const maxRetries = 5;
30-
mockHttpGet.mockReturnValueOnce(Promise.resolve("foo"));
31-
for (let i = 0; i < maxRetries - 1; i++) {
32-
mockHttpGet.mockReturnValueOnce(Promise.reject("No!"));
33-
}
34-
mockHttpGet.mockReturnValueOnce(Promise.resolve(JSON.stringify(creds)));
35-
36-
expect(await fromInstanceMetadata({ maxRetries })()).toEqual(
37-
fromImdsCredentials(creds)
38-
);
39-
expect(mockHttpGet.mock.calls.length).toEqual(maxRetries + 1);
41+
beforeEach(() => {
42+
((isImdsCredentials as unknown) as jest.Mock).mockReturnValue(true);
43+
(providerConfigFromInit as jest.Mock).mockReturnValue({
44+
timeout: mockTimeout,
45+
maxRetries: mockMaxRetries
46+
});
4047
});
4148

42-
it("should retry responses that receive invalid response values", async () => {
43-
mockHttpGet.mockReturnValueOnce(Promise.resolve("foo"));
44-
for (let key of Object.keys(creds)) {
45-
const invalidCreds: any = { ...creds };
46-
delete invalidCreds[key];
47-
mockHttpGet.mockReturnValueOnce(
48-
Promise.resolve(JSON.stringify(invalidCreds))
49-
);
50-
}
51-
mockHttpGet.mockReturnValueOnce(Promise.resolve(JSON.stringify(creds)));
52-
53-
await fromInstanceMetadata({ maxRetries: 100 })();
54-
expect(mockHttpGet.mock.calls.length).toEqual(
55-
Object.keys(creds).length + 2
56-
);
49+
afterEach(() => {
50+
jest.resetAllMocks();
5751
});
5852

59-
it("should pass relevant configuration to httpGet", async () => {
60-
const timeout = Math.ceil(Math.random() * 1000);
61-
const profile = "foo-profile";
62-
mockHttpGet.mockReturnValueOnce(Promise.resolve(profile));
63-
mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds)));
64-
await fromInstanceMetadata({ timeout })();
65-
expect(mockHttpGet.mock.calls.length).toEqual(2);
66-
expect(mockHttpGet.mock.calls[0][0]).toEqual({
67-
host: "169.254.169.254",
68-
path: "/latest/meta-data/iam/security-credentials/",
69-
timeout
70-
});
71-
expect(mockHttpGet.mock.calls[1][0]).toEqual({
72-
host: "169.254.169.254",
73-
path: `/latest/meta-data/iam/security-credentials/${profile}`,
74-
timeout
53+
it("gets profile name from IMDS, and passes profile name to fetch credentials", async () => {
54+
(httpGet as jest.Mock)
55+
.mockResolvedValueOnce(mockProfile)
56+
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));
57+
58+
(retry as jest.Mock).mockImplementation((fn: any) => fn());
59+
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);
60+
61+
await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
62+
expect(httpGet).toHaveBeenCalledTimes(2);
63+
expect(httpGet).toHaveBeenNthCalledWith(1, mockHttpGetOptions);
64+
expect(httpGet).toHaveBeenNthCalledWith(2, {
65+
...mockHttpGetOptions,
66+
path: `${mockHttpGetOptions.path}${mockProfile}`
7567
});
7668
});
7769

78-
it("should retry the profile name fetch as necessary", async () => {
79-
const defaultTimeout = 1000;
80-
const profile = "foo-profile";
81-
mockHttpGet.mockReturnValueOnce(Promise.reject("Too busy"));
82-
mockHttpGet.mockReturnValueOnce(Promise.resolve(profile));
83-
mockHttpGet.mockReturnValueOnce(Promise.resolve(JSON.stringify(creds)));
84-
85-
await fromInstanceMetadata({ maxRetries: 1 })();
86-
expect(mockHttpGet.mock.calls.length).toEqual(3);
87-
expect(mockHttpGet.mock.calls[2][0]).toEqual({
88-
host: "169.254.169.254",
89-
path: `/latest/meta-data/iam/security-credentials/${profile}`,
90-
timeout: defaultTimeout
70+
it("trims profile returned name from IMDS", async () => {
71+
(httpGet as jest.Mock)
72+
.mockResolvedValueOnce(" " + mockProfile + " ")
73+
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));
74+
75+
(retry as jest.Mock).mockImplementation((fn: any) => fn());
76+
(fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds);
77+
78+
await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
79+
expect(httpGet).toHaveBeenCalledTimes(2);
80+
expect(httpGet).toHaveBeenNthCalledWith(1, mockHttpGetOptions);
81+
expect(httpGet).toHaveBeenNthCalledWith(2, {
82+
...mockHttpGetOptions,
83+
path: `${mockHttpGetOptions.path}${mockProfile}`
9184
});
92-
for (let index of [0, 1]) {
93-
expect(mockHttpGet.mock.calls[index][0]).toEqual({
94-
host: "169.254.169.254",
95-
path: "/latest/meta-data/iam/security-credentials/",
96-
timeout: defaultTimeout
97-
});
98-
}
85+
});
86+
87+
it("passes {} to providerConfigFromInit if init not defined", async () => {
88+
(retry as jest.Mock)
89+
.mockResolvedValueOnce(mockProfile)
90+
.mockResolvedValueOnce(mockCreds);
91+
92+
await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
93+
expect(providerConfigFromInit).toHaveBeenCalledTimes(1);
94+
expect(providerConfigFromInit).toHaveBeenCalledWith({});
95+
});
96+
97+
it("passes init to providerConfigFromInit", async () => {
98+
(retry as jest.Mock)
99+
.mockResolvedValueOnce(mockProfile)
100+
.mockResolvedValueOnce(mockCreds);
101+
102+
const init = { maxRetries: 5, timeout: 1213 };
103+
await expect(fromInstanceMetadata(init)()).resolves.toEqual(mockCreds);
104+
expect(providerConfigFromInit).toHaveBeenCalledTimes(1);
105+
expect(providerConfigFromInit).toHaveBeenCalledWith(init);
106+
});
107+
108+
it("passes maxRetries returned from providerConfigFromInit to retry", async () => {
109+
(retry as jest.Mock)
110+
.mockResolvedValueOnce(mockProfile)
111+
.mockResolvedValueOnce(mockCreds);
112+
113+
await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
114+
expect(retry).toHaveBeenCalledTimes(2);
115+
expect((retry as jest.Mock).mock.calls[0][1]).toBe(mockMaxRetries);
116+
expect((retry as jest.Mock).mock.calls[1][1]).toBe(mockMaxRetries);
117+
});
118+
119+
it("throws ProviderError if credentials returned are incorrect", async () => {
120+
(httpGet as jest.Mock)
121+
.mockResolvedValueOnce(mockProfile)
122+
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds));
123+
124+
(retry as jest.Mock).mockImplementation((fn: any) => fn());
125+
((isImdsCredentials as unknown) as jest.Mock).mockReturnValueOnce(false);
126+
127+
await expect(fromInstanceMetadata()()).rejects.toEqual(
128+
new ProviderError(
129+
"Invalid response received from instance metadata service."
130+
)
131+
);
132+
expect(retry).toHaveBeenCalledTimes(2);
133+
expect(httpGet).toHaveBeenCalledTimes(2);
134+
expect(isImdsCredentials).toHaveBeenCalledTimes(1);
135+
expect(isImdsCredentials).toHaveBeenCalledWith(mockImdsCreds);
136+
expect(fromImdsCredentials).not.toHaveBeenCalled();
137+
});
138+
139+
it("throws Error if requestFromEc2Imds for profile fails", async () => {
140+
const mockError = new Error("profile not found");
141+
(httpGet as jest.Mock).mockRejectedValueOnce(mockError);
142+
(retry as jest.Mock).mockImplementation((fn: any) => fn());
143+
144+
await expect(fromInstanceMetadata()()).rejects.toEqual(mockError);
145+
expect(retry).toHaveBeenCalledTimes(1);
146+
expect(httpGet).toHaveBeenCalledTimes(1);
147+
});
148+
149+
it("throws Error if requestFromEc2Imds for credentials fails", async () => {
150+
const mockError = new Error("creds not found");
151+
(httpGet as jest.Mock)
152+
.mockResolvedValueOnce(mockProfile)
153+
.mockRejectedValueOnce(mockError);
154+
(retry as jest.Mock).mockImplementation((fn: any) => fn());
155+
156+
await expect(fromInstanceMetadata()()).rejects.toEqual(mockError);
157+
expect(retry).toHaveBeenCalledTimes(2);
158+
expect(httpGet).toHaveBeenCalledTimes(2);
159+
expect(fromImdsCredentials).not.toHaveBeenCalled();
160+
});
161+
162+
it("throws SyntaxError if requestFromEc2Imds returns unparseable creds", async () => {
163+
(httpGet as jest.Mock)
164+
.mockResolvedValueOnce(mockProfile)
165+
.mockResolvedValueOnce(".");
166+
(retry as jest.Mock).mockImplementation((fn: any) => fn());
167+
168+
await expect(fromInstanceMetadata()()).rejects.toEqual(
169+
new SyntaxError("Unexpected token . in JSON at position 0")
170+
);
171+
expect(retry).toHaveBeenCalledTimes(2);
172+
expect(httpGet).toHaveBeenCalledTimes(2);
173+
expect(fromImdsCredentials).not.toHaveBeenCalled();
99174
});
100175
});

Diff for: packages/credential-provider-imds/src/fromInstanceMetadata.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import { ProviderError } from "@aws-sdk/property-provider";
1515
* Creates a credential provider that will source credentials from the EC2
1616
* Instance Metadata Service
1717
*/
18-
export function fromInstanceMetadata(
18+
export const fromInstanceMetadata = (
1919
init: RemoteProviderInit = {}
20-
): CredentialProvider {
20+
): CredentialProvider => {
2121
const { timeout, maxRetries } = providerConfigFromInit(init);
2222
return async () => {
2323
const profile = (
@@ -40,15 +40,19 @@ export function fromInstanceMetadata(
4040
return fromImdsCredentials(credsResponse);
4141
}, maxRetries);
4242
};
43-
}
43+
};
4444

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

48-
function requestFromEc2Imds(timeout: number, path?: string): Promise<string> {
49-
return httpGet({
48+
const requestFromEc2Imds = async (
49+
timeout: number,
50+
path?: string
51+
): Promise<string> => {
52+
const buffer = await httpGet({
5053
host: IMDS_IP,
5154
path: `/${IMDS_PATH}/${path ? path : ""}`,
5255
timeout
53-
}).then(buffer => buffer.toString());
54-
}
56+
});
57+
return buffer.toString();
58+
};

0 commit comments

Comments
 (0)