diff --git a/.changeset/late-glasses-jam.md b/.changeset/late-glasses-jam.md new file mode 100644 index 00000000000..1f45828f3f5 --- /dev/null +++ b/.changeset/late-glasses-jam.md @@ -0,0 +1,8 @@ +--- +"@smithy/credential-provider-imds": minor +"@smithy/shared-ini-file-loader": minor +"@smithy/node-config-provider": minor +"@smithy/property-provider": minor +--- + +new logging-compatible signature for CredentialsProviderError diff --git a/packages/credential-provider-imds/src/fromContainerMetadata.ts b/packages/credential-provider-imds/src/fromContainerMetadata.ts index 0336931c361..2cfdff55568 100644 --- a/packages/credential-provider-imds/src/fromContainerMetadata.ts +++ b/packages/credential-provider-imds/src/fromContainerMetadata.ts @@ -1,5 +1,5 @@ import { CredentialsProviderError } from "@smithy/property-provider"; -import { AwsCredentialIdentityProvider } from "@smithy/types"; +import { AwsCredentialIdentityProvider, Logger } from "@smithy/types"; import { RequestOptions } from "http"; import { parse } from "url"; @@ -31,10 +31,12 @@ export const fromContainerMetadata = (init: RemoteProviderInit = {}): AwsCredent const { timeout, maxRetries } = providerConfigFromInit(init); return () => retry(async () => { - const requestOptions = await getCmdsUri(); + const requestOptions = await getCmdsUri({ logger: init.logger }); const credsResponse = JSON.parse(await requestFromEcsImds(timeout, requestOptions)); if (!isImdsCredentials(credsResponse)) { - throw new CredentialsProviderError("Invalid response received from instance metadata service."); + throw new CredentialsProviderError("Invalid response received from instance metadata service.", { + logger: init.logger, + }); } return fromImdsCredentials(credsResponse); }, maxRetries); @@ -65,7 +67,7 @@ const GREENGRASS_PROTOCOLS = { "https:": true, }; -const getCmdsUri = async (): Promise => { +const getCmdsUri = async ({ logger }: { logger?: Logger }): Promise => { if (process.env[ENV_CMDS_RELATIVE_URI]) { return { hostname: CMDS_IP, @@ -76,17 +78,17 @@ const getCmdsUri = async (): Promise => { if (process.env[ENV_CMDS_FULL_URI]) { const parsed = parse(process.env[ENV_CMDS_FULL_URI]!); if (!parsed.hostname || !(parsed.hostname in GREENGRASS_HOSTS)) { - throw new CredentialsProviderError( - `${parsed.hostname} is not a valid container metadata service hostname`, - false - ); + throw new CredentialsProviderError(`${parsed.hostname} is not a valid container metadata service hostname`, { + tryNextLink: false, + logger, + }); } if (!parsed.protocol || !(parsed.protocol in GREENGRASS_PROTOCOLS)) { - throw new CredentialsProviderError( - `${parsed.protocol} is not a valid container metadata service protocol`, - false - ); + throw new CredentialsProviderError(`${parsed.protocol} is not a valid container metadata service protocol`, { + tryNextLink: false, + logger, + }); } return { @@ -99,6 +101,9 @@ const getCmdsUri = async (): Promise => { "The container metadata credential provider cannot be used unless" + ` the ${ENV_CMDS_RELATIVE_URI} or ${ENV_CMDS_FULL_URI} environment` + " variable is set", - false + { + tryNextLink: false, + logger, + } ); }; diff --git a/packages/credential-provider-imds/src/fromInstanceMetadata.ts b/packages/credential-provider-imds/src/fromInstanceMetadata.ts index 875d238b542..0d9caf8dee3 100644 --- a/packages/credential-provider-imds/src/fromInstanceMetadata.ts +++ b/packages/credential-provider-imds/src/fromInstanceMetadata.ts @@ -47,7 +47,8 @@ const getInstanceImdsProvider = (init: RemoteProviderInit) => { fallbackBlockedFromProcessEnv = !!envValue && envValue !== "false"; if (envValue === undefined) { throw new CredentialsProviderError( - `${AWS_EC2_METADATA_V1_DISABLED} not set in env, checking config file next.` + `${AWS_EC2_METADATA_V1_DISABLED} not set in env, checking config file next.`, + { logger: init.logger } ); } return fallbackBlockedFromProcessEnv; @@ -98,7 +99,7 @@ const getInstanceImdsProvider = (init: RemoteProviderInit) => { return retry(async () => { let creds: AwsCredentialIdentity; try { - creds = await getCredentialsFromProfile(imdsProfile, options); + creds = await getCredentialsFromProfile(imdsProfile, options, init); } catch (err) { if (err.statusCode === 401) { disableFetchToken = false; @@ -152,7 +153,7 @@ const getMetadataToken = async (options: RequestOptions) => const getProfile = async (options: RequestOptions) => (await httpRequest({ ...options, path: IMDS_PATH })).toString(); -const getCredentialsFromProfile = async (profile: string, options: RequestOptions) => { +const getCredentialsFromProfile = async (profile: string, options: RequestOptions, init: RemoteProviderInit) => { const credsResponse = JSON.parse( ( await httpRequest({ @@ -163,7 +164,9 @@ const getCredentialsFromProfile = async (profile: string, options: RequestOption ); if (!isImdsCredentials(credsResponse)) { - throw new CredentialsProviderError("Invalid response received from instance metadata service."); + throw new CredentialsProviderError("Invalid response received from instance metadata service.", { + logger: init.logger, + }); } return fromImdsCredentials(credsResponse); diff --git a/packages/node-config-provider/src/fromEnv.ts b/packages/node-config-provider/src/fromEnv.ts index 6e4b4e9faaf..33941b27fcc 100644 --- a/packages/node-config-provider/src/fromEnv.ts +++ b/packages/node-config-provider/src/fromEnv.ts @@ -1,5 +1,5 @@ import { CredentialsProviderError } from "@smithy/property-provider"; -import { Provider } from "@smithy/types"; +import { Logger, Provider } from "@smithy/types"; import { getSelectorName } from "./getSelectorName"; @@ -11,7 +11,7 @@ export type GetterFromEnv = (env: Record) => T | * environment variable. */ export const fromEnv = - (envVarSelector: GetterFromEnv): Provider => + (envVarSelector: GetterFromEnv, logger?: Logger): Provider => async () => { try { const config = envVarSelector(process.env); @@ -21,7 +21,8 @@ export const fromEnv = return config as T; } catch (e) { throw new CredentialsProviderError( - e.message || `Not found in ENV: ${getSelectorName(envVarSelector.toString())}` + e.message || `Not found in ENV: ${getSelectorName(envVarSelector.toString())}`, + { logger } ); } }; diff --git a/packages/node-config-provider/src/fromSharedConfigFiles.ts b/packages/node-config-provider/src/fromSharedConfigFiles.ts index cb28d71cffd..4947aaaa118 100644 --- a/packages/node-config-provider/src/fromSharedConfigFiles.ts +++ b/packages/node-config-provider/src/fromSharedConfigFiles.ts @@ -43,7 +43,8 @@ export const fromSharedConfigFiles = return configValue; } catch (e) { throw new CredentialsProviderError( - e.message || `Not found in config files w/ profile [${profile}]: ${getSelectorName(configSelector.toString())}` + e.message || `Not found in config files w/ profile [${profile}]: ${getSelectorName(configSelector.toString())}`, + { logger: init.logger } ); } }; diff --git a/packages/property-provider/src/CredentialsProviderError.spec.ts b/packages/property-provider/src/CredentialsProviderError.spec.ts index 7b6f9caa550..83a7fe0f473 100644 --- a/packages/property-provider/src/CredentialsProviderError.spec.ts +++ b/packages/property-provider/src/CredentialsProviderError.spec.ts @@ -2,22 +2,22 @@ import { CredentialsProviderError } from "./CredentialsProviderError"; import { ProviderError } from "./ProviderError"; describe(CredentialsProviderError.name, () => { - afterAll(() => { - CredentialsProviderError.logLimit = 100; - }); - it("should be named as CredentialsProviderError", () => { expect(new CredentialsProviderError("PANIC").name).toBe("CredentialsProviderError"); }); - it("should log up to its logLimit", () => { - CredentialsProviderError.logLimit = 5; - - Array.from({ length: CredentialsProviderError.logLimit + 2 }).forEach(() => { - new CredentialsProviderError("PANIC"); - }); + it("should use logger.trace if provided", () => { + const logger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + }; + new CredentialsProviderError("PANIC", { logger }); - expect(CredentialsProviderError.log.length).toEqual(CredentialsProviderError.logLimit); + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.trace).toHaveBeenCalled(); }); describe.each([Error, ProviderError, CredentialsProviderError])("should be instanceof %p", (classConstructor) => { diff --git a/packages/property-provider/src/CredentialsProviderError.ts b/packages/property-provider/src/CredentialsProviderError.ts index 50c1e8b8b14..5431cbf8256 100644 --- a/packages/property-provider/src/CredentialsProviderError.ts +++ b/packages/property-provider/src/CredentialsProviderError.ts @@ -1,5 +1,15 @@ +import { Logger } from "@smithy/types"; + import { ProviderError } from "./ProviderError"; +/** + * @public + */ +export type CredentialsProviderErrorOptionsType = { + tryNextLink?: boolean | undefined; + logger?: Logger; +}; + /** * @public * @@ -12,18 +22,35 @@ import { ProviderError } from "./ProviderError"; * ensures the chain will stop if an entirely unexpected error is encountered. */ export class CredentialsProviderError extends ProviderError { - public static log: string[] = []; - public static logLimit = 100; public name = "CredentialsProviderError"; - public constructor( - message: string, - public readonly tryNextLink: boolean = true - ) { + public readonly tryNextLink: boolean = true; + + /** + * @deprecated constructor should be given a logger. + */ + public constructor(message: string); + /** + * @deprecated constructor should be given a logger. + */ + public constructor(message: string, tryNextLink?: boolean | undefined); + /** + * This signature is preferred for logging capability. + */ + public constructor(message: string, options: CredentialsProviderErrorOptionsType); + public constructor(message: string, options: boolean | CredentialsProviderErrorOptionsType = true) { + let logger: Logger | undefined; + let tryNextLink: boolean = true; + + if (typeof options === "boolean") { + logger = undefined; + tryNextLink = options; + } else if (options != null && typeof options === "object") { + logger = options.logger; + tryNextLink = options.tryNextLink ?? true; + } super(message, tryNextLink); + Object.setPrototypeOf(this, CredentialsProviderError.prototype); - CredentialsProviderError.log.push(`${new Date().toISOString()} ${tryNextLink ? "->" : "(!)"} ${message}`); - while (CredentialsProviderError.log.length > CredentialsProviderError.logLimit) { - CredentialsProviderError.log.shift(); - } + logger?.trace?.(`${new Date().toISOString()} ${tryNextLink ? "->" : "(!)"} ${message}`); } } diff --git a/packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts b/packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts index 3b16fcd408f..9bf6336e877 100644 --- a/packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts +++ b/packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts @@ -1,4 +1,4 @@ -import { SharedConfigFiles } from "@smithy/types"; +import { Logger, SharedConfigFiles } from "@smithy/types"; import { getConfigData } from "./getConfigData"; import { getConfigFilepath } from "./getConfigFilepath"; @@ -26,6 +26,11 @@ export interface SharedConfigInit { * property is set, the provider will always reload any configuration files loaded before. */ ignoreCache?: boolean; + + /** + * For credential resolution trace logging. + */ + logger?: Logger; } const swallowError = () => ({});