diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index 436fe376f9624..12e57cbc200ee 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -201,21 +201,24 @@ portfolio.addProduct(product); ## Tag Options TagOptions allow administrators to easily manage tags on provisioned products by creating a selection of tags for end users to choose from. -For example, an end user can choose an `ec2` for the instance type size. -TagOptions are created by specifying a key with a selection of values and can be associated with both portfolios and products. +TagOptions are created by specifying a tag key with a selection of allowed values and can be associated with both portfolios and products. When launching a product, both the TagOptions associated with the product and the containing portfolio are made available. At the moment, TagOptions can only be disabled in the console. ```ts fixture=portfolio-product -const tagOptionsForPortfolio = new servicecatalog.TagOptions({ - costCenter: ['Data Insights', 'Marketing'], +const tagOptionsForPortfolio = new servicecatalog.TagOptions(this, 'OrgTagOptions', { + allowedValuesForTags: { + Group: ['finance', 'engineering', 'marketing', 'research'], + CostCenter: ['01', '02','03'], + }, }); portfolio.associateTagOptions(tagOptionsForPortfolio); -const tagOptionsForProduct = new servicecatalog.TagOptions({ - ec2InstanceType: ['A1', 'M4'], - ec2InstanceSize: ['medium', 'large'], +const tagOptionsForProduct = new servicecatalog.TagOptions(this, 'ProductTagOptions', { + allowedValuesForTags: { + Environment: ['dev', 'alpha', 'prod'], + }, }); product.associateTagOptions(tagOptionsForProduct); ``` diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts index e1e4ee8de38da..dd44ef0f022fc 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts @@ -9,7 +9,7 @@ import { IPortfolio } from '../portfolio'; import { IProduct } from '../product'; import { CfnLaunchNotificationConstraint, CfnLaunchRoleConstraint, CfnLaunchTemplateConstraint, CfnPortfolioProductAssociation, - CfnResourceUpdateConstraint, CfnStackSetConstraint, CfnTagOption, CfnTagOptionAssociation, + CfnResourceUpdateConstraint, CfnStackSetConstraint, CfnTagOptionAssociation, } from '../servicecatalog.generated'; import { TagOptions } from '../tag-options'; import { hashValues } from './util'; @@ -139,33 +139,16 @@ export class AssociationManager { } } - public static associateTagOptions(resource: cdk.IResource, resourceId: string, tagOptions: TagOptions): void { - const resourceStack = cdk.Stack.of(resource); - for (const [key, tagOptionsList] of Object.entries(tagOptions.tagOptionsMap)) { - InputValidator.validateLength(resource.node.addr, 'TagOption key', 1, 128, key); - tagOptionsList.forEach((value: string) => { - InputValidator.validateLength(resource.node.addr, 'TagOption value', 1, 256, value); - const tagOptionKey = hashValues(key, value, resourceStack.node.addr); - const tagOptionConstructId = `TagOption${tagOptionKey}`; - let cfnTagOption = resourceStack.node.tryFindChild(tagOptionConstructId) as CfnTagOption; - if (!cfnTagOption) { - cfnTagOption = new CfnTagOption(resourceStack, tagOptionConstructId, { - key: key, - value: value, - active: true, - }); - } - const tagAssocationKey = hashValues(key, value, resource.node.addr); - const tagAssocationConstructId = `TagOptionAssociation${tagAssocationKey}`; - if (!resource.node.tryFindChild(tagAssocationConstructId)) { - new CfnTagOptionAssociation(resource as cdk.Resource, tagAssocationConstructId, { - resourceId: resourceId, - tagOptionId: cfnTagOption.ref, - }); - } - }); - }; + for (const cfnTagOption of tagOptions._cfnTagOptions) { + const tagAssocationConstructId = `TagOptionAssociation${hashValues(cfnTagOption.key, cfnTagOption.value, resource.node.addr)}`; + if (!resource.node.tryFindChild(tagAssocationConstructId)) { + new CfnTagOptionAssociation(resource as cdk.Resource, tagAssocationConstructId, { + resourceId: resourceId, + tagOptionId: cfnTagOption.ref, + }); + } + } } private static setLaunchRoleConstraint( diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts index 29a47fc6932a9..3c4e8bd9fb59f 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts @@ -1,11 +1,11 @@ import { ArnFormat, IResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { TagOptions } from '.'; import { CloudFormationTemplate } from './cloudformation-template'; import { MessageLanguage } from './common'; import { AssociationManager } from './private/association-manager'; import { InputValidator } from './private/validation'; import { CfnCloudFormationProduct } from './servicecatalog.generated'; +import { TagOptions } from './tag-options'; /** * A Service Catalog product, currently only supports type CloudFormationProduct @@ -137,7 +137,7 @@ export interface CloudFormationProductProps { * * @default - No tagOptions provided */ - readonly tagOptions?: TagOptions + readonly tagOptions?: TagOptions; } /** diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts b/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts index 808ea78add4a3..b0342c6336bd3 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts @@ -1,14 +1,70 @@ +import * as cdk from '@aws-cdk/core'; +import { hashValues } from './private/util'; +import { InputValidator } from './private/validation'; +import { CfnTagOption } from './servicecatalog.generated'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from 'constructs'; + +/** + * Properties for TagOptions. + */ +export interface TagOptionsProps { + /** + * The values that are allowed to be set for specific tags. + * The keys of the map represent the tag keys, + * and the values of the map are a list of allowed values for that particular tag key. + */ + readonly allowedValuesForTags: { [tagKey: string]: string[] }; +} + /** - * Defines a Tag Option, which are similar to tags - * but have multiple values per key. + * Defines a set of TagOptions, which are a list of key-value pairs managed in AWS Service Catalog. + * It is not an AWS tag, but serves as a template for creating an AWS tag based on the TagOption. + * See https://docs.aws.amazon.com/servicecatalog/latest/adminguide/tagoptions.html + * + * @resource AWS::ServiceCatalog::TagOption */ -export class TagOptions { +export class TagOptions extends cdk.Resource { /** - * List of CfnTagOption - */ - public readonly tagOptionsMap: { [key: string]: string[] }; + * List of underlying CfnTagOption resources. + * + * @internal + */ + public _cfnTagOptions: CfnTagOption[]; - constructor(tagOptionsMap: { [key: string]: string[]} ) { - this.tagOptionsMap = { ...tagOptionsMap }; + constructor(scope: Construct, id: string, props: TagOptionsProps) { + super(scope, id); + + this._cfnTagOptions = this.createUnderlyingTagOptions(props.allowedValuesForTags); + } + + private createUnderlyingTagOptions(allowedValuesForTags: { [tagKey: string]: string[] }): CfnTagOption[] { + if (Object.keys(allowedValuesForTags).length === 0) { + throw new Error(`No tag option keys or values were provided for resource ${this.node.path}`); + } + var tagOptions: CfnTagOption[] = []; + + for (const [tagKey, tagValues] of Object.entries(allowedValuesForTags)) { + InputValidator.validateLength(this.node.addr, 'TagOption key', 1, 128, tagKey); + + const uniqueTagValues = new Set(tagValues); + if (uniqueTagValues.size === 0) { + throw new Error(`No tag option values were provided for tag option key ${tagKey} for resource ${this.node.path}`); + } + uniqueTagValues.forEach((tagValue: string) => { + InputValidator.validateLength(this.node.addr, 'TagOption value', 1, 256, tagValue); + const tagOptionIdentifier = hashValues(tagKey, tagValue); + const tagOption = new CfnTagOption(this, tagOptionIdentifier, { + key: tagKey, + value: tagValue, + active: true, + }); + tagOptions.push(tagOption); + }); + } + return tagOptions; } } + diff --git a/packages/@aws-cdk/aws-servicecatalog/package.json b/packages/@aws-cdk/aws-servicecatalog/package.json index 13567c9c5c88d..e11b48a0c3392 100644 --- a/packages/@aws-cdk/aws-servicecatalog/package.json +++ b/packages/@aws-cdk/aws-servicecatalog/package.json @@ -105,7 +105,13 @@ "props-physical-name:@aws-cdk/aws-servicecatalog.CloudFormationProductProps", "resource-attribute:@aws-cdk/aws-servicecatalog.Portfolio.portfolioName", "props-physical-name:@aws-cdk/aws-servicecatalog.PortfolioProps", - "props-physical-name:@aws-cdk/aws-servicecatalog.ProductStack" + "props-physical-name:@aws-cdk/aws-servicecatalog.ProductStack", + "props-struct-name:@aws-cdk/aws-servicecatalog.ITagOptions", + "props-physical-name:@aws-cdk/aws-servicecatalog.TagOptionsProps", + "ref-via-interface:@aws-cdk/aws-servicecatalog.CloudFormationProductProps.tagOptions", + "ref-via-interface:@aws-cdk/aws-servicecatalog.IProduct.associateTagOptions.tagOptions", + "ref-via-interface:@aws-cdk/aws-servicecatalog.IPortfolio.associateTagOptions.tagOptions", + "ref-via-interface:@aws-cdk/aws-servicecatalog.PortfolioProps.tagOptions" ] }, "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json index c298f292d039d..c25d867209591 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json @@ -81,7 +81,7 @@ "Ref": "TestPortfolio4AC794EB" }, "TagOptionId": { - "Ref": "TagOptionc0d88a3c4b8b" + "Ref": "TagOptions5f31c54ba705F110F743" } } }, @@ -92,7 +92,7 @@ "Ref": "TestPortfolio4AC794EB" }, "TagOptionId": { - "Ref": "TagOption9b16df08f83d" + "Ref": "TagOptions8d263919cebb6764AC10" } } }, @@ -103,7 +103,7 @@ "Ref": "TestPortfolio4AC794EB" }, "TagOptionId": { - "Ref": "TagOptiondf34c1c83580" + "Ref": "TagOptionsa260cbbd99c416C40F73" } } }, @@ -217,7 +217,7 @@ "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" ] }, - "TagOptionc0d88a3c4b8b": { + "TagOptions5f31c54ba705F110F743": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key1", @@ -225,7 +225,7 @@ "Active": true } }, - "TagOption9b16df08f83d": { + "TagOptions8d263919cebb6764AC10": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key1", @@ -233,7 +233,7 @@ "Active": true } }, - "TagOptiondf34c1c83580": { + "TagOptionsa260cbbd99c416C40F73": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key2", @@ -263,7 +263,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptionc0d88a3c4b8b" + "Ref": "TagOptions5f31c54ba705F110F743" } } }, @@ -274,7 +274,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOption9b16df08f83d" + "Ref": "TagOptions8d263919cebb6764AC10" } } }, @@ -285,7 +285,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptiondf34c1c83580" + "Ref": "TagOptionsa260cbbd99c416C40F73" } } }, diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts index 669016f35be2a..c5941dec42062 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts @@ -22,9 +22,11 @@ const portfolio = new servicecatalog.Portfolio(stack, 'TestPortfolio', { portfolio.giveAccessToRole(role); portfolio.giveAccessToGroup(group); -const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], +const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); portfolio.associateTagOptions(tagOptions); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json index fb51ec2ad0df4..05ea621ec10fd 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json @@ -226,7 +226,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptionab501c9aef99" + "Ref": "TagOptions5f31c54ba705F110F743" } } }, @@ -237,7 +237,7 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptiona453ac93ee6f" + "Ref": "TagOptions8d263919cebb6764AC10" } } }, @@ -248,11 +248,11 @@ "Ref": "TestProduct7606930B" }, "TagOptionId": { - "Ref": "TagOptiona006431604cb" + "Ref": "TagOptionsa260cbbd99c416C40F73" } } }, - "TagOptionab501c9aef99": { + "TagOptions5f31c54ba705F110F743": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key1", @@ -260,7 +260,7 @@ "Active": true } }, - "TagOptiona453ac93ee6f": { + "TagOptions8d263919cebb6764AC10": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key1", @@ -268,7 +268,7 @@ "Active": true } }, - "TagOptiona006431604cb": { + "TagOptionsa260cbbd99c416C40F73": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { "Key": "key2", diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts index e1e08105ee3ce..22429b3ddbf83 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts @@ -38,9 +38,11 @@ const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { ], }); -const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], +const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); product.associateTagOptions(tagOptions); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts index 43a283c157f80..74406931a069f 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts @@ -304,9 +304,11 @@ describe('portfolio associations and product constraints', () => { }), test('add tag options to portfolio', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); portfolio.associateTagOptions(tagOptions); @@ -316,9 +318,11 @@ describe('portfolio associations and product constraints', () => { }), test('add tag options to portfolio as prop', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolioWithTag', { @@ -331,49 +335,55 @@ describe('portfolio associations and product constraints', () => { Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 3); }), - test('adding identical tag options to portfolio is idempotent', () => { - const tagOptions1 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], - }); - - const tagOptions2 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], + test('adding tag options to portfolio multiple times is idempotent', () => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); - portfolio.associateTagOptions(tagOptions1); - portfolio.associateTagOptions(tagOptions2); // If not idempotent this would fail + portfolio.associateTagOptions(tagOptions); + portfolio.associateTagOptions(tagOptions); // If not idempotent this would fail Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 3); }), - test('fails to add tag options with invalid minimum key length', () => { - const tagOptions = new servicecatalog.TagOptions({ - '': ['value1', 'value2'], - 'key2': ['value1'], - }); + test('fails to create and then add tag options with invalid minimum key length', () => { expect(() => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + '': ['value1', 'value2'], + 'key2': ['value1'], + }, + }); + portfolio.associateTagOptions(tagOptions); }).toThrowError(/Invalid TagOption key for resource/); }); - test('fails to add tag options with invalid maxium key length', () => { - const tagOptions = new servicecatalog.TagOptions({ - ['key1'.repeat(1000)]: ['value1', 'value2'], - key2: ['value1'], - }); + test('fails to create and then add tag options with invalid maxium key length', () => { expect(() => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + ['key1'.repeat(1000)]: ['value1', 'value2'], + key2: ['value1'], + }, + }); + portfolio.associateTagOptions(tagOptions); }).toThrowError(/Invalid TagOption key for resource/); }), - test('fails to add tag options with invalid value length', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1'.repeat(1000), 'value2'], - key2: ['value1'], - }); + test('fails to create and then add tag options with invalid value length', () => { expect(() => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1'.repeat(1000), 'value2'], + key2: ['value1'], + }, + }); portfolio.associateTagOptions(tagOptions); }).toThrowError(/Invalid TagOption value for resource/); }), diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts index 0afc91ce86153..691ae9d14d9f1 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts @@ -271,7 +271,7 @@ describe('Product', () => { productVersions: [], }); }).toThrowError(/Invalid product versions for resource Default\/MyProduct/); - }), + }); describe('adding and associating TagOptions to a product', () => { let product: servicecatalog.IProduct; @@ -286,12 +286,14 @@ describe('Product', () => { }, ], }); - }), + }); test('add tag options to product', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); product.associateTagOptions(tagOptions); @@ -301,9 +303,11 @@ describe('Product', () => { }), test('add tag options as input to product in props', () => { - const tagOptions = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); new servicecatalog.CloudFormationProduct(stack, 'MyProductWithTagOptions', { @@ -321,32 +325,27 @@ describe('Product', () => { Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 3); }), - test('adding identical tag options to product is idempotent', () => { - const tagOptions1 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], - }); - - const tagOptions2 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], + test('adding tag options to product multiple times is idempotent', () => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); - product.associateTagOptions(tagOptions1); - product.associateTagOptions(tagOptions2); // If not idempotent this would fail + product.associateTagOptions(tagOptions); + product.associateTagOptions(tagOptions); // If not idempotent this would fail Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 3); }), - test('adding duplicate tag options to portfolio and product creates unique tag options and enumerated associations', () => { - const tagOptions1 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value1'], - }); - - const tagOptions2 = new servicecatalog.TagOptions({ - key1: ['value1', 'value2'], - key2: ['value2'], + test('adding tag options to portfolio and product creates unique tag options and enumerated associations', () => { + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1'], + }, }); const portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { @@ -354,10 +353,10 @@ describe('Product', () => { providerName: 'testProvider', }); - portfolio.associateTagOptions(tagOptions1); - product.associateTagOptions(tagOptions2); // If not idempotent this would fail + portfolio.associateTagOptions(tagOptions); + product.associateTagOptions(tagOptions); - Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 4); //Generates a resource for each unique key-value pair + Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 6); }); }); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/tag-option.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/tag-option.test.ts new file mode 100644 index 0000000000000..67f601ed6e521 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/test/tag-option.test.ts @@ -0,0 +1,163 @@ +import { Template } from '@aws-cdk/assertions'; +import * as cdk from '@aws-cdk/core'; +import * as servicecatalog from '../lib'; + +describe('TagOptions', () => { + let app: cdk.App; + let stack: cdk.Stack; + + beforeEach(() => { + app = new cdk.App(); + stack = new cdk.Stack(app); + }); + + describe('creating tagOption(s)', () => { + test('default tagOptions creation', () => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1', 'value2', 'value3'], + }, + }); + + Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 5); + }), + + test('fails to create tag option with invalid minimum key length', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + '': ['value1', 'value2'], + }, + }); + }).toThrowError(/Invalid TagOption key for resource/); + }), + + test('fails to create tag option with invalid maxium key length', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + ['longKey'.repeat(1000)]: ['value1', 'value2'], + }, + }); + }).toThrowError(/Invalid TagOption key for resource/); + }), + + test('fails to create tag option with invalid value length', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key: ['tagOptionValue'.repeat(1000)], + }, + }); + }).toThrowError(/Invalid TagOption value for resource/); + }), + + test('fails to create tag options with no tag keys or values', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: {}, + }); + }).toThrowError(/No tag option keys or values were provided/); + }), + + test('fails to create tag options for tag key with no values', () => { + expect(() => { + new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: [], + }, + }); + }).toThrowError(/No tag option values were provided for tag option key/); + }), + + test('associate tag options', () => { + const portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { + displayName: 'testPortfolio', + providerName: 'testProvider', + }); + + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1', 'value2', 'value3'], + }, + }); + portfolio.associateTagOptions(tagOptions); + + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOption', 5); + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOptionAssociation', 5); + }), + + test('creating tag options with duplicate values is idempotent', () => { + const portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { + displayName: 'testPortfolio', + providerName: 'testProvider', + }); + + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2', 'value2'], + key2: ['value1', 'value2', 'value3', 'value3'], + }, + }); + portfolio.associateTagOptions(tagOptions); + + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOption', 5); + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOptionAssociation', 5); + }), + + test('create and associate tag options to different resources', () => { + const portfolio1 = new servicecatalog.Portfolio(stack, 'MyPortfolio1', { + displayName: 'testPortfolio1', + providerName: 'testProvider1', + }); + + const portfolio2 = new servicecatalog.Portfolio(stack, 'MyPortfolio2', { + displayName: 'testPortfolio2', + providerName: 'testProvider2', + }); + + const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1', 'value2', 'value3'], + }, + }); + + portfolio1.associateTagOptions(tagOptions); + portfolio2.associateTagOptions(tagOptions); + + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOption', 5); + Template.fromStack(stack).hasResource('AWS::ServiceCatalog::TagOptionAssociation', 10); + }), + + test('create and associate tag options in another stack', () => { + const tagOptionsStack = new cdk.Stack(app, 'TagOptionsStack'); + const productStack = new cdk.Stack(app, 'ProductStack'); + + const tagOptions = new servicecatalog.TagOptions(tagOptionsStack, 'TagOptions', { + allowedValuesForTags: { + key1: ['value1', 'value2'], + key2: ['value1', 'value2', 'value3'], + }, + }); + + new servicecatalog.CloudFormationProduct(productStack, 'MyProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl('https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template'), + }, + ], + tagOptions: tagOptions, + }); + + Template.fromStack(tagOptionsStack).hasResource('AWS::ServiceCatalog::TagOption', 5); + Template.fromStack(productStack).resourceCountIs('AWS::ServiceCatalog::TagOption', 0); + Template.fromStack(productStack).hasResource('AWS::ServiceCatalog::TagOptionAssociation', 5); + }); + }); +});