From 016a9f3e2b7ac250383a12ac3b64d2147375b037 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 Dec 2021 17:41:04 +0100 Subject: [PATCH 1/4] chore: update contract for registerContractProvider We're trialling open context providers internally. Not ready yet to call this a public API but we will maintain firmer guarantees on this function going forward. --- packages/aws-cdk/lib/context-providers/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts index 4de652715dcf2..dfce0d8a82d18 100644 --- a/packages/aws-cdk/lib/context-providers/index.ts +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -59,7 +59,8 @@ export async function provideContextValues( /** * Register a context provider * - * (Only available for testing right now). + * (Only available for testing and internal usage at Amazon. Don't change the signature + * of this function). */ export function registerContextProvider(name: string, provider: ProviderConstructor) { availableContextProviders[name] = provider; From 6c632ad67c8c7c13cc4d07382e004380ff5bc6fb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 Dec 2021 18:12:08 +0100 Subject: [PATCH 2/4] Expose on PluginHost as well --- .../aws-cdk/lib/context-providers/index.ts | 44 +++++++++++-------- .../aws-cdk/lib/context-providers/provider.ts | 4 ++ packages/aws-cdk/lib/plugin.ts | 28 ++++++++++++ .../aws-cdk/test/api/cloud-executable.test.ts | 6 +-- .../test/context-providers/generic.test.ts | 40 +++++++++++++---- 5 files changed, 92 insertions(+), 30 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts index dfce0d8a82d18..68cfe45217420 100644 --- a/packages/aws-cdk/lib/context-providers/index.ts +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -15,8 +15,8 @@ import { SecurityGroupContextProviderPlugin } from './security-groups'; import { SSMContextProviderPlugin } from './ssm-parameters'; import { VpcNetworkContextProviderPlugin } from './vpcs'; -type ProviderConstructor = (new (sdk: SdkProvider, lookupRoleArn?: string) => ContextProviderPlugin); -export type ProviderMap = {[name: string]: ProviderConstructor}; +export type ContextProviderFactory = ((sdk: SdkProvider) => ContextProviderPlugin); +export type ProviderMap = {[name: string]: ContextProviderFactory}; /** * Iterate over the list of missing context values and invoke the appropriate providers from the map to retrieve them @@ -28,13 +28,13 @@ export async function provideContextValues( for (const missingContext of missingValues) { const key = missingContext.key; - const constructor = availableContextProviders[missingContext.provider]; - if (!constructor) { + const factory = availableContextProviders[missingContext.provider]; + if (!factory) { // eslint-disable-next-line max-len throw new Error(`Unrecognized context provider name: ${missingContext.provider}. You might need to update the toolkit to match the version of the construct library.`); } - const provider = new constructor(sdk); + const provider = factory(sdk); let value; try { @@ -59,22 +59,30 @@ export async function provideContextValues( /** * Register a context provider * - * (Only available for testing and internal usage at Amazon. Don't change the signature - * of this function). + * A context provider cannot reuse the SDKs authentication mechanisms. */ -export function registerContextProvider(name: string, provider: ProviderConstructor) { +export function registerContextProvider(name: string, provider: ContextProviderPlugin) { + availableContextProviders[name] = () => provider; +} + +/** + * Register a context provider factory + * + * A context provider factory takes an SdkProvider and the context provider plugin. + */ +export function registerContextProviderFactory(name: string, provider: ContextProviderFactory) { availableContextProviders[name] = provider; } const availableContextProviders: ProviderMap = { - [cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER]: AZContextProviderPlugin, - [cxschema.ContextProvider.SSM_PARAMETER_PROVIDER]: SSMContextProviderPlugin, - [cxschema.ContextProvider.HOSTED_ZONE_PROVIDER]: HostedZoneContextProviderPlugin, - [cxschema.ContextProvider.VPC_PROVIDER]: VpcNetworkContextProviderPlugin, - [cxschema.ContextProvider.AMI_PROVIDER]: AmiContextProviderPlugin, - [cxschema.ContextProvider.ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER]: EndpointServiceAZContextProviderPlugin, - [cxschema.ContextProvider.SECURITY_GROUP_PROVIDER]: SecurityGroupContextProviderPlugin, - [cxschema.ContextProvider.LOAD_BALANCER_PROVIDER]: LoadBalancerContextProviderPlugin, - [cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER]: LoadBalancerListenerContextProviderPlugin, - [cxschema.ContextProvider.KEY_PROVIDER]: KeyContextProviderPlugin, + [cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER]: (s) => new AZContextProviderPlugin(s), + [cxschema.ContextProvider.SSM_PARAMETER_PROVIDER]: (s) => new SSMContextProviderPlugin(s), + [cxschema.ContextProvider.HOSTED_ZONE_PROVIDER]: (s) => new HostedZoneContextProviderPlugin(s), + [cxschema.ContextProvider.VPC_PROVIDER]: (s) => new VpcNetworkContextProviderPlugin(s), + [cxschema.ContextProvider.AMI_PROVIDER]: (s) => new AmiContextProviderPlugin(s), + [cxschema.ContextProvider.ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER]: (s) => new EndpointServiceAZContextProviderPlugin(s), + [cxschema.ContextProvider.SECURITY_GROUP_PROVIDER]: (s) => new SecurityGroupContextProviderPlugin(s), + [cxschema.ContextProvider.LOAD_BALANCER_PROVIDER]: (s) => new LoadBalancerContextProviderPlugin(s), + [cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER]: (s) => new LoadBalancerListenerContextProviderPlugin(s), + [cxschema.ContextProvider.KEY_PROVIDER]: (s) => new KeyContextProviderPlugin(s), }; diff --git a/packages/aws-cdk/lib/context-providers/provider.ts b/packages/aws-cdk/lib/context-providers/provider.ts index 3d8a59938e9ef..29c3ebadbde64 100644 --- a/packages/aws-cdk/lib/context-providers/provider.ts +++ b/packages/aws-cdk/lib/context-providers/provider.ts @@ -1,3 +1,7 @@ export interface ContextProviderPlugin { getValue(args: {[key: string]: any}): Promise; } + +export function isContextProviderPlugin(x: unknown): x is ContextProviderPlugin { + return typeof x === 'object' && !!x && !!(x as any).getValue; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/plugin.ts b/packages/aws-cdk/lib/plugin.ts index 705fa3c3d859d..f59e2266ca41d 100644 --- a/packages/aws-cdk/lib/plugin.ts +++ b/packages/aws-cdk/lib/plugin.ts @@ -1,6 +1,9 @@ +import { inspect } from 'util'; import { green } from 'colors/safe'; import { CredentialProviderSource } from './api/aws-auth/credentials'; +import { registerContextProvider } from './context-providers'; +import { ContextProviderPlugin, isContextProviderPlugin } from './context-providers/provider'; import { error } from './logging'; /** @@ -86,4 +89,29 @@ export class PluginHost { public registerCredentialProviderSource(source: CredentialProviderSource) { this.credentialProviderSources.push(source); } + + /** + * (EXPERIMENTAL) Allow plugins to register context providers + * + * Context providers are objects with the following method: + * + * ```ts + * getValue(args: {[key: string]: any}): Promise; + * ``` + * + * Currently, they cannot reuse the CDK's authentication mechanism, so they + * must be prepared to either not make AWS calls or use their own authentication + * mechanism. + * + * This feature is experimental, and only intended to be used internally at Amazon + * as a trial. + * + * @experimental + */ + public registerContextProviderAlpha(providerName: string, provider: ContextProviderPlugin) { + if (!isContextProviderPlugin(provider)) { + throw new Error(`Object you gave me does not look like a ContextProviderPlugin: ${inspect(provider)}`); + } + registerContextProvider(providerName, provider); + } } diff --git a/packages/aws-cdk/test/api/cloud-executable.test.ts b/packages/aws-cdk/test/api/cloud-executable.test.ts index a4895761ba46e..37b2db03d40e5 100644 --- a/packages/aws-cdk/test/api/cloud-executable.test.ts +++ b/packages/aws-cdk/test/api/cloud-executable.test.ts @@ -49,10 +49,10 @@ describe('AWS::CDK::Metadata', () => { }); test('stop executing if context providers are not making progress', async () => { - registerContextProvider(cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER, class { - public async getValue(_: { [key: string]: any }): Promise { + registerContextProvider(cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER, { + async getValue(_: { [key: string]: any }): Promise { return 'foo'; - } + }, }); const cloudExecutable = new MockCloudExecutable({ diff --git a/packages/aws-cdk/test/context-providers/generic.test.ts b/packages/aws-cdk/test/context-providers/generic.test.ts index 66d837b2dab98..44cf1610ec358 100644 --- a/packages/aws-cdk/test/context-providers/generic.test.ts +++ b/packages/aws-cdk/test/context-providers/generic.test.ts @@ -1,3 +1,4 @@ +import { PluginHost } from '../../lib'; import * as contextproviders from '../../lib/context-providers'; import { Context, TRANSIENT_CONTEXT_KEY } from '../../lib/settings'; import { MockSdkProvider } from '../util/mock-sdk'; @@ -8,10 +9,10 @@ const TEST_PROVIDER: any = 'testprovider'; test('errors are reported into the context value', async () => { // GIVEN - contextproviders.registerContextProvider(TEST_PROVIDER, class { - public async getValue(_: {[key: string]: any}): Promise { + contextproviders.registerContextProvider(TEST_PROVIDER, { + async getValue(_: {[key: string]: any}): Promise { throw new Error('Something went wrong'); - } + }, }); const context = new Context(); @@ -29,8 +30,8 @@ test('errors are reported into the context value', async () => { test('lookup role ARN is resolved', async () => { // GIVEN - contextproviders.registerContextProvider(TEST_PROVIDER, class { - public async getValue(args: {[key: string]: any}): Promise { + contextproviders.registerContextProvider(TEST_PROVIDER, { + async getValue(args: {[key: string]: any}): Promise { if (args.lookupRoleArn == null) { throw new Error('No lookupRoleArn'); } @@ -40,7 +41,7 @@ test('lookup role ARN is resolved', async () => { } return 'some resolved value'; - } + }, }); const context = new Context(); @@ -63,10 +64,10 @@ test('lookup role ARN is resolved', async () => { test('errors are marked transient', async () => { // GIVEN - contextproviders.registerContextProvider(TEST_PROVIDER, class { - public async getValue(_: {[key: string]: any}): Promise { + contextproviders.registerContextProvider(TEST_PROVIDER, { + async getValue(_: {[key: string]: any}): Promise { throw new Error('Something went wrong'); - } + }, }); const context = new Context(); @@ -78,3 +79,24 @@ test('errors are marked transient', async () => { // THEN - error is marked transient expect(context.get('asdf')[TRANSIENT_CONTEXT_KEY]).toBeTruthy(); }); + +test('context provider can be registered using PluginHost', async () => { + let called = false; + + // GIVEN + PluginHost.instance.registerContextProviderAlpha(TEST_PROVIDER, { + async getValue(_: {[key: string]: any}): Promise { + called = true; + return ''; + }, + }); + const context = new Context(); + + // WHEN + await contextproviders.provideContextValues([ + { key: 'asdf', props: { account: '1234', region: 'us-east-1' }, provider: TEST_PROVIDER }, + ], context, mockSDK); + + // THEN - error is marked transient + expect(called).toEqual(true); +}); From 75f1b6b60ab545bc4671a1aee597c7376dfa23bb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 Dec 2021 18:14:42 +0100 Subject: [PATCH 3/4] Fix typo --- packages/aws-cdk/lib/context-providers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts index 68cfe45217420..164e2acde7591 100644 --- a/packages/aws-cdk/lib/context-providers/index.ts +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -68,7 +68,7 @@ export function registerContextProvider(name: string, provider: ContextProviderP /** * Register a context provider factory * - * A context provider factory takes an SdkProvider and the context provider plugin. + * A context provider factory takes an SdkProvider and returns the context provider plugin. */ export function registerContextProviderFactory(name: string, provider: ContextProviderFactory) { availableContextProviders[name] = provider; From 23a3c6637718caa6c96aa96f3f1de76159872278 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 10 Dec 2021 18:24:17 +0100 Subject: [PATCH 4/4] Make it not crash if account/region are not present --- .../aws-cdk/lib/context-providers/index.ts | 9 +++++++-- packages/aws-cdk/lib/plugin.ts | 6 +++--- .../test/context-providers/generic.test.ts | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts index 164e2acde7591..fe643ca946c83 100644 --- a/packages/aws-cdk/lib/context-providers/index.ts +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -38,8 +38,13 @@ export async function provideContextValues( let value; try { - const environment = cxapi.EnvironmentUtils.make(missingContext.props.account, missingContext.props.region); - const resolvedEnvironment = await sdk.resolveEnvironment(environment); + const environment = missingContext.props.account && missingContext.props.region + ? cxapi.EnvironmentUtils.make(missingContext.props.account, missingContext.props.region) + : undefined; + + const resolvedEnvironment: cxapi.Environment = environment + ? await sdk.resolveEnvironment(environment) + : { account: '?', region: '?', name: '?' }; const arns = await replaceEnvPlaceholders({ lookupRoleArn: missingContext.props.lookupRoleArn, diff --git a/packages/aws-cdk/lib/plugin.ts b/packages/aws-cdk/lib/plugin.ts index f59e2266ca41d..9534af27dc1da 100644 --- a/packages/aws-cdk/lib/plugin.ts +++ b/packages/aws-cdk/lib/plugin.ts @@ -99,9 +99,9 @@ export class PluginHost { * getValue(args: {[key: string]: any}): Promise; * ``` * - * Currently, they cannot reuse the CDK's authentication mechanism, so they - * must be prepared to either not make AWS calls or use their own authentication - * mechanism. + * Currently, they cannot reuse the CDK's authentication mechanisms, so they + * must be prepared to either not make AWS calls or use their own source of + * AWS credentials. * * This feature is experimental, and only intended to be used internally at Amazon * as a trial. diff --git a/packages/aws-cdk/test/context-providers/generic.test.ts b/packages/aws-cdk/test/context-providers/generic.test.ts index 44cf1610ec358..ad26b22689171 100644 --- a/packages/aws-cdk/test/context-providers/generic.test.ts +++ b/packages/aws-cdk/test/context-providers/generic.test.ts @@ -100,3 +100,21 @@ test('context provider can be registered using PluginHost', async () => { // THEN - error is marked transient expect(called).toEqual(true); }); + +test('context provider can be called without account/region', async () => { + // GIVEN + PluginHost.instance.registerContextProviderAlpha(TEST_PROVIDER, { + async getValue(_: {[key: string]: any}): Promise { + return 'yay'; + }, + }); + const context = new Context(); + + // WHEN + await contextproviders.provideContextValues([ + { key: 'asdf', props: { banana: 'yellow' } as any, provider: TEST_PROVIDER }, + ], context, mockSDK); + + // THEN - error is marked transient + expect(context.get('asdf')).toEqual('yay'); +});