Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: update contract for registerContextProvider #17954

Merged
merged 5 commits into from
Dec 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 33 additions & 19 deletions packages/aws-cdk/lib/context-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,18 +28,23 @@ 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 {
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,
Expand All @@ -59,21 +64,30 @@ export async function provideContextValues(
/**
* Register a context provider
*
* (Only available for testing right now).
* A context provider cannot reuse the SDKs authentication mechanisms.
*/
export function registerContextProvider(name: string, provider: ContextProviderPlugin) {
availableContextProviders[name] = () => provider;
}

/**
* Register a context provider factory
*
* A context provider factory takes an SdkProvider and returns the context provider plugin.
*/
export function registerContextProvider(name: string, provider: ProviderConstructor) {
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),
};
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/context-providers/provider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export interface ContextProviderPlugin {
getValue(args: {[key: string]: any}): Promise<any>;
}

export function isContextProviderPlugin(x: unknown): x is ContextProviderPlugin {
return typeof x === 'object' && !!x && !!(x as any).getValue;
}
28 changes: 28 additions & 0 deletions packages/aws-cdk/lib/plugin.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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<any>;
* ```
*
* 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.
*
* @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);
}
}
6 changes: 3 additions & 3 deletions packages/aws-cdk/test/api/cloud-executable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
registerContextProvider(cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER, {
async getValue(_: { [key: string]: any }): Promise<any> {
return 'foo';
}
},
});

const cloudExecutable = new MockCloudExecutable({
Expand Down
58 changes: 49 additions & 9 deletions packages/aws-cdk/test/context-providers/generic.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<any> {
contextproviders.registerContextProvider(TEST_PROVIDER, {
async getValue(_: {[key: string]: any}): Promise<any> {
throw new Error('Something went wrong');
}
},
});
const context = new Context();

Expand All @@ -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<any> {
contextproviders.registerContextProvider(TEST_PROVIDER, {
async getValue(args: {[key: string]: any}): Promise<any> {
if (args.lookupRoleArn == null) {
throw new Error('No lookupRoleArn');
}
Expand All @@ -40,7 +41,7 @@ test('lookup role ARN is resolved', async () => {
}

return 'some resolved value';
}
},
});
const context = new Context();

Expand All @@ -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<any> {
contextproviders.registerContextProvider(TEST_PROVIDER, {
async getValue(_: {[key: string]: any}): Promise<any> {
throw new Error('Something went wrong');
}
},
});
const context = new Context();

Expand All @@ -78,3 +79,42 @@ 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<any> {
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);
});

test('context provider can be called without account/region', async () => {
// GIVEN
PluginHost.instance.registerContextProviderAlpha(TEST_PROVIDER, {
async getValue(_: {[key: string]: any}): Promise<any> {
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');
});