Skip to content

Commit 90b3b53

Browse files
committed
Embed chain continuation flag in provider promise rejections
1 parent cb856cd commit 90b3b53

File tree

17 files changed

+181
-78
lines changed

17 files changed

+181
-78
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
*.js
3+
*.js.map
4+
*.d.ts

packages/credential-provider/__tests__/chain.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {chain} from "../lib/chain";
22
import {fromCredentials} from "../lib/fromCredentials";
33
import {isCredentials} from "../lib/isCredentials";
4+
import {CredentialError} from "../lib/CredentialError";
45

56
describe('chain', () => {
67
it('should distill many credential providers into one', async () => {
@@ -15,8 +16,8 @@ describe('chain', () => {
1516
it('should return the resolved value of the first successful promise', async () => {
1617
const creds = {accessKeyId: 'foo', secretAccessKey: 'bar'};
1718
const provider = chain(
18-
() => Promise.reject('Move along'),
19-
() => Promise.reject('Nothing to see here'),
19+
() => Promise.reject(new CredentialError('Move along')),
20+
() => Promise.reject(new CredentialError('Nothing to see here')),
2021
fromCredentials(creds)
2122
);
2223

@@ -26,7 +27,7 @@ describe('chain', () => {
2627
it('should not invoke subsequent providers one resolves', async () => {
2728
const creds = {accessKeyId: 'foo', secretAccessKey: 'bar'};
2829
const providers = [
29-
jest.fn(() => Promise.reject('Move along')),
30+
jest.fn(() => Promise.reject(new CredentialError('Move along'))),
3031
jest.fn(() => Promise.resolve(creds)),
3132
jest.fn(() => fail('This provider should not be invoked'))
3233
];

packages/credential-provider/__tests__/fromContainerMetadata.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ describe('fromContainerMetadata', () => {
4242
delete process.env[ENV_CMDS_RELATIVE_URI];
4343
await fromContainerMetadata()().then(
4444
() => { throw new Error('The promise should have been rejected'); },
45-
() => { /* promise rejected, as expected */ }
45+
err => {
46+
expect((err as any).tryNextLink).toBeFalsy();
47+
}
4648
);
4749
}
4850
);

packages/credential-provider/__tests__/fromEnv.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ENV_SESSION,
55
fromEnv,
66
} from "../lib/fromEnv";
7+
import {CredentialError} from "../lib/CredentialError";
78

89
const akid = process.env[ENV_KEY];
910
const secret = process.env[ENV_SECRET];
@@ -54,4 +55,13 @@ describe('fromEnv', () => {
5455
);
5556
}
5657
);
58+
59+
it('should flag a lack of credentials as a non-terminal error', async () => {
60+
await fromEnv()().then(
61+
() => { throw new Error('The promise should have been rejected.'); },
62+
err => {
63+
expect((err as CredentialError).tryNextLink).toBe(true);
64+
}
65+
);
66+
});
5767
});

packages/credential-provider/__tests__/fromIni.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ENV_PROFILE,
1414
fromIni
1515
} from "../lib/fromIni";
16+
import {CredentialError} from "../lib/CredentialError";
1617

1718
const DEFAULT_CREDS = {
1819
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
@@ -54,6 +55,15 @@ afterAll(() => {
5455
});
5556

5657
describe('fromIni', () => {
58+
it('should flag a lack of credentials as a non-terminal error', async () => {
59+
await fromIni()().then(
60+
() => { throw new Error('The promise should have been rejected.'); },
61+
err => {
62+
expect((err as CredentialError).tryNextLink).toBe(true);
63+
}
64+
);
65+
});
66+
5767
describe('shared credentials file', () => {
5868
const SIMPLE_CREDS_FILE = `
5969
[default]
@@ -100,7 +110,7 @@ aws_session_token = ${FOO_CREDS.sessionToken}`.trim();
100110
async () => {
101111
process.env[ENV_CREDENTIALS_PATH] = join('foo', 'bar', 'baz');
102112
__addMatcher(
103-
process.env[ENV_CREDENTIALS_PATH],
113+
process.env[ENV_CREDENTIALS_PATH],
104114
SIMPLE_CREDS_FILE
105115
);
106116

@@ -464,7 +474,7 @@ source_profile = default`.trim()
464474
});
465475

466476
it(
467-
'should reject the promise if no role assumer provided',
477+
'should reject the promise with a terminal error if no role assumer provided',
468478
async () => {
469479
__addMatcher(join(homedir(), '.aws', 'credentials'), `
470480
[default]
@@ -479,7 +489,9 @@ source_profile = bar`.trim()
479489

480490
await fromIni({profile: 'foo'})().then(
481491
() => { throw new Error('The promise should have been rejected'); },
482-
() => { /* Promise rejected as expected */ }
492+
err => {
493+
expect((err as any).tryNextLink).toBeFalsy();
494+
}
483495
);
484496
}
485497
);
@@ -679,7 +691,7 @@ source_profile = default`.trim()
679691
);
680692

681693
it(
682-
'should reject the promise if a MFA serial is present but no mfaCodeProvider was provided',
694+
'should reject the promise with a terminal error if a MFA serial is present but no mfaCodeProvider was provided',
683695
async () => {
684696
const roleArn = 'arn:aws:iam::123456789:role/foo';
685697
const mfaSerial = 'mfaSerial';
@@ -702,7 +714,9 @@ source_profile = default`.trim()
702714

703715
await provider().then(
704716
() => { throw new Error('The promise should have been rejected'); },
705-
() => { /* Promise rejected as expected */ }
717+
err => {
718+
expect((err as any).tryNextLink).toBeFalsy();
719+
}
706720
);
707721
}
708722
);

packages/credential-provider/__tests__/fromInstanceMetadata.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,6 @@ describe('fromInstanceMetadata', () => {
9797
});
9898
});
9999

100-
101-
102100
it('should retry the profile name fetch as necessary', async () => {
103101
const defaultTimeout = 1000;
104102
const profile = 'foo-profile';

packages/credential-provider/__tests__/remoteProvider/httpGet.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {createServer} from 'http';
22
import {httpGet} from "../../lib/remoteProvider/httpGet";
3+
import {CredentialError} from "../../lib/CredentialError";
34

45
const matchers = new Map<string, string>();
56

@@ -55,14 +56,37 @@ describe('httpGet', () => {
5556
.toEqual(expectedResponse);
5657
});
5758

58-
it('should reject the promise if a 404 status code is received', async () => {
59-
addMatcher('/fizz', 'buzz');
59+
it(
60+
'should reject the promise with a non-terminal error if a 404 status code is received',
61+
async () => {
62+
addMatcher('/fizz', 'buzz');
6063

61-
await httpGet(`http://localhost:${port}/foo`).then(
62-
() => { throw new Error('The promise should have been rejected'); },
63-
() => { /* promise rejected, as expected */ }
64-
);
65-
});
64+
await httpGet(`http://localhost:${port}/foo`).then(
65+
() => {
66+
throw new Error('The promise should have been rejected');
67+
},
68+
err => {
69+
expect((err as CredentialError).tryNextLink).toBe(true);
70+
}
71+
);
72+
}
73+
);
74+
75+
it(
76+
'should reject the promise with a non-terminal error if the remote server cannot be contacted',
77+
async () => {
78+
server.close();
79+
80+
await httpGet(`http://localhost:${port}/foo`).then(
81+
() => {
82+
throw new Error('The promise should have been rejected');
83+
},
84+
err => {
85+
expect((err as CredentialError).tryNextLink).toBe(true);
86+
}
87+
);
88+
}
89+
);
6690
});
6791

6892

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class CredentialError extends Error {
2+
constructor(message: string, public readonly tryNextLink: boolean = true) {
3+
super(message);
4+
}
5+
}

packages/credential-provider/lib/chain.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
1-
import {CredentialProvider, Credentials} from "@aws/types";
1+
import {CredentialProvider} from "@aws/types";
2+
import {CredentialError} from "./CredentialError";
23

3-
export function chain(...providers: Array<CredentialProvider>): CredentialProvider {
4+
export function chain(
5+
...providers: Array<CredentialProvider>
6+
): CredentialProvider {
47
return () => {
58
providers = providers.slice(0);
69
let provider = providers.shift();
710
if (provider === undefined) {
8-
return Promise.reject<Credentials>('No credential providers in chain');
11+
return Promise.reject(new CredentialError(
12+
'No credential providers in chain'
13+
));
914
}
1015
let promise = provider();
1116
while (provider = providers.shift()) {
12-
promise = promise.catch(provider);
17+
promise = promise.catch((provider => {
18+
return (err: CredentialError) => {
19+
if (err.tryNextLink) {
20+
return provider();
21+
}
22+
23+
throw err;
24+
}
25+
})(provider));
1326
}
1427

1528
return promise;

packages/credential-provider/lib/fromContainerMetadata.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isImdsCredentials,
1010
} from './remoteProvider/ImdsCredentials';
1111
import {retry} from './remoteProvider/retry';
12+
import {CredentialError} from "./CredentialError";
1213

1314
export const ENV_CMDS_RELATIVE_URI = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI';
1415

@@ -20,17 +21,20 @@ export function fromContainerMetadata(
2021
const path = process.env[ENV_CMDS_RELATIVE_URI];
2122

2223
if (!path) {
23-
return Promise.reject(new Error('The container metadata credential'
24-
+ ` provider cannot be used unless the ${ENV_CMDS_RELATIVE_URI}`
25-
+ ' environment variable is set'));
24+
return Promise.reject(new CredentialError(
25+
'The container metadata credential provider cannot be used' +
26+
` unless the ${ENV_CMDS_RELATIVE_URI} environment variable` +
27+
' is set',
28+
false
29+
));
2630
}
2731

2832
return retry(async () => {
2933
const credsResponse = JSON.parse(
3034
await requestFromEcsImds(timeout, path)
3135
);
3236
if (!isImdsCredentials(credsResponse)) {
33-
throw new Error(
37+
throw new CredentialError(
3438
'Invalid response received from instance metadata service.'
3539
);
3640
}
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {CredentialProvider, Credentials} from "@aws/types";
1+
import {CredentialProvider} from "@aws/types";
2+
import {CredentialError} from "./CredentialError";
23

34
export const ENV_KEY = 'AWS_ACCESS_KEY_ID';
45
export const ENV_SECRET = 'AWS_SECRET_ACCESS_KEY';
@@ -9,13 +10,15 @@ export function fromEnv(): CredentialProvider {
910
const accessKeyId: string = process.env[ENV_KEY];
1011
const secretAccessKey: string = process.env[ENV_SECRET];
1112
if (accessKeyId && secretAccessKey) {
12-
return Promise.resolve<Credentials>({
13+
return Promise.resolve({
1314
accessKeyId,
1415
secretAccessKey,
1516
sessionToken: process.env[ENV_SESSION],
1617
});
1718
}
1819

19-
return Promise.reject<Credentials>('Unable to find environment variable credentials.');
20+
return Promise.reject(new CredentialError(
21+
'Unable to find environment variable credentials.'
22+
));
2023
};
2124
}

0 commit comments

Comments
 (0)