Skip to content

Commit 1452cd4

Browse files
authoredJan 24, 2024
feat(credential-provider-node): use dynamic import for credential providers (#5698)
1 parent e2b9012 commit 1452cd4

File tree

12 files changed

+587
-436
lines changed

12 files changed

+587
-436
lines changed
 

‎Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ unlink-smithy:
2020
rm ./node_modules/\@smithy
2121
yarn --check-files
2222

23+
copy-smithy:
24+
node ./scripts/copy-smithy-dist-files
25+
2326
# Runs build for all packages using Turborepo
2427
turbo-build:
2528
(cd scripts/remote-cache && yarn)

‎package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"@tsconfig/recommended": "1.0.1",
7070
"@types/chai-as-promised": "^7.1.2",
7171
"@types/fs-extra": "^8.0.1",
72-
"@types/jest": "29.5.2",
72+
"@types/jest": "29.5.11",
7373
"@typescript-eslint/eslint-plugin": "5.55.0",
7474
"@typescript-eslint/parser": "5.55.0",
7575
"async": "3.2.4",
@@ -93,8 +93,8 @@
9393
"glob": "7.1.6",
9494
"husky": "^4.2.3",
9595
"jasmine-core": "^3.5.0",
96-
"jest": "29.5.0",
97-
"jest-environment-jsdom": "29.5.0",
96+
"jest": "29.7.0",
97+
"jest-environment-jsdom": "29.7.0",
9898
"jmespath": "^0.15.0",
9999
"json5": "^2.2.0",
100100
"karma": "6.4.0",
@@ -113,7 +113,7 @@
113113
"mocha": "10.0.0",
114114
"prettier": "2.8.5",
115115
"rimraf": "3.0.2",
116-
"ts-jest": "29.1.0",
116+
"ts-jest": "29.1.1",
117117
"ts-loader": "9.4.2",
118118
"ts-mocha": "10.0.0",
119119
"ts-node": "10.9.1",

‎packages/credential-provider-node/src/defaultProvider.spec.ts

+53-73
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { fromIni } from "@aws-sdk/credential-provider-ini";
33
import { fromProcess } from "@aws-sdk/credential-provider-process";
44
import { fromSSO } from "@aws-sdk/credential-provider-sso";
55
import { fromTokenFile } from "@aws-sdk/credential-provider-web-identity";
6-
import { chain, CredentialsProviderError, memoize } from "@smithy/property-provider";
6+
import { CredentialsProviderError } from "@smithy/property-provider";
77
import { ENV_PROFILE, loadSharedConfigFiles } from "@smithy/shared-ini-file-loader";
88

9-
import { defaultProvider } from "./defaultProvider";
9+
import { credentialsTreatedAsExpired, credentialsWillNeedRefresh, defaultProvider } from "./defaultProvider";
1010
import { remoteProvider } from "./remoteProvider";
1111

1212
jest.mock("@aws-sdk/credential-provider-env");
@@ -15,7 +15,6 @@ jest.mock("@aws-sdk/credential-provider-ini");
1515
jest.mock("@aws-sdk/credential-provider-process");
1616
jest.mock("@aws-sdk/credential-provider-sso");
1717
jest.mock("@aws-sdk/credential-provider-web-identity");
18-
jest.mock("@smithy/property-provider");
1918
jest.mock("@smithy/shared-ini-file-loader");
2019
jest.mock("./remoteProvider");
2120

@@ -25,19 +24,24 @@ describe(defaultProvider.name, () => {
2524
secretAccessKey: "mockSecretAccessKey",
2625
};
2726

27+
const credentials = () => {
28+
throw new CredentialsProviderError("test", true);
29+
};
30+
31+
const finalCredentials = () => {
32+
return mockCreds;
33+
};
34+
2835
const mockInit = {
2936
profile: "mockProfile",
3037
};
3138

32-
const mockEnvFn = jest.fn();
33-
const mockSsoFn = jest.fn();
34-
const mockIniFn = jest.fn();
35-
const mockProcessFn = jest.fn();
36-
const mockTokenFileFn = jest.fn();
37-
const mockRemoteProviderFn = jest.fn();
38-
39-
const mockChainFn = jest.fn();
40-
const mockMemoizeFn = jest.fn().mockResolvedValue(mockCreds);
39+
const mockEnvFn = jest.fn().mockImplementation(() => credentials());
40+
const mockSsoFn = jest.fn().mockImplementation(() => credentials());
41+
const mockIniFn = jest.fn().mockImplementation(() => credentials());
42+
const mockProcessFn = jest.fn().mockImplementation(() => credentials());
43+
const mockTokenFileFn = jest.fn().mockImplementation(() => credentials());
44+
const mockRemoteProviderFn = jest.fn().mockImplementation(() => finalCredentials());
4145

4246
beforeEach(() => {
4347
[
@@ -47,51 +51,48 @@ describe(defaultProvider.name, () => {
4751
[fromProcess, mockProcessFn],
4852
[fromTokenFile, mockTokenFileFn],
4953
[remoteProvider, mockRemoteProviderFn],
50-
[chain, mockChainFn],
51-
[memoize, mockMemoizeFn],
5254
].forEach(([fromFn, mockFn]) => {
5355
(fromFn as jest.Mock).mockReturnValue(mockFn);
5456
});
5557
});
5658

5759
afterEach(async () => {
58-
const errorFnIndex = (chain as jest.Mock).mock.calls[0].length;
59-
const errorFn = (chain as jest.Mock).mock.calls[0][errorFnIndex - 1];
60-
const expectedError = new CredentialsProviderError("Could not load credentials from any providers", false);
61-
try {
62-
await errorFn();
63-
fail(`expected ${expectedError}`);
64-
} catch (error) {
65-
expect(error.toString()).toStrictEqual(expectedError.toString());
66-
}
67-
68-
expect(memoize).toHaveBeenCalledWith(mockChainFn, expect.any(Function), expect.any(Function));
69-
7060
jest.clearAllMocks();
7161
});
7262

7363
describe("without fromEnv", () => {
74-
afterEach(() => {
75-
expect(chain).toHaveBeenCalledWith(
76-
mockSsoFn,
77-
mockIniFn,
78-
mockProcessFn,
79-
mockTokenFileFn,
80-
mockRemoteProviderFn,
81-
expect.any(Function)
82-
);
83-
});
84-
8564
it("creates provider chain and memoizes it", async () => {
86-
const receivedCreds = await defaultProvider(mockInit)();
87-
expect(receivedCreds).toStrictEqual(mockCreds);
65+
const provider = defaultProvider(mockInit);
8866

89-
expect(fromEnv).not.toHaveBeenCalled();
90-
for (const fromFn of [fromSSO, fromIni, fromProcess, fromTokenFile, remoteProvider]) {
91-
expect(fromFn).toHaveBeenCalledWith(mockInit);
67+
// initial call proceeds through the chain.
68+
{
69+
const receivedCreds = await provider();
70+
expect(receivedCreds).toEqual(mockCreds);
71+
72+
expect(fromEnv).not.toHaveBeenCalled();
73+
expect(fromSSO).toHaveBeenCalledWith(mockInit);
74+
expect(fromIni).toHaveBeenCalledWith(mockInit);
75+
expect(fromProcess).toHaveBeenCalledWith(mockInit);
76+
expect(fromTokenFile).toHaveBeenCalledWith(mockInit);
77+
expect(remoteProvider).toHaveBeenCalledWith(mockInit);
78+
79+
expect(loadSharedConfigFiles).not.toHaveBeenCalled();
9280
}
9381

94-
expect(loadSharedConfigFiles).not.toHaveBeenCalled();
82+
jest.clearAllMocks();
83+
84+
// subsequent call does not enter the chain.
85+
{
86+
const receivedCreds = await provider();
87+
expect(receivedCreds).toEqual(mockCreds);
88+
89+
expect(fromEnv).not.toHaveBeenCalled();
90+
expect(fromSSO).not.toHaveBeenCalledWith(mockInit);
91+
expect(fromIni).not.toHaveBeenCalledWith(mockInit);
92+
expect(fromProcess).not.toHaveBeenCalledWith(mockInit);
93+
expect(fromTokenFile).not.toHaveBeenCalledWith(mockInit);
94+
expect(remoteProvider).not.toHaveBeenCalledWith(mockInit);
95+
}
9596
});
9697

9798
it(`if env['${ENV_PROFILE}'] is set`, async () => {
@@ -123,63 +124,42 @@ describe(defaultProvider.name, () => {
123124
for (const fromFn of [fromSSO, fromIni, fromProcess, fromTokenFile, remoteProvider]) {
124125
expect(fromFn).toHaveBeenCalledWith(mockInitWithoutProfile);
125126
}
126-
127-
expect(chain).toHaveBeenCalledWith(
128-
mockEnvFn,
129-
mockSsoFn,
130-
mockIniFn,
131-
mockProcessFn,
132-
mockTokenFileFn,
133-
mockRemoteProviderFn,
134-
expect.any(Function)
135-
);
136127
});
137128

138-
describe("memoize isExpired", () => {
129+
describe(credentialsTreatedAsExpired.name, () => {
139130
const mockDateNow = Date.now();
140131
beforeEach(async () => {
141132
jest.spyOn(Date, "now").mockReturnValueOnce(mockDateNow);
142-
await defaultProvider(mockInit)();
143133
});
144134

145135
it("returns true if expiration is defined, and creds have expired", () => {
146-
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1];
147136
const expiration = new Date(mockDateNow - 24 * 60 * 60 * 1000);
148-
expect(memoizeExpiredFn({ expiration })).toEqual(true);
137+
expect(credentialsTreatedAsExpired({ ...mockCreds, expiration })).toEqual(true);
149138
});
150139

151140
it("returns true if expiration is defined, and creds expire in <5 mins", () => {
152-
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1];
153141
const expiration = new Date(mockDateNow + 299 * 1000);
154-
expect(memoizeExpiredFn({ expiration })).toEqual(true);
142+
expect(credentialsTreatedAsExpired({ ...mockCreds, expiration })).toEqual(true);
155143
});
156144

157145
it("returns false if expiration is defined, but creds expire in >5 mins", () => {
158-
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1];
159146
const expiration = new Date(mockDateNow + 301 * 1000);
160-
expect(memoizeExpiredFn({ expiration })).toEqual(false);
147+
expect(credentialsTreatedAsExpired({ ...mockCreds, expiration })).toEqual(false);
161148
});
162149

163150
it("returns false if expiration is not defined", () => {
164-
const memoizeExpiredFn = (memoize as jest.Mock).mock.calls[0][1];
165-
expect(memoizeExpiredFn({})).toEqual(false);
151+
expect(credentialsTreatedAsExpired({ ...mockCreds })).toEqual(false);
166152
});
167153
});
168154

169-
describe("memoize requiresRefresh", () => {
170-
beforeEach(async () => {
171-
await defaultProvider(mockInit)();
172-
});
173-
155+
describe(credentialsWillNeedRefresh.name, () => {
174156
it("returns true if expiration is not defined", () => {
175-
const memoizeRefreshFn = (memoize as jest.Mock).mock.calls[0][2];
176-
const expiration = Date.now();
177-
expect(memoizeRefreshFn({ expiration })).toEqual(true);
157+
const expiration = new Date();
158+
expect(credentialsWillNeedRefresh({ ...mockCreds, expiration })).toEqual(true);
178159
});
179160

180161
it("returns false if expiration is not defined", () => {
181-
const memoizeRefreshFn = (memoize as jest.Mock).mock.calls[0][2];
182-
expect(memoizeRefreshFn({})).toEqual(false);
162+
expect(credentialsWillNeedRefresh({ ...mockCreds })).toEqual(false);
183163
});
184164
});
185165
});

‎packages/credential-provider-node/src/defaultProvider.ts

+49-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { fromEnv } from "@aws-sdk/credential-provider-env";
2-
import { fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
3-
import { fromProcess, FromProcessInit } from "@aws-sdk/credential-provider-process";
4-
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
5-
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
6-
import { RemoteProviderInit } from "@smithy/credential-provider-imds";
1+
import type { FromIniInit } from "@aws-sdk/credential-provider-ini";
2+
import type { FromProcessInit } from "@aws-sdk/credential-provider-process";
3+
import type { FromSSOInit } from "@aws-sdk/credential-provider-sso";
4+
import type { FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
5+
import type { RemoteProviderInit } from "@smithy/credential-provider-imds";
76
import { chain, CredentialsProviderError, memoize } from "@smithy/property-provider";
87
import { ENV_PROFILE } from "@smithy/shared-ini-file-loader";
98
import { AwsCredentialIdentity, MemoizedProvider } from "@smithy/types";
@@ -49,16 +48,52 @@ export type DefaultProviderInit = FromIniInit & RemoteProviderInit & FromProcess
4948
export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvider<AwsCredentialIdentity> =>
5049
memoize(
5150
chain(
52-
...(init.profile || process.env[ENV_PROFILE] ? [] : [fromEnv()]),
53-
fromSSO(init),
54-
fromIni(init),
55-
fromProcess(init),
56-
fromTokenFile(init),
57-
remoteProvider(init),
51+
...(init.profile || process.env[ENV_PROFILE]
52+
? []
53+
: [
54+
async () => {
55+
const { fromEnv } = await import("@aws-sdk/credential-provider-env");
56+
return fromEnv()();
57+
},
58+
]),
59+
async () => {
60+
const { fromSSO } = await import("@aws-sdk/credential-provider-sso");
61+
return fromSSO(init)();
62+
},
63+
async () => {
64+
const { fromIni } = await import("@aws-sdk/credential-provider-ini");
65+
return fromIni(init)();
66+
},
67+
async () => {
68+
const { fromProcess } = await import("@aws-sdk/credential-provider-process");
69+
return fromProcess(init)();
70+
},
71+
async () => {
72+
const { fromTokenFile } = await import("@aws-sdk/credential-provider-web-identity");
73+
return fromTokenFile(init)();
74+
},
75+
async () => {
76+
return (await remoteProvider(init))();
77+
},
5878
async () => {
5979
throw new CredentialsProviderError("Could not load credentials from any providers", false);
6080
}
6181
),
62-
(credentials) => credentials.expiration !== undefined && credentials.expiration.getTime() - Date.now() < 300000,
63-
(credentials) => credentials.expiration !== undefined
82+
credentialsTreatedAsExpired,
83+
credentialsWillNeedRefresh
6484
);
85+
86+
/**
87+
* @internal
88+
*
89+
* @returns credentials have expiration.
90+
*/
91+
export const credentialsWillNeedRefresh = (credentials: AwsCredentialIdentity) => credentials?.expiration !== undefined;
92+
93+
/**
94+
* @internal
95+
*
96+
* @returns credentials with less than 5 minutes left.
97+
*/
98+
export const credentialsTreatedAsExpired = (credentials: AwsCredentialIdentity) =>
99+
credentials?.expiration !== undefined && credentials.expiration.getTime() - Date.now() < 300000;

‎packages/credential-provider-node/src/remoteProvider.spec.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe(remoteProvider.name, () => {
4242
"returns fromContainerMetadata if env['%s'] is set",
4343
async (key) => {
4444
process.env[key] = "defined";
45-
const receivedCreds = await remoteProvider(mockInit)();
45+
const receivedCreds = await (await remoteProvider(mockInit))();
4646
expect(receivedCreds).toStrictEqual(mockCredsFromContainer);
4747
expect(fromContainerMetadata).toHaveBeenCalledWith(mockInit);
4848
expect(fromInstanceMetadata).not.toHaveBeenCalled();
@@ -53,7 +53,9 @@ describe(remoteProvider.name, () => {
5353
process.env[ENV_IMDS_DISABLED] = "1";
5454
const expectedError = new CredentialsProviderError("EC2 Instance Metadata Service access disabled");
5555
try {
56-
await remoteProvider(mockInit)();
56+
await (
57+
await remoteProvider(mockInit)
58+
)();
5759
fail(`expectedError ${expectedError}`);
5860
} catch (error) {
5961
expect(error).toStrictEqual(expectedError);
@@ -63,7 +65,7 @@ describe(remoteProvider.name, () => {
6365
});
6466

6567
it("returns fromInstanceMetadata if environment variables are not set", async () => {
66-
const receivedCreds = await remoteProvider(mockInit)();
68+
const receivedCreds = await (await remoteProvider(mockInit))();
6769
expect(receivedCreds).toStrictEqual(mockSourceCredsFromInstanceMetadata);
6870
expect(fromInstanceMetadata).toHaveBeenCalledWith(mockInit);
6971
expect(fromContainerMetadata).not.toHaveBeenCalled();

‎packages/credential-provider-node/src/remoteProvider.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import {
2-
ENV_CMDS_FULL_URI,
3-
ENV_CMDS_RELATIVE_URI,
4-
fromContainerMetadata,
5-
fromInstanceMetadata,
6-
RemoteProviderInit,
7-
} from "@smithy/credential-provider-imds";
1+
import type { RemoteProviderInit } from "@smithy/credential-provider-imds";
82
import { CredentialsProviderError } from "@smithy/property-provider";
9-
import { AwsCredentialIdentityProvider } from "@smithy/types";
3+
import type { AwsCredentialIdentityProvider } from "@smithy/types";
104

115
export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
126

13-
export const remoteProvider = (init: RemoteProviderInit): AwsCredentialIdentityProvider => {
7+
/**
8+
* @internal
9+
*/
10+
export const remoteProvider = async (init: RemoteProviderInit): Promise<AwsCredentialIdentityProvider> => {
11+
const { ENV_CMDS_FULL_URI, ENV_CMDS_RELATIVE_URI, fromContainerMetadata, fromInstanceMetadata } = await import(
12+
"@smithy/credential-provider-imds"
13+
);
14+
1415
if (process.env[ENV_CMDS_RELATIVE_URI] || process.env[ENV_CMDS_FULL_URI]) {
1516
return fromContainerMetadata(init);
1617
}

‎packages/middleware-sdk-s3/src/middleware-sdk-s3.integ.spec.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import { GetObjectCommand, PutObjectCommand, S3 } from "@aws-sdk/client-s3";
2-
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
3-
import { NoOpLogger } from "@smithy/smithy-client";
4-
import { parseUrl } from "@smithy/url-parser";
1+
import { S3 } from "@aws-sdk/client-s3";
52

63
import { requireRequestsFrom } from "../../../private/aws-util-test/src";
74

‎packages/polly-request-presigner/src/getSignedUrls.spec.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ jest.mock("@aws-sdk/util-format-url", () => ({
1414
formatUrl: (url: any) => url,
1515
}));
1616

17-
import { RequestPresigningArguments } from "@smithy/types";
17+
import { AwsCredentialIdentity, RequestPresigningArguments } from "@smithy/types";
1818

1919
import { getSignedUrl } from "./getSignedUrls";
2020

2121
describe("getSignedUrl", () => {
22-
const clientParams = { region: "us-foo-1" };
22+
const credentials: AwsCredentialIdentity = {
23+
secretAccessKey: "unit-test",
24+
accessKeyId: "unit-test",
25+
sessionToken: "unit-test",
26+
};
27+
const clientParams = { region: "us-foo-1", credentials };
2328

2429
beforeEach(() => {
2530
mockPresign.mockReset();

‎private/aws-client-retry-test/src/ClientRetryTest.spec.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { HeadObjectCommand, S3, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
22
import { HttpHandler, HttpResponse } from "@smithy/protocol-http";
3-
import { RequestHandlerOutput } from "@smithy/types";
3+
import { AwsCredentialIdentity, RequestHandlerOutput } from "@smithy/types";
44
import { ConfiguredRetryStrategy, StandardRetryStrategy } from "@smithy/util-retry";
55
import { Readable } from "stream";
66

@@ -22,6 +22,12 @@ class MockRequestHandler implements HttpHandler {
2222
}
2323

2424
describe("util-retry integration tests", () => {
25+
const credentials: AwsCredentialIdentity = {
26+
accessKeyId: "test",
27+
secretAccessKey: "test",
28+
sessionToken: "test",
29+
};
30+
2531
const mockThrottled: RequestHandlerOutput<HttpResponse> = {
2632
response: new HttpResponse({
2733
statusCode: 429,
@@ -48,6 +54,7 @@ describe("util-retry integration tests", () => {
4854
httpHandlerConfigs: () => ({}),
4955
},
5056
region: MOCK_REGION,
57+
credentials,
5158
});
5259
expect(await client.config.retryStrategy()).toBeInstanceOf(StandardRetryStrategy);
5360
const response = await client.send(headObjectCommand);
@@ -69,6 +76,7 @@ describe("util-retry integration tests", () => {
6976
updateHttpClientConfig: () => {},
7077
},
7178
region: MOCK_REGION,
79+
credentials,
7280
});
7381
expect(await client.config.retryStrategy()).toBeInstanceOf(StandardRetryStrategy);
7482
const response = await client.send(headObjectCommand);
@@ -97,6 +105,7 @@ describe("util-retry integration tests", () => {
97105
updateHttpClientConfig: () => {},
98106
},
99107
region: MOCK_REGION,
108+
credentials,
100109
});
101110
expect(await client.config.retryStrategy()).toBeInstanceOf(StandardRetryStrategy);
102111
try {
@@ -125,6 +134,7 @@ describe("util-retry integration tests", () => {
125134
requestHandler: new MockRequestHandler(),
126135
retryStrategy,
127136
region: MOCK_REGION,
137+
credentials,
128138
});
129139

130140
expect(retryStrategy.getCapacity()).toEqual(expectedInitialCapacity);

‎private/aws-middleware-test/src/middleware-content-length.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { AccessAnalyzer } from "@aws-sdk/client-accessanalyzer";
22
import { S3 } from "@aws-sdk/client-s3";
33
import { XRay } from "@aws-sdk/client-xray";
44

5-
import { requireRequestsFrom } from "../../aws-util-test/src";
5+
import { requireRequestsFrom } from "../../../private/aws-util-test/src";
66

77
describe("middleware-content-length", () => {
88
describe(AccessAnalyzer.name, () => {

‎scripts/compilation/Inliner.js

+3
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ module.exports = class Inliner {
157157
mainFields: ["main"],
158158
allowOverwrite: true,
159159
entryPoints: [path.join(root, this.subfolder, this.package, "src", "index.ts")],
160+
supported: {
161+
"dynamic-import": false,
162+
},
160163
outfile: this.outfile,
161164
keepNames: true,
162165
packages: "external",

‎yarn.lock

+440-325
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.