Skip to content

Commit ba11080

Browse files
committed
update the testcases accordingly
1 parent c865c2a commit ba11080

File tree

2 files changed

+171
-34
lines changed

2 files changed

+171
-34
lines changed

packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
import { loadConfig } from "@smithy/node-config-provider";
12
import { CredentialsProviderError } from "@smithy/property-provider";
23
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
3-
44
import { InstanceMetadataV1FallbackError } from "./error/InstanceMetadataV1FallbackError";
5-
import { fromInstanceMetadata } from "./fromInstanceMetadata";
5+
import { checkIfImdsDisabled, fromInstanceMetadata } from "./fromInstanceMetadata";
6+
import * as fromInstanceMetadataModule from "./fromInstanceMetadata";
67
import { httpRequest } from "./remoteProvider/httpRequest";
78
import { fromImdsCredentials, isImdsCredentials } from "./remoteProvider/ImdsCredentials";
89
import { providerConfigFromInit } from "./remoteProvider/RemoteProviderInit";
910
import { retry } from "./remoteProvider/retry";
1011
import { getInstanceMetadataEndpoint } from "./utils/getInstanceMetadataEndpoint";
1112
import { staticStabilityProvider } from "./utils/staticStabilityProvider";
1213

14+
vi.mock("@smithy/node-config-provider");
1315
vi.mock("./remoteProvider/httpRequest");
1416
vi.mock("./remoteProvider/ImdsCredentials");
1517
vi.mock("./remoteProvider/retry");
@@ -36,7 +38,7 @@ describe("fromInstanceMetadata", () => {
3638

3739
const mockProfileRequestOptions = {
3840
hostname,
39-
path: "/latest/meta-data/iam/security-credentials/",
41+
path: "/latest/meta-data/iam/security-credentials-extended/",
4042
timeout: mockTimeout,
4143
headers: {
4244
"x-aws-ec2-metadata-token": mockToken,
@@ -49,18 +51,22 @@ describe("fromInstanceMetadata", () => {
4951
SecretAccessKey: "bar",
5052
Token: "baz",
5153
Expiration: ONE_HOUR_IN_FUTURE.toISOString(),
54+
AccountId: "123456789012",
5255
});
5356

5457
const mockCreds = Object.freeze({
5558
accessKeyId: mockImdsCreds.AccessKeyId,
5659
secretAccessKey: mockImdsCreds.SecretAccessKey,
5760
sessionToken: mockImdsCreds.Token,
5861
expiration: new Date(mockImdsCreds.Expiration),
62+
accountId: mockImdsCreds.AccountId,
5963
});
6064

6165
beforeEach(() => {
6266
vi.mocked(staticStabilityProvider).mockImplementation((input) => input);
6367
vi.mocked(getInstanceMetadataEndpoint).mockResolvedValue({ hostname } as any);
68+
vi.mocked(loadConfig).mockReturnValue(() => Promise.resolve(false));
69+
vi.spyOn(fromInstanceMetadataModule, "checkIfImdsDisabled").mockResolvedValue(undefined);
6470
(isImdsCredentials as unknown as any).mockReturnValue(true);
6571
vi.mocked(providerConfigFromInit).mockReturnValue({
6672
timeout: mockTimeout,
@@ -72,6 +78,65 @@ describe("fromInstanceMetadata", () => {
7278
vi.resetAllMocks();
7379
});
7480

81+
it("returns no credentials when AWS_EC2_METADATA_DISABLED=true", async () => {
82+
vi.mocked(loadConfig).mockReturnValueOnce(() => Promise.resolve(true));
83+
vi.mocked(fromInstanceMetadataModule.checkIfImdsDisabled).mockRejectedValueOnce(
84+
new CredentialsProviderError("IMDS credential fetching is disabled")
85+
);
86+
const provider = fromInstanceMetadata({});
87+
88+
await expect(provider()).rejects.toEqual(new CredentialsProviderError("IMDS credential fetching is disabled"));
89+
expect(httpRequest).not.toHaveBeenCalled();
90+
});
91+
92+
it("returns valid credentials with account ID when ec2InstanceProfileName is provided", async () => {
93+
const profileName = "my-profile-0002";
94+
95+
vi.mocked(httpRequest)
96+
.mockResolvedValueOnce(mockToken as any)
97+
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds) as any);
98+
99+
vi.mocked(retry).mockImplementation((fn: any) => fn());
100+
vi.mocked(fromImdsCredentials).mockReturnValue(mockCreds);
101+
102+
const result = await fromInstanceMetadata({ ec2InstanceProfileName: profileName })();
103+
104+
expect(result).toEqual(mockCreds);
105+
expect(result.accountId).toBe(mockCreds.accountId);
106+
107+
expect(httpRequest).toHaveBeenCalledTimes(2);
108+
expect(httpRequest).toHaveBeenNthCalledWith(1, mockTokenRequestOptions);
109+
expect(httpRequest).toHaveBeenNthCalledWith(2, {
110+
...mockProfileRequestOptions,
111+
path: `${mockProfileRequestOptions.path}${profileName}`,
112+
});
113+
});
114+
115+
it("returns valid credentials with account ID when profile is discovered from IMDS", async () => {
116+
vi.mocked(httpRequest)
117+
.mockResolvedValueOnce(mockToken as any)
118+
.mockResolvedValueOnce(mockProfile as any)
119+
.mockResolvedValueOnce(JSON.stringify(mockImdsCreds) as any);
120+
121+
vi.mocked(retry).mockImplementation((fn: any) => fn());
122+
vi.mocked(fromImdsCredentials).mockReturnValue(mockCreds);
123+
124+
const provider = fromInstanceMetadata({});
125+
126+
const result = await provider();
127+
128+
expect(result).toEqual(mockCreds);
129+
expect(result.accountId).toBe(mockCreds.accountId);
130+
131+
expect(httpRequest).toHaveBeenCalledTimes(3);
132+
expect(httpRequest).toHaveBeenNthCalledWith(1, mockTokenRequestOptions);
133+
expect(httpRequest).toHaveBeenNthCalledWith(2, mockProfileRequestOptions);
134+
expect(httpRequest).toHaveBeenNthCalledWith(3, {
135+
...mockProfileRequestOptions,
136+
path: `${mockProfileRequestOptions.path}${mockProfile}`,
137+
});
138+
});
139+
75140
it("gets token and profile name to fetch credentials", async () => {
76141
vi.mocked(httpRequest)
77142
.mockResolvedValueOnce(mockToken as any)
@@ -99,6 +164,7 @@ describe("fromInstanceMetadata", () => {
99164

100165
vi.mocked(retry).mockImplementation((fn: any) => fn());
101166
vi.mocked(fromImdsCredentials).mockReturnValue(mockCreds);
167+
vi.mocked(checkIfImdsDisabled).mockReturnValueOnce(Promise.resolve());
102168

103169
await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
104170
expect(httpRequest).toHaveBeenNthCalledWith(3, {
@@ -109,6 +175,7 @@ describe("fromInstanceMetadata", () => {
109175

110176
it("passes {} to providerConfigFromInit if init not defined", async () => {
111177
vi.mocked(retry).mockResolvedValueOnce(mockProfile).mockResolvedValueOnce(mockCreds);
178+
vi.mocked(loadConfig).mockReturnValueOnce(() => Promise.resolve(false));
112179

113180
await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
114181
expect(providerConfigFromInit).toHaveBeenCalledTimes(1);
@@ -117,6 +184,7 @@ describe("fromInstanceMetadata", () => {
117184

118185
it("passes init to providerConfigFromInit", async () => {
119186
vi.mocked(retry).mockResolvedValueOnce(mockProfile).mockResolvedValueOnce(mockCreds);
187+
vi.mocked(loadConfig).mockReturnValueOnce(() => Promise.resolve(false));
120188

121189
const init = { maxRetries: 5, timeout: 1213 };
122190
await expect(fromInstanceMetadata(init)()).resolves.toEqual(mockCreds);
@@ -213,6 +281,73 @@ describe("fromInstanceMetadata", () => {
213281
expect(vi.mocked(staticStabilityProvider)).toBeCalledTimes(1);
214282
});
215283

284+
describe("getImdsProfileHelper", () => {
285+
beforeEach(() => {
286+
vi.mocked(httpRequest).mockClear();
287+
vi.mocked(loadConfig).mockClear();
288+
vi.mocked(retry).mockImplementation((fn: any) => fn());
289+
});
290+
291+
it("uses ec2InstanceProfileName from init if provided", async () => {
292+
const profileName = "profile-from-init";
293+
const options = { hostname } as any;
294+
295+
// Only use vi.spyOn for imported functions
296+
vi.spyOn(fromInstanceMetadataModule, "getConfiguredProfileName").mockResolvedValueOnce(profileName);
297+
298+
const result = await fromInstanceMetadataModule.getImdsProfileHelper(options, mockMaxRetries, {
299+
ec2InstanceProfileName: profileName,
300+
});
301+
302+
expect(result).toBe(profileName);
303+
expect(httpRequest).not.toHaveBeenCalled();
304+
});
305+
306+
it("uses environment variable if ec2InstanceProfileName not provided", async () => {
307+
const envProfileName = "profile-from-env";
308+
const options = { hostname } as any;
309+
310+
// Mock loadConfig to simulate env variable present
311+
vi.mocked(loadConfig).mockReturnValue(() => Promise.resolve(envProfileName));
312+
313+
const result = await fromInstanceMetadataModule.getImdsProfileHelper(options, mockMaxRetries, {});
314+
315+
expect(result).toBe(envProfileName);
316+
expect(httpRequest).not.toHaveBeenCalled();
317+
});
318+
319+
it("uses profile from config file if present, otherwise falls back to IMDS (extended then legacy)", async () => {
320+
const configProfileName = "profile-from-config";
321+
const legacyProfileName = "profile-from-legacy";
322+
const options = { hostname } as any;
323+
324+
// 1. Simulate config file present: should return configProfileName, no IMDS call
325+
vi.mocked(loadConfig).mockReturnValue(() => Promise.resolve(configProfileName));
326+
327+
let result = await fromInstanceMetadataModule.getImdsProfileHelper(options, mockMaxRetries, {});
328+
expect(result).toBe(configProfileName);
329+
expect(httpRequest).not.toHaveBeenCalled();
330+
331+
// 2. Simulate config file missing: should call IMDS (extended fails, legacy succeeds)
332+
vi.mocked(loadConfig).mockReturnValue(() => Promise.resolve(null));
333+
vi.mocked(httpRequest)
334+
.mockRejectedValueOnce(Object.assign(new Error(), { statusCode: 404 }))
335+
.mockResolvedValueOnce(legacyProfileName as any);
336+
337+
result = await fromInstanceMetadataModule.getImdsProfileHelper(options, mockMaxRetries, {});
338+
expect(result).toBe(legacyProfileName);
339+
expect(httpRequest).toHaveBeenCalledTimes(2);
340+
expect(httpRequest).toHaveBeenNthCalledWith(1, {
341+
...options,
342+
path: "/latest/meta-data/iam/security-credentials-extended/",
343+
});
344+
expect(httpRequest).toHaveBeenNthCalledWith(2, {
345+
...options,
346+
path: "/latest/meta-data/iam/security-credentials/",
347+
});
348+
});
349+
});
350+
216351
describe("disables fetching of token", () => {
217352
beforeEach(() => {
218353
vi.mocked(retry).mockImplementation((fn: any) => fn());

packages/credential-provider-imds/src/fromInstanceMetadata.ts

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ const X_AWS_EC2_METADATA_TOKEN = "x-aws-ec2-metadata-token";
2121

2222
// Environment variables and config keys
2323

24-
const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
25-
const CONFIG_IMDS_DISABLED = "disable_ec2_metadata";
26-
const ENV_PROFILE_NAME = "AWS_EC2_INSTANCE_PROFILE_NAME";
27-
const CONFIG_PROFILE_NAME = "ec2_instance_profile_name";
24+
const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
25+
const CONFIG_IMDS_DISABLED = "disable_ec2_metadata";
26+
const ENV_PROFILE_NAME = "AWS_EC2_INSTANCE_PROFILE_NAME";
27+
const CONFIG_PROFILE_NAME = "ec2_instance_profile_name";
2828

2929
/**
3030
* @internal
@@ -44,7 +44,6 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
4444
const { logger, profile } = init;
4545
const { timeout, maxRetries } = providerConfigFromInit(init);
4646

47-
4847
const getCredentials = async (maxRetries: number, options: RequestOptions) => {
4948
const isImdsV1Fallback = disableFetchToken || options.headers?.[X_AWS_EC2_METADATA_TOKEN] == null;
5049

@@ -110,8 +109,7 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
110109
}, maxRetries);
111110
};
112111

113-
114-
return async () => {
112+
return async () => {
115113
const endpoint = await getInstanceMetadataEndpoint();
116114
await checkIfImdsDisabled(profile, logger);
117115
if (disableFetchToken) {
@@ -147,15 +145,21 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
147145
* @internal
148146
* Gets IMDS profile with proper error handling and retries
149147
*/
150-
const getImdsProfileHelper = async (
148+
export const getImdsProfileHelper = async (
151149
options: RequestOptions,
152150
maxRetries: number,
153151
init: RemoteProviderInit = {},
154-
profile?: string
152+
profile?: string,
153+
resetCache?: boolean
155154
): Promise<string> => {
156155
let apiVersion: "unknown" | "extended" | "legacy" = "unknown";
157156
let resolvedProfile: string | null = null;
158-
157+
158+
// If resetCache is true, clear the cached profile name
159+
if (resetCache) {
160+
resolvedProfile = null;
161+
}
162+
159163
return retry<string>(async () => {
160164
// First check if a profile name is configured
161165
const configuredName = await getConfiguredProfileName(init, profile);
@@ -169,7 +173,7 @@ const getImdsProfileHelper = async (
169173
try {
170174
// Try extended API first
171175
try {
172-
const response = await httpRequest({...options, path: IMDS_EXTENDED_PATH});
176+
const response = await httpRequest({ ...options, path: IMDS_EXTENDED_PATH });
173177
resolvedProfile = response.toString().trim();
174178
if (apiVersion === "unknown") {
175179
apiVersion = "extended";
@@ -178,7 +182,7 @@ const getImdsProfileHelper = async (
178182
} catch (error) {
179183
if (error?.statusCode === 404 && apiVersion === "unknown") {
180184
apiVersion = "legacy";
181-
const response = await httpRequest({...options, path: IMDS_LEGACY_PATH});
185+
const response = await httpRequest({ ...options, path: IMDS_LEGACY_PATH });
182186
resolvedProfile = response.toString().trim();
183187
return resolvedProfile;
184188
} else {
@@ -191,7 +195,6 @@ const getImdsProfileHelper = async (
191195
}, maxRetries);
192196
};
193197

194-
195198
const getMetadataToken = async (options: RequestOptions) =>
196199
httpRequest({
197200
...options,
@@ -206,7 +209,7 @@ const getMetadataToken = async (options: RequestOptions) =>
206209
* @internal
207210
* Checks if IMDS credential fetching is disabled through configuration
208211
*/
209-
const checkIfImdsDisabled = async (profile?: string, logger?: any): Promise<void> => {
212+
export const checkIfImdsDisabled = async (profile?: string, logger?: any): Promise<void> => {
210213
// Load configuration in priority order
211214
const disableImds = await loadConfig(
212215
{
@@ -224,7 +227,7 @@ const checkIfImdsDisabled = async (profile?: string, logger?: any): Promise<void
224227
},
225228
{ profile }
226229
)();
227-
230+
228231
// If IMDS is disabled, throw error
229232
if (disableImds) {
230233
throw new CredentialsProviderError("IMDS credential fetching is disabled", { logger });
@@ -235,7 +238,7 @@ const checkIfImdsDisabled = async (profile?: string, logger?: any): Promise<void
235238
* @internal
236239
* Gets configured profile name from various sources
237240
*/
238-
const getConfiguredProfileName = async (init: RemoteProviderInit, profile?: string): Promise<string | null> => {
241+
export const getConfiguredProfileName = async (init: RemoteProviderInit, profile?: string): Promise<string | null> => {
239242
// Load configuration in priority order
240243
const profileName = await loadConfig(
241244
{
@@ -247,19 +250,18 @@ const getConfiguredProfileName = async (init: RemoteProviderInit, profile?: stri
247250
},
248251
{ profile }
249252
)();
250-
253+
251254
// Check runtime config (highest priority)
252255
const name = init.ec2InstanceProfileName || profileName;
253-
256+
254257
// Validate if name is provided but empty
255-
if (typeof name === 'string' && name.trim() === "") {
258+
if (typeof name === "string" && name.trim() === "") {
256259
throw new CredentialsProviderError("EC2 instance profile name cannot be empty");
257260
}
258-
261+
259262
return name;
260263
};
261264

262-
263265
/**
264266
* @internal
265267
* Gets credentials from profile
@@ -275,12 +277,12 @@ const getCredentialsFromProfile = async (profile: string, options: RequestOption
275277
return await getCredentialsFromPath(IMDS_LEGACY_PATH + profile, options);
276278
} catch (legacyError) {
277279
if (legacyError.statusCode === 404 && init.ec2InstanceProfileName === undefined) {
278-
// If legacy API also returns 404 and we're using a cached profile name,
279-
// the profile might have changed - clear cache and retry
280-
const resolvedProfile = null;
281-
const newProfileName = await getImdsProfileHelper(options, init.maxRetries ?? 3, init, profile);
282-
return getCredentialsFromProfile(newProfileName, options, init);
283-
}
280+
// If legacy API also returns 404 and we're using a cached profile name,
281+
// the profile might have changed - clear cache and retry
282+
const resolvedProfile = null;
283+
const newProfileName = await getImdsProfileHelper(options, init.maxRetries ?? 3, init, profile, true);
284+
return getCredentialsFromProfile(newProfileName, options, init);
285+
}
284286
throw legacyError;
285287
}
286288
}
@@ -297,14 +299,14 @@ async function getCredentialsFromPath(path: string, options: RequestOptions) {
297299
...options,
298300
path,
299301
});
300-
302+
301303
const credentialsResponse = JSON.parse(response.toString());
302-
304+
303305
// Validate response
304306
if (!isImdsCredentials(credentialsResponse)) {
305307
throw new CredentialsProviderError("Invalid response received from instance metadata service.");
306308
}
307-
309+
308310
// Convert IMDS credentials format to standard format
309311
return fromImdsCredentials(credentialsResponse);
310-
}
312+
}

0 commit comments

Comments
 (0)