Skip to content

Commit

Permalink
chore: update contract for registerContextProvider (#17954)
Browse files Browse the repository at this point in the history
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.

Issues already uncovered by doing this that the more general open framework will have to deal with:

* `SdkProvider` would need to be open and stable
* What if the provider doesn't need account/region?
* Schema validation in query and response
* Side channel instructions to the context framework
* (not to mention: how will the code get on the user's machine?)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
rix0rrr authored Dec 10, 2021
1 parent 95b8da9 commit 203c42b
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 31 deletions.
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');
});

0 comments on commit 203c42b

Please sign in to comment.