Skip to content

Commit 1cdd3be

Browse files
authored
chore: add logging for CredentialsProviderError (#1290)
* chore: add logging for CredentialsProviderError * use options object * var name clarity * test fixes * formatting * set internal tag on getSelectorName
1 parent 764047e commit 1cdd3be

File tree

13 files changed

+238
-87
lines changed

13 files changed

+238
-87
lines changed

.changeset/late-glasses-jam.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@smithy/credential-provider-imds": minor
3+
"@smithy/shared-ini-file-loader": minor
4+
"@smithy/node-config-provider": minor
5+
"@smithy/property-provider": minor
6+
---
7+
8+
new logging-compatible signature for CredentialsProviderError

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

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CredentialsProviderError } from "@smithy/property-provider";
2-
import { AwsCredentialIdentityProvider } from "@smithy/types";
2+
import { AwsCredentialIdentityProvider, Logger } from "@smithy/types";
33
import { RequestOptions } from "http";
44
import { parse } from "url";
55

@@ -31,10 +31,12 @@ export const fromContainerMetadata = (init: RemoteProviderInit = {}): AwsCredent
3131
const { timeout, maxRetries } = providerConfigFromInit(init);
3232
return () =>
3333
retry(async () => {
34-
const requestOptions = await getCmdsUri();
34+
const requestOptions = await getCmdsUri({ logger: init.logger });
3535
const credsResponse = JSON.parse(await requestFromEcsImds(timeout, requestOptions));
3636
if (!isImdsCredentials(credsResponse)) {
37-
throw new CredentialsProviderError("Invalid response received from instance metadata service.");
37+
throw new CredentialsProviderError("Invalid response received from instance metadata service.", {
38+
logger: init.logger,
39+
});
3840
}
3941
return fromImdsCredentials(credsResponse);
4042
}, maxRetries);
@@ -65,7 +67,7 @@ const GREENGRASS_PROTOCOLS = {
6567
"https:": true,
6668
};
6769

68-
const getCmdsUri = async (): Promise<RequestOptions> => {
70+
const getCmdsUri = async ({ logger }: { logger?: Logger }): Promise<RequestOptions> => {
6971
if (process.env[ENV_CMDS_RELATIVE_URI]) {
7072
return {
7173
hostname: CMDS_IP,
@@ -76,17 +78,17 @@ const getCmdsUri = async (): Promise<RequestOptions> => {
7678
if (process.env[ENV_CMDS_FULL_URI]) {
7779
const parsed = parse(process.env[ENV_CMDS_FULL_URI]!);
7880
if (!parsed.hostname || !(parsed.hostname in GREENGRASS_HOSTS)) {
79-
throw new CredentialsProviderError(
80-
`${parsed.hostname} is not a valid container metadata service hostname`,
81-
false
82-
);
81+
throw new CredentialsProviderError(`${parsed.hostname} is not a valid container metadata service hostname`, {
82+
tryNextLink: false,
83+
logger,
84+
});
8385
}
8486

8587
if (!parsed.protocol || !(parsed.protocol in GREENGRASS_PROTOCOLS)) {
86-
throw new CredentialsProviderError(
87-
`${parsed.protocol} is not a valid container metadata service protocol`,
88-
false
89-
);
88+
throw new CredentialsProviderError(`${parsed.protocol} is not a valid container metadata service protocol`, {
89+
tryNextLink: false,
90+
logger,
91+
});
9092
}
9193

9294
return {
@@ -99,6 +101,9 @@ const getCmdsUri = async (): Promise<RequestOptions> => {
99101
"The container metadata credential provider cannot be used unless" +
100102
` the ${ENV_CMDS_RELATIVE_URI} or ${ENV_CMDS_FULL_URI} environment` +
101103
" variable is set",
102-
false
104+
{
105+
tryNextLink: false,
106+
logger,
107+
}
103108
);
104109
};

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ const X_AWS_EC2_METADATA_TOKEN = "x-aws-ec2-metadata-token";
2525
* Instance Metadata Service
2626
*/
2727
export const fromInstanceMetadata = (init: RemoteProviderInit = {}): Provider<InstanceMetadataCredentials> =>
28-
staticStabilityProvider(getInstanceImdsProvider(init), { logger: init.logger });
28+
staticStabilityProvider(getInstanceMetadataProvider(init), { logger: init.logger });
2929

30-
const getInstanceImdsProvider = (init: RemoteProviderInit) => {
30+
/**
31+
* @internal
32+
*/
33+
const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
3134
// when set to true, metadata service will not fetch token
3235
let disableFetchToken = false;
3336
const { logger, profile } = init;
@@ -47,7 +50,8 @@ const getInstanceImdsProvider = (init: RemoteProviderInit) => {
4750
fallbackBlockedFromProcessEnv = !!envValue && envValue !== "false";
4851
if (envValue === undefined) {
4952
throw new CredentialsProviderError(
50-
`${AWS_EC2_METADATA_V1_DISABLED} not set in env, checking config file next.`
53+
`${AWS_EC2_METADATA_V1_DISABLED} not set in env, checking config file next.`,
54+
{ logger: init.logger }
5155
);
5256
}
5357
return fallbackBlockedFromProcessEnv;
@@ -98,7 +102,7 @@ const getInstanceImdsProvider = (init: RemoteProviderInit) => {
98102
return retry(async () => {
99103
let creds: AwsCredentialIdentity;
100104
try {
101-
creds = await getCredentialsFromProfile(imdsProfile, options);
105+
creds = await getCredentialsFromProfile(imdsProfile, options, init);
102106
} catch (err) {
103107
if (err.statusCode === 401) {
104108
disableFetchToken = false;
@@ -152,8 +156,8 @@ const getMetadataToken = async (options: RequestOptions) =>
152156

153157
const getProfile = async (options: RequestOptions) => (await httpRequest({ ...options, path: IMDS_PATH })).toString();
154158

155-
const getCredentialsFromProfile = async (profile: string, options: RequestOptions) => {
156-
const credsResponse = JSON.parse(
159+
const getCredentialsFromProfile = async (profile: string, options: RequestOptions, init: RemoteProviderInit) => {
160+
const credentialsResponse = JSON.parse(
157161
(
158162
await httpRequest({
159163
...options,
@@ -162,9 +166,11 @@ const getCredentialsFromProfile = async (profile: string, options: RequestOption
162166
).toString()
163167
);
164168

165-
if (!isImdsCredentials(credsResponse)) {
166-
throw new CredentialsProviderError("Invalid response received from instance metadata service.");
169+
if (!isImdsCredentials(credentialsResponse)) {
170+
throw new CredentialsProviderError("Invalid response received from instance metadata service.", {
171+
logger: init.logger,
172+
});
167173
}
168174

169-
return fromImdsCredentials(credsResponse);
175+
return fromImdsCredentials(credentialsResponse);
170176
};

packages/node-config-provider/src/fromEnv.spec.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,30 @@ import { fromEnv, GetterFromEnv } from "./fromEnv";
44

55
describe("fromEnv", () => {
66
describe("with env var getter", () => {
7-
const envVarName = "ENV_VAR_NAME";
7+
const ENV_VAR_NAME = "ENV_VAR_NAME";
88

99
// Using Record<string, string | undefined> instead of NodeJS.ProcessEnv, in order to not get type errors in non node environments
10-
const envVarGetter: GetterFromEnv<string> = (env: Record<string, string | undefined>) => env[envVarName]!;
11-
const envVarValue = process.env[envVarName];
10+
const envVarGetter: GetterFromEnv<string> = (env: Record<string, string | undefined>) => env[ENV_VAR_NAME]!;
11+
const envVarValue = process.env[ENV_VAR_NAME];
1212
const mockEnvVarValue = "mockEnvVarValue";
1313

14-
const getCredentialsProviderError = (getter: GetterFromEnv<string>) =>
15-
new CredentialsProviderError(`Cannot load config from environment variables with getter: ${getter}`);
16-
1714
beforeEach(() => {
18-
delete process.env[envVarName];
15+
delete process.env[ENV_VAR_NAME];
1916
});
2017

2118
afterAll(() => {
22-
process.env[envVarName] = envVarValue;
19+
process.env[ENV_VAR_NAME] = envVarValue;
20+
});
21+
22+
describe("CredentialsProviderError", () => {
23+
it("is behaving as expected cross-package in jest", () => {
24+
expect(new CredentialsProviderError("msg", {}).message).toEqual("msg");
25+
expect(new CredentialsProviderError("msg", {}).name).toEqual("CredentialsProviderError");
26+
});
2327
});
2428

25-
it(`returns string value in '${envVarName}' env var when set`, () => {
26-
process.env[envVarName] = mockEnvVarValue;
29+
it(`returns string value in '${ENV_VAR_NAME}' env var when set`, () => {
30+
process.env[ENV_VAR_NAME] = mockEnvVarValue;
2731
return expect(fromEnv(envVarGetter)()).resolves.toBe(mockEnvVarValue);
2832
});
2933

@@ -35,9 +39,10 @@ describe("fromEnv", () => {
3539
return expect(fromEnv(getter)()).resolves.toEqual(value);
3640
});
3741

38-
it(`throws when '${envVarName}' env var is not set`, () => {
42+
it(`throws when '${ENV_VAR_NAME}' env var is not set`, async () => {
3943
expect.assertions(1);
40-
return expect(fromEnv(envVarGetter)()).rejects.toMatchObject(getCredentialsProviderError(envVarGetter));
44+
const error = await fromEnv(envVarGetter)().catch((_) => _);
45+
return expect(error).toEqual(new CredentialsProviderError(`Not found in ENV: ENV_VAR_NAME`, {}));
4146
});
4247

4348
it("throws when the getter function throws", () => {

packages/node-config-provider/src/fromEnv.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { CredentialsProviderError } from "@smithy/property-provider";
2-
import { Provider } from "@smithy/types";
2+
import { Logger, Provider } from "@smithy/types";
3+
4+
import { getSelectorName } from "./getSelectorName";
35

46
// Using Record<string, string | undefined> instead of NodeJS.ProcessEnv, in order to not get type errors in non node environments
57
export type GetterFromEnv<T> = (env: Record<string, string | undefined>) => T | undefined;
@@ -9,7 +11,7 @@ export type GetterFromEnv<T> = (env: Record<string, string | undefined>) => T |
911
* environment variable.
1012
*/
1113
export const fromEnv =
12-
<T = string>(envVarSelector: GetterFromEnv<T>): Provider<T> =>
14+
<T = string>(envVarSelector: GetterFromEnv<T>, logger?: Logger): Provider<T> =>
1315
async () => {
1416
try {
1517
const config = envVarSelector(process.env);
@@ -19,7 +21,8 @@ export const fromEnv =
1921
return config as T;
2022
} catch (e) {
2123
throw new CredentialsProviderError(
22-
e.message || `Cannot load config from environment variables with getter: ${envVarSelector}`
24+
e.message || `Not found in ENV: ${getSelectorName(envVarSelector.toString())}`,
25+
{ logger }
2326
);
2427
}
2528
};

packages/node-config-provider/src/fromSharedConfigFiles.spec.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@ jest.mock("@smithy/shared-ini-file-loader", () => ({
1010
}));
1111

1212
describe("fromSharedConfigFiles", () => {
13-
const configKey = "config_key";
14-
const configGetter: GetterFromConfig<string> = (profile: Profile) => profile[configKey];
13+
const CONFIG_KEY = "config_key";
14+
const configGetter: GetterFromConfig<string> = (profile: Profile) => profile[CONFIG_KEY];
1515

16-
const getCredentialsProviderError = (profile: string, getter: GetterFromConfig<string>) =>
17-
new CredentialsProviderError(
18-
`Cannot load config for profile ${profile} in SDK configuration files with getter: ${getter}`
19-
);
16+
const getCredentialsProviderError = (profile: string) =>
17+
new CredentialsProviderError(`Not found in config files w/ profile [${profile}]: CONFIG_KEY`, {});
2018

2119
describe("loadedConfig", () => {
2220
const mockConfigAnswer = "mockConfigAnswer";
@@ -36,33 +34,33 @@ describe("fromSharedConfigFiles", () => {
3634
{
3735
message: "returns configValue from default profile",
3836
iniDataInConfig: {
39-
default: { [configKey]: mockConfigAnswer },
37+
default: { [CONFIG_KEY]: mockConfigAnswer },
4038
},
4139
iniDataInCredentials: {
42-
default: { [configKey]: mockCredentialsNotAnswer },
40+
default: { [CONFIG_KEY]: mockCredentialsNotAnswer },
4341
},
4442
configValueToVerify: mockConfigAnswer,
4543
},
4644
{
4745
message: "returns configValue from designated profile",
4846
iniDataInConfig: {
49-
default: { [configKey]: mockConfigNotAnswer },
50-
foo: { [configKey]: mockConfigAnswer },
47+
default: { [CONFIG_KEY]: mockConfigNotAnswer },
48+
foo: { [CONFIG_KEY]: mockConfigAnswer },
5149
},
5250
iniDataInCredentials: {
53-
foo: { [configKey]: mockCredentialsNotAnswer },
51+
foo: { [CONFIG_KEY]: mockCredentialsNotAnswer },
5452
},
5553
profile: "foo",
5654
configValueToVerify: mockConfigAnswer,
5755
},
5856
{
5957
message: "returns configValue from credentials file if preferred",
6058
iniDataInConfig: {
61-
default: { [configKey]: mockConfigNotAnswer },
62-
foo: { [configKey]: mockConfigNotAnswer },
59+
default: { [CONFIG_KEY]: mockConfigNotAnswer },
60+
foo: { [CONFIG_KEY]: mockConfigNotAnswer },
6361
},
6462
iniDataInCredentials: {
65-
foo: { [configKey]: mockCredentialsAnswer },
63+
foo: { [CONFIG_KEY]: mockCredentialsAnswer },
6664
},
6765
profile: "foo",
6866
preferredFile: "credentials",
@@ -71,7 +69,7 @@ describe("fromSharedConfigFiles", () => {
7169
{
7270
message: "returns configValue from config file if preferred credentials file doesn't contain config",
7371
iniDataInConfig: {
74-
foo: { [configKey]: mockConfigAnswer },
72+
foo: { [CONFIG_KEY]: mockConfigAnswer },
7573
},
7674
iniDataInCredentials: {},
7775
configValueToVerify: mockConfigAnswer,
@@ -82,7 +80,7 @@ describe("fromSharedConfigFiles", () => {
8280
message: "returns configValue from credential file if preferred config file doesn't contain config",
8381
iniDataInConfig: {},
8482
iniDataInCredentials: {
85-
foo: { [configKey]: mockCredentialsAnswer },
83+
foo: { [CONFIG_KEY]: mockCredentialsAnswer },
8684
},
8785
configValueToVerify: mockCredentialsAnswer,
8886
profile: "foo",
@@ -93,14 +91,14 @@ describe("fromSharedConfigFiles", () => {
9391
{
9492
message: "rejects if default profile is not present and profile value is not passed",
9593
iniDataInConfig: {
96-
foo: { [configKey]: mockConfigNotAnswer },
94+
foo: { [CONFIG_KEY]: mockConfigNotAnswer },
9795
},
9896
iniDataInCredentials: {},
9997
},
10098
{
10199
message: "rejects if designated profile is not present",
102100
iniDataInConfig: {
103-
default: { [configKey]: mockConfigNotAnswer },
101+
default: { [CONFIG_KEY]: mockConfigNotAnswer },
104102
},
105103
iniDataInCredentials: {},
106104
profile: "foo",
@@ -129,8 +127,8 @@ describe("fromSharedConfigFiles", () => {
129127
credentialsFile: iniDataInCredentials,
130128
});
131129
(getProfileName as jest.Mock).mockReturnValueOnce(profile ?? "default");
132-
return expect(fromSharedConfigFiles(configGetter, { profile, preferredFile })()).rejects.toMatchObject(
133-
getCredentialsProviderError(profile ?? "default", configGetter)
130+
return expect(fromSharedConfigFiles(configGetter, { profile, preferredFile })()).rejects.toEqual(
131+
getCredentialsProviderError(profile ?? "default")
134132
);
135133
});
136134
});
@@ -144,18 +142,18 @@ describe("fromSharedConfigFiles", () => {
144142
configFile: {},
145143
credentialsFile: {},
146144
});
147-
return expect(fromSharedConfigFiles(failGetter)()).rejects.toMatchObject(new CredentialsProviderError(message));
145+
return expect(fromSharedConfigFiles(failGetter)()).rejects.toEqual(new CredentialsProviderError(message));
148146
});
149147
});
150148

151149
describe("profile", () => {
152150
const loadedConfigData = {
153151
configFile: {
154-
default: { [configKey]: "configFileDefault" },
155-
foo: { [configKey]: "configFileFoo" },
152+
default: { [CONFIG_KEY]: "configFileDefault" },
153+
foo: { [CONFIG_KEY]: "configFileFoo" },
156154
},
157155
credentialsFile: {
158-
default: { [configKey]: "credentialsFileDefault" },
156+
default: { [CONFIG_KEY]: "credentialsFileDefault" },
159157
},
160158
};
161159

@@ -166,7 +164,7 @@ describe("fromSharedConfigFiles", () => {
166164
it.each(["foo", "default"])("returns config value from %s profile", (profile) => {
167165
(getProfileName as jest.Mock).mockReturnValueOnce(profile);
168166
return expect(fromSharedConfigFiles(configGetter)()).resolves.toBe(
169-
loadedConfigData.configFile[profile][configKey]
167+
loadedConfigData.configFile[profile][CONFIG_KEY]
170168
);
171169
});
172170
});

packages/node-config-provider/src/fromSharedConfigFiles.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { CredentialsProviderError } from "@smithy/property-provider";
22
import { getProfileName, loadSharedConfigFiles, SourceProfileInit } from "@smithy/shared-ini-file-loader";
33
import { ParsedIniData, Profile, Provider } from "@smithy/types";
44

5+
import { getSelectorName } from "./getSelectorName";
6+
57
export interface SharedConfigInit extends SourceProfileInit {
68
/**
79
* The preferred shared ini file to load the config. "config" option refers to
@@ -41,8 +43,8 @@ export const fromSharedConfigFiles =
4143
return configValue;
4244
} catch (e) {
4345
throw new CredentialsProviderError(
44-
e.message ||
45-
`Cannot load config for profile ${profile} in SDK configuration files with getter: ${configSelector}`
46+
e.message || `Not found in config files w/ profile [${profile}]: ${getSelectorName(configSelector.toString())}`,
47+
{ logger: init.logger }
4648
);
4749
}
4850
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Attempts to extract the name of the variable that the functional selector is looking for.
3+
* Improves readability over the raw Function.toString() value.
4+
* @internal
5+
* @param functionString - function's string representation.
6+
*
7+
* @returns constant value used within the function.
8+
*/
9+
export function getSelectorName(functionString: string): string {
10+
try {
11+
const constants = new Set(Array.from(functionString.match(/([A-Z_]){3,}/g) ?? []));
12+
constants.delete("CONFIG");
13+
constants.delete("CONFIG_PREFIX_SEPARATOR");
14+
constants.delete("ENV");
15+
return [...constants].join(", ");
16+
} catch (e) {
17+
return functionString;
18+
}
19+
}

0 commit comments

Comments
 (0)