From 653b78d490161d4ae08ec709affe08050e34c8d6 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 17 Oct 2018 01:20:32 +0300 Subject: [PATCH] feat(aws-cloudformation): allow specifying custom resource type Adds the `resourceType` property to `CustomResource` which allows specifying the type name for the resource. Custom resource type names must start with "Custom::" and have some naming restrictions, which are validated. --- .../aws-cloudformation/lib/custom-resource.ts | 42 ++++++++++++++ .../aws-cloudformation/test/test.resource.ts | 57 ++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts index 32854366bc3ab..9c4fae592a51b 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts @@ -32,6 +32,27 @@ export interface CustomResourceProps { * Properties to pass to the Lambda */ properties?: Properties; + + /** + * For custom resources, you can specify AWS::CloudFormation::CustomResource + * (the default) as the resource type, or you can specify your own resource + * type name. For example, you can use "Custom::MyCustomResourceTypeName". + * + * Custom resource type names must begin with "Custom::" and can include + * alphanumeric characters and the following characters: _@-. You can specify + * a custom resource type name up to a maximum length of 60 characters. You + * cannot change the type during an update. + * + * Using your own resource type names helps you quickly differentiate the + * types of custom resources in your stack. For example, if you had two custom + * resources that conduct two different ping tests, you could name their type + * as Custom::PingTester to make them easily identifiable as ping testers + * (instead of using AWS::CloudFormation::CustomResource). + * + * @see + * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html#aws-cfn-resource-type-name + */ + resourceType?: string; } /** @@ -57,6 +78,10 @@ export class CustomResource extends cloudformation.CustomResource { }); this.userProperties = props.properties; + + if (props.resourceType) { + this.useCustomResourceType(props.resourceType); + } } /** @@ -67,6 +92,23 @@ export class CustomResource extends cloudformation.CustomResource { return Object.assign(props, uppercaseProperties(this.userProperties || {})); } + private useCustomResourceType(resourceType: string) { + if (!resourceType.startsWith('Custom::')) { + throw new Error(`Custom resource type must begin with "Custom::" (${resourceType})`); + } + + const typeName = resourceType.substr(resourceType.indexOf('::') + 2); + if (typeName.length > 60) { + throw new Error(`Custom resource type length > 60 (${resourceType})`); + } + + if (!/^[a-z0-9_@-]+$/i.test(typeName)) { + throw new Error(`Custom resource type name can only include alphanumeric characters and _@- (${typeName})`); + } + + this.addOverride('Type', resourceType); + } + } /** diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.resource.ts b/packages/@aws-cdk/aws-cloudformation/test/test.resource.ts index 4ac56d02e0422..e460dce095f8e 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.resource.ts @@ -1,5 +1,6 @@ -import { expect } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert'; import lambda = require('@aws-cdk/aws-lambda'); +import sns = require('@aws-cdk/aws-sns'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import { CustomResource } from '../lib'; @@ -85,7 +86,59 @@ export = { } }); test.done(); - } + }, + + 'custom resources can specify a resource type that starts with Custom::'(test: Test) { + const stack = new cdk.Stack(); + new CustomResource(stack, 'MyCustomResource', { + resourceType: 'Custom::MyCustomResourceType', + topicProvider: new sns.Topic(stack, 'Provider') + }); + expect(stack).to(haveResource('Custom::MyCustomResourceType')); + test.done(); + }, + + 'fails if custom resource type is invalid': { + 'does not start with "Custom::"'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new CustomResource(stack, 'MyCustomResource', { + resourceType: 'NoCustom::MyCustomResourceType', + topicProvider: new sns.Topic(stack, 'Provider') + }); + }, /Custom resource type must begin with "Custom::"/); + + test.done(); + }, + + 'has invalid characters'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new CustomResource(stack, 'MyCustomResource', { + resourceType: 'Custom::My Custom?ResourceType', + topicProvider: new sns.Topic(stack, 'Provider') + }); + }, /Custom resource type name can only include alphanumeric characters and/); + + test.done(); + }, + + 'is longer than 60 characters'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new CustomResource(stack, 'MyCustomResource', { + resourceType: 'Custom::0123456789012345678901234567890123456789012345678901234567891', + topicProvider: new sns.Topic(stack, 'Provider') + }); + }, /Custom resource type length > 60/); + + test.done(); + }, + + }, }; class TestCustomResource extends cdk.Construct {