Skip to content

Commit

Permalink
feat(servicecatalog): Create TagOptions Construct (aws#18314)
Browse files Browse the repository at this point in the history
Fixes:  [aws#17753](aws#17753)


Previously TagOptions were defined via an interface and we only created the underlying
resources upon an association.  This broke CX if tagoptions were mangaged centrally.  We move to
make the TagOptions class a wrapper around aggregate individual TagOptions.

BREAKING CHANGE: `TagOptions` now have `scope` and `props` argument in constructor, and data is now passed via a `allowedValueForTags` field in props

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
arcrank authored and LukvonStrom committed Jan 26, 2022
1 parent c52df24 commit bbf0571
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 127 deletions.
17 changes: 10 additions & 7 deletions packages/@aws-cdk/aws-servicecatalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-servicecatalog/lib/product.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -137,7 +137,7 @@ export interface CloudFormationProductProps {
*
* @default - No tagOptions provided
*/
readonly tagOptions?: TagOptions
readonly tagOptions?: TagOptions;
}

/**
Expand Down
72 changes: 64 additions & 8 deletions packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}

8 changes: 7 additions & 1 deletion packages/@aws-cdk/aws-servicecatalog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"Ref": "TestPortfolio4AC794EB"
},
"TagOptionId": {
"Ref": "TagOptionc0d88a3c4b8b"
"Ref": "TagOptions5f31c54ba705F110F743"
}
}
},
Expand All @@ -92,7 +92,7 @@
"Ref": "TestPortfolio4AC794EB"
},
"TagOptionId": {
"Ref": "TagOption9b16df08f83d"
"Ref": "TagOptions8d263919cebb6764AC10"
}
}
},
Expand All @@ -103,7 +103,7 @@
"Ref": "TestPortfolio4AC794EB"
},
"TagOptionId": {
"Ref": "TagOptiondf34c1c83580"
"Ref": "TagOptionsa260cbbd99c416C40F73"
}
}
},
Expand Down Expand Up @@ -217,23 +217,23 @@
"TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7"
]
},
"TagOptionc0d88a3c4b8b": {
"TagOptions5f31c54ba705F110F743": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key1",
"Value": "value1",
"Active": true
}
},
"TagOption9b16df08f83d": {
"TagOptions8d263919cebb6764AC10": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key1",
"Value": "value2",
"Active": true
}
},
"TagOptiondf34c1c83580": {
"TagOptionsa260cbbd99c416C40F73": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key2",
Expand Down Expand Up @@ -263,7 +263,7 @@
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptionc0d88a3c4b8b"
"Ref": "TagOptions5f31c54ba705F110F743"
}
}
},
Expand All @@ -274,7 +274,7 @@
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOption9b16df08f83d"
"Ref": "TagOptions8d263919cebb6764AC10"
}
}
},
Expand All @@ -285,7 +285,7 @@
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptiondf34c1c83580"
"Ref": "TagOptionsa260cbbd99c416C40F73"
}
}
},
Expand Down
8 changes: 5 additions & 3 deletions packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptionab501c9aef99"
"Ref": "TagOptions5f31c54ba705F110F743"
}
}
},
Expand All @@ -237,7 +237,7 @@
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptiona453ac93ee6f"
"Ref": "TagOptions8d263919cebb6764AC10"
}
}
},
Expand All @@ -248,27 +248,27 @@
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptiona006431604cb"
"Ref": "TagOptionsa260cbbd99c416C40F73"
}
}
},
"TagOptionab501c9aef99": {
"TagOptions5f31c54ba705F110F743": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key1",
"Value": "value1",
"Active": true
}
},
"TagOptiona453ac93ee6f": {
"TagOptions8d263919cebb6764AC10": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key1",
"Value": "value2",
"Active": true
}
},
"TagOptiona006431604cb": {
"TagOptionsa260cbbd99c416C40F73": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key2",
Expand Down
8 changes: 5 additions & 3 deletions packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit bbf0571

Please sign in to comment.