Skip to content

Commit 5b9b136

Browse files
committed
chore: aws chain memoizer
1 parent 72bf56d commit 5b9b136

File tree

7 files changed

+418
-190
lines changed

7 files changed

+418
-190
lines changed

packages/credential-provider-ini/src/fromIni.integ.spec.ts

Lines changed: 0 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -306,164 +306,4 @@ describe("fromIni region search order", () => {
306306
sessionToken: "STS_AR_SESSION_TOKEN_us-east-1",
307307
});
308308
});
309-
310-
describe("logic table", () => {
311-
type Parameters = {
312-
// has caller context client
313-
withCaller: boolean;
314-
// has region specified on the caller client
315-
codeRegion: boolean;
316-
// AWS_REGION is set
317-
envRegion: boolean;
318-
// profile regions are set
319-
profileRegion: boolean;
320-
// provider itself has a clientConfig.region
321-
providerRegion: boolean;
322-
// profile name
323-
profile: string | undefined;
324-
};
325-
326-
for (const withCaller of [true, false]) {
327-
for (const codeRegion of [true, false]) {
328-
for (const envRegion of [true, false]) {
329-
for (const profileRegion of [true, false]) {
330-
for (const providerRegion of [true, false]) {
331-
for (const profile of ["default", "alt", undefined]) {
332-
if (!codeRegion && !profileRegion && !envRegion) {
333-
continue;
334-
}
335-
336-
const params = {
337-
withCaller,
338-
codeRegion,
339-
envRegion,
340-
profileRegion,
341-
providerRegion,
342-
profile,
343-
};
344-
345-
it(`should resolve region as expected with params=${JSON.stringify(params)}`, async () => {
346-
const region = await resolveStsRegion(params);
347-
348-
if (providerRegion) {
349-
expect(region).toBe("provider-region");
350-
return;
351-
}
352-
353-
if (profileRegion) {
354-
expect(region).toBe(`${profile ?? "default"}-profile-region`);
355-
return;
356-
}
357-
358-
if (codeRegion && withCaller) {
359-
expect(region).toBe("code-region");
360-
return;
361-
}
362-
363-
if (envRegion) {
364-
expect(region).toBe("env-region");
365-
return;
366-
}
367-
368-
expect(region).toBe("us-east-1");
369-
});
370-
}
371-
}
372-
}
373-
}
374-
}
375-
}
376-
377-
async function resolveStsRegion({
378-
withCaller,
379-
envRegion,
380-
profile,
381-
profileRegion,
382-
codeRegion,
383-
providerRegion,
384-
}: Parameters) {
385-
if (envRegion) {
386-
process.env.AWS_REGION = "env-region";
387-
} else {
388-
delete process.env.AWS_REGION;
389-
}
390-
391-
if (profileRegion) {
392-
iniProfileData = {
393-
default: {
394-
region: "default-profile-region",
395-
role_arn: "ROLE_ARN",
396-
role_session_name: "ROLE_SESSION_NAME",
397-
external_id: "EXTERNAL_ID",
398-
source_profile: "assume",
399-
},
400-
assume: {
401-
region: "assume-profile-region",
402-
aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY",
403-
aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY",
404-
},
405-
alt: {
406-
region: "alt-profile-region",
407-
role_arn: "ROLE_ARN",
408-
role_session_name: "ROLE_SESSION_NAME",
409-
external_id: "EXTERNAL_ID",
410-
source_profile: "assume2",
411-
},
412-
assume2: {
413-
region: "assume2-profile-region",
414-
aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY",
415-
aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY",
416-
},
417-
};
418-
} else {
419-
iniProfileData = {
420-
default: {
421-
role_arn: "ROLE_ARN",
422-
role_session_name: "ROLE_SESSION_NAME",
423-
external_id: "EXTERNAL_ID",
424-
source_profile: "assume",
425-
},
426-
assume: {
427-
aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY",
428-
aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY",
429-
},
430-
alt: {
431-
role_arn: "ROLE_ARN",
432-
role_session_name: "ROLE_SESSION_NAME",
433-
external_id: "EXTERNAL_ID",
434-
source_profile: "assume2",
435-
},
436-
assume2: {
437-
aws_access_key_id: "ASSUME_STATIC_ACCESS_KEY",
438-
aws_secret_access_key: "ASSUME_STATIC_SECRET_KEY",
439-
},
440-
};
441-
}
442-
setIniProfileData(iniProfileData);
443-
444-
const provider = fromIni({
445-
profile,
446-
clientConfig: {
447-
region: providerRegion ? "provider-region" : undefined,
448-
requestHandler: new MockNodeHttpHandler(),
449-
},
450-
});
451-
452-
if (withCaller) {
453-
const sts = new STS({
454-
profile,
455-
requestHandler: new MockNodeHttpHandler(),
456-
region: codeRegion ? "code-region" : undefined,
457-
credentials: provider,
458-
});
459-
460-
await sts.getCallerIdentity({});
461-
const credentials = await sts.config.credentials();
462-
return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", "");
463-
}
464-
465-
const credentials = await provider();
466-
return credentials.sessionToken!.replace("STS_AR_SESSION_TOKEN_", "");
467-
}
468-
});
469309
});

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ describe(defaultProvider.name, () => {
2626
};
2727

2828
const credentials = () => {
29-
throw new CredentialsProviderError("test", true);
29+
throw new CredentialsProviderError("test", {
30+
tryNextLink: true,
31+
});
3032
};
3133

3234
const finalCredentials = () => {

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

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import type { FromIniInit } from "@aws-sdk/credential-provider-ini";
44
import type { FromProcessInit } from "@aws-sdk/credential-provider-process";
55
import type { FromSSOInit, SsoCredentialsParameters } from "@aws-sdk/credential-provider-sso";
66
import type { FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
7+
import type { AwsIdentityProperties } from "@aws-sdk/types";
78
import type { RemoteProviderInit } from "@smithy/credential-provider-imds";
8-
import { chain, CredentialsProviderError, memoize } from "@smithy/property-provider";
9+
import { CredentialsProviderError } from "@smithy/property-provider";
910
import { ENV_PROFILE } from "@smithy/shared-ini-file-loader";
10-
import { AwsCredentialIdentity, MemoizedProvider } from "@smithy/types";
11+
import type { AwsCredentialIdentity } from "@smithy/types";
1112

1213
import { remoteProvider } from "./remoteProvider";
14+
import { type MemoizedRuntimeConfigAwsCredentialIdentityProvider, memoizeChain } from "./runtime/memoize-chain";
1315

1416
/**
1517
* @public
@@ -60,9 +62,9 @@ let multipleCredentialSourceWarningEmitted = false;
6062
* @see {@link fromContainerMetadata} The function used to source credentials from the
6163
* ECS Container Metadata Service.
6264
*/
63-
export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvider<AwsCredentialIdentity> =>
64-
memoize(
65-
chain(
65+
export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedRuntimeConfigAwsCredentialIdentityProvider =>
66+
memoizeChain(
67+
[
6668
async () => {
6769
const profile = init.profile ?? process.env[ENV_PROFILE];
6870
if (profile) {
@@ -95,7 +97,7 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide
9597
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromEnv");
9698
return fromEnv(init)();
9799
},
98-
async () => {
100+
async (awsIdentityProperties?: AwsIdentityProperties) => {
99101
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromSSO");
100102
const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoSession } = init;
101103
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName && !ssoSession) {
@@ -105,22 +107,22 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide
105107
);
106108
}
107109
const { fromSSO } = await import("@aws-sdk/credential-provider-sso");
108-
return fromSSO(init)();
110+
return fromSSO(init)(awsIdentityProperties);
109111
},
110-
async () => {
112+
async (awsIdentityProperties?: AwsIdentityProperties) => {
111113
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromIni");
112114
const { fromIni } = await import("@aws-sdk/credential-provider-ini");
113-
return fromIni(init)();
115+
return fromIni(init)(awsIdentityProperties);
114116
},
115-
async () => {
117+
async (awsIdentityProperties?: AwsIdentityProperties) => {
116118
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromProcess");
117119
const { fromProcess } = await import("@aws-sdk/credential-provider-process");
118-
return fromProcess(init)();
120+
return fromProcess(init)(awsIdentityProperties);
119121
},
120-
async () => {
122+
async (awsIdentityProperties?: AwsIdentityProperties) => {
121123
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::fromTokenFile");
122124
const { fromTokenFile } = await import("@aws-sdk/credential-provider-web-identity");
123-
return fromTokenFile(init)();
125+
return fromTokenFile(init)(awsIdentityProperties);
124126
},
125127
async () => {
126128
init.logger?.debug("@aws-sdk/credential-provider-node - defaultProvider::remoteProvider");
@@ -131,10 +133,9 @@ export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvide
131133
tryNextLink: false,
132134
logger: init.logger,
133135
});
134-
}
135-
),
136-
credentialsTreatedAsExpired,
137-
credentialsWillNeedRefresh
136+
},
137+
],
138+
credentialsTreatedAsExpired
138139
);
139140

140141
/**
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { AwsIdentityProperties, RuntimeConfigAwsCredentialIdentityProvider } from "@aws-sdk/types";
2+
import { beforeEach, describe, expect, test as it, vi } from "vitest";
3+
4+
import { credentialsWillNeedRefresh } from "../defaultProvider";
5+
import { memoizeChain } from "./memoize-chain";
6+
7+
describe("memoize runtime config aware AWS credential chain", () => {
8+
let staticCredentials!: RuntimeConfigAwsCredentialIdentityProvider;
9+
let expiringCredentials!: RuntimeConfigAwsCredentialIdentityProvider;
10+
11+
const expiration = new Date();
12+
13+
beforeEach(() => {
14+
vi.resetAllMocks();
15+
staticCredentials = vi.fn().mockImplementation(async (options?: AwsIdentityProperties) => {
16+
await new Promise((r) => setTimeout(r, 100));
17+
return {
18+
accessKeyId: "",
19+
secretAccessKey: "",
20+
runtimeOptions: Object.keys(options ?? {}).concat(Object.keys(options?.callerClientConfig ?? {})),
21+
};
22+
});
23+
24+
let sequence = 0;
25+
26+
expiringCredentials = vi.fn().mockImplementation(async (options?: AwsIdentityProperties) => {
27+
await new Promise((r) => setTimeout(r, 100));
28+
return {
29+
accessKeyId: "",
30+
secretAccessKey: "",
31+
expiration,
32+
sequence: sequence++,
33+
runtimeOptions: Object.keys(options ?? {}).concat(Object.keys(options?.callerClientConfig ?? {})),
34+
};
35+
});
36+
});
37+
38+
it("should call composed provider functions", async () => {
39+
const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh);
40+
41+
const credentials = await provider({
42+
callerClientConfig: {
43+
region: async () => "context-region",
44+
profile: "alt",
45+
},
46+
});
47+
48+
expect(credentials).toEqual({
49+
accessKeyId: "",
50+
secretAccessKey: "",
51+
runtimeOptions: ["callerClientConfig", "region", "profile"],
52+
});
53+
expect(staticCredentials).toHaveBeenCalledTimes(1);
54+
});
55+
56+
it("should use an active lock when no credentials exist", async () => {
57+
const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh);
58+
59+
const [credentials] = await Promise.all([provider(), provider(), provider(), provider(), provider()]);
60+
61+
expect(credentials).toEqual({
62+
accessKeyId: "",
63+
secretAccessKey: "",
64+
runtimeOptions: [],
65+
});
66+
expect(staticCredentials).toHaveBeenCalledTimes(1);
67+
});
68+
69+
it("should use a cache", async () => {
70+
const provider = memoizeChain([staticCredentials], credentialsWillNeedRefresh);
71+
72+
await Promise.all([provider(), provider(), provider(), provider(), provider()]);
73+
const [credentials] = await Promise.all([provider(), provider(), provider(), provider(), provider()]);
74+
75+
expect(credentials).toEqual({
76+
accessKeyId: "",
77+
secretAccessKey: "",
78+
runtimeOptions: [],
79+
});
80+
expect(staticCredentials).toHaveBeenCalledTimes(1);
81+
});
82+
83+
it("should use a passive lock when credentials do exist", async () => {
84+
const provider = memoizeChain([expiringCredentials], credentialsWillNeedRefresh);
85+
86+
{
87+
// initial invocation returns sequence-0 credentials.
88+
const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]);
89+
for (const c of credentials) {
90+
expect(c).toEqual({
91+
accessKeyId: "",
92+
secretAccessKey: "",
93+
expiration,
94+
sequence: 0,
95+
runtimeOptions: [],
96+
});
97+
}
98+
expect(expiringCredentials).toHaveBeenCalledTimes(1);
99+
}
100+
101+
{
102+
// second invocation returns sequence-0 credentials, but background initializes refresh.
103+
const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]);
104+
for (const c of credentials) {
105+
expect(c).toEqual({
106+
accessKeyId: "",
107+
secretAccessKey: "",
108+
expiration,
109+
sequence: 0,
110+
runtimeOptions: [],
111+
});
112+
}
113+
expect(expiringCredentials).toHaveBeenCalledTimes(2);
114+
}
115+
116+
// allow new credentials to settle
117+
await new Promise((r) => setTimeout(r, 200));
118+
119+
{
120+
// third invocation group returns sequence-1 credentials, also with background refresh.
121+
const credentials = await Promise.all([provider(), provider(), provider(), provider(), provider()]);
122+
for (const c of credentials) {
123+
expect(c).toEqual({
124+
accessKeyId: "",
125+
secretAccessKey: "",
126+
expiration,
127+
sequence: 1,
128+
runtimeOptions: [],
129+
});
130+
}
131+
expect(expiringCredentials).toHaveBeenCalledTimes(3);
132+
}
133+
});
134+
});

0 commit comments

Comments
 (0)