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

feat(servicecatalog): Create TagOptions Construct #18314

Merged
merged 19 commits into from
Jan 25, 2022
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
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.
arcrank marked this conversation as resolved.
Show resolved Hide resolved
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.
arcrank marked this conversation as resolved.
Show resolved Hide resolved
* See https://docs.aws.amazon.com/servicecatalog/latest/adminguide/tagoptions.html
*
* @resource AWS::ServiceCatalog::TagOption
arcrank marked this conversation as resolved.
Show resolved Hide resolved
*/
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);
arcrank marked this conversation as resolved.
Show resolved Hide resolved

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
arcrank marked this conversation as resolved.
Show resolved Hide resolved
}
},
"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