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 1 commit
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
Next Next commit
feat(servicecatalog): Create standalone TagOptions Construct
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.

Based on updated contributing guidelines, need to figure out how to note this as a breaking change, or figure out how to properly deprecate old version.
  • Loading branch information
Aidan Crank committed Jan 7, 2022
commit d0e30d30c9a6380121bc20fef34f12b29fd15e9d
11 changes: 5 additions & 6 deletions packages/@aws-cdk/aws-servicecatalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,21 +201,20 @@ 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.
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, {
arcrank marked this conversation as resolved.
Show resolved Hide resolved
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, {
environment: ['dev', 'alpha', 'prod'],
});
product.associateTagOptions(tagOptionsForProduct);
```
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export interface IPortfolio extends cdk.IResource {
* A TagOption is a key-value pair 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.
*/
associateTagOptions(tagOptions: TagOptions): void;
associateTagOptions(tagOptionsResource: TagOptions): void;
arcrank marked this conversation as resolved.
Show resolved Hide resolved

/**
* Add a Resource Update Constraint.
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,36 +139,7 @@ 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,
});
}
});
};
}

private static setLaunchRoleConstraint(
public static setLaunchRoleConstraint(
arcrank marked this conversation as resolved.
Show resolved Hide resolved
portfolio: IPortfolio, product: IProduct, options: CommonConstraintOptions,
roleOptions: LaunchRoleConstraintRoleOptions,
): void {
Expand Down Expand Up @@ -196,6 +167,18 @@ export class AssociationManager {
}
}

public static associateTagOptions(resource: cdk.IResource, resourceId: string, tagOptions: TagOptions): void {
arcrank marked this conversation as resolved.
Show resolved Hide resolved
for (const [tagOptionIdentifier, cfnTagOption] of Object.entries(tagOptions.tagOptionsMap)) {
const tagAssocationConstructId = `TagOptionAssociation${hashValues(resource.node.addr, tagOptionIdentifier)}`;
if (!resource.node.tryFindChild(tagAssocationConstructId)) {
new CfnTagOptionAssociation(resource as cdk.Resource, tagAssocationConstructId, {
resourceId: resourceId,
tagOptionId: cfnTagOption.ref,
});
}
}
}

private static stackSetConstraintLogicalId(associationKey: string): string {
return `StackSetConstraint${associationKey}`;
}
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
42 changes: 34 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,40 @@
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';

/**
* 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
*/
export class TagOptions {
arcrank marked this conversation as resolved.
Show resolved Hide resolved
/**
* List of CfnTagOption
*/
public readonly tagOptionsMap: { [key: string]: string[] };
* Map of underlying TagOption resources.
*/
public readonly tagOptionsMap: { [key: string]: CfnTagOption };
arcrank marked this conversation as resolved.
Show resolved Hide resolved

constructor(scope: Construct, tagOptions: { [key: string]: string[] }) {
this.tagOptionsMap = this.createUnderlyingTagOptions(scope, tagOptions);
}

constructor(tagOptionsMap: { [key: string]: string[]} ) {
this.tagOptionsMap = { ...tagOptionsMap };
private createUnderlyingTagOptions(scope: Construct, tagOptions: { [key: string]: string[] }): { [key: string]: CfnTagOption } {
var tagOptionMap: { [key: string]: CfnTagOption } = {};
for (const [key, tagOptionsList] of Object.entries(tagOptions)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key is also a bad name. This is actually a tagName, right?

Copy link
Contributor Author

@arcrank arcrank Jan 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure,
the raw values -> tagKey: tagValue and then the hashed value in map tagOptionIdentifier

InputValidator.validateLength(cdk.Stack.of(scope).node.addr, 'TagOption key', 1, 128, key);
tagOptionsList.forEach((value: string) => {
arcrank marked this conversation as resolved.
Show resolved Hide resolved
InputValidator.validateLength(cdk.Stack.of(scope).node.addr, 'TagOption value', 1, 256, value);
arcrank marked this conversation as resolved.
Show resolved Hide resolved
const tagOptionIdentifier = `TagOptions${hashValues(key, value)}`;
const tagOption = new CfnTagOption(cdk.Stack.of(scope), tagOptionIdentifier, {
key: key,
value: value,
});
tagOptionMap[tagOptionIdentifier] = tagOption;
});
}
return tagOptionMap;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,36 +74,36 @@
"PrincipalType": "IAM"
}
},
"TestPortfolioTagOptionAssociation517ba9dbaf19EA8252F0": {
"TestPortfolioTagOptionAssociationf06f122d082847BFE4AE": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestPortfolio4AC794EB"
},
"TagOptionId": {
"Ref": "TagOptionc0d88a3c4b8b"
"Ref": "TagOptions5f31c54ba705"
}
}
},
"TestPortfolioTagOptionAssociationb38e9aae7f1bD3708991": {
"TestPortfolioTagOptionAssociationb35420abccbaCA297C32": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestPortfolio4AC794EB"
},
"TagOptionId": {
"Ref": "TagOption9b16df08f83d"
"Ref": "TagOptions8d263919cebb"
}
}
},
"TestPortfolioTagOptionAssociationeeabbf0db0e3ADBF0A6D": {
"TestPortfolioTagOptionAssociatione2071bfe8775D9A3459B": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestPortfolio4AC794EB"
},
"TagOptionId": {
"Ref": "TagOptiondf34c1c83580"
"Ref": "TagOptionsa260cbbd99c4"
}
}
},
Expand Down Expand Up @@ -217,28 +217,25 @@
"TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7"
]
},
"TagOptionc0d88a3c4b8b": {
"TagOptions5f31c54ba705": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key1",
"Value": "value1",
"Active": true
arcrank marked this conversation as resolved.
Show resolved Hide resolved
"Value": "value1"
}
},
"TagOption9b16df08f83d": {
"TagOptions8d263919cebb": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key1",
"Value": "value2",
"Active": true
"Value": "value2"
}
},
"TagOptiondf34c1c83580": {
"TagOptionsa260cbbd99c4": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key2",
"Value": "value1",
"Active": true
"Value": "value1"
}
},
"TestProduct7606930B": {
Expand All @@ -256,36 +253,36 @@
]
}
},
"TestProductTagOptionAssociation667d45e6d8a1F30303D6": {
"TestProductTagOptionAssociationb5c57714747b17B5A4B9": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptionc0d88a3c4b8b"
"Ref": "TagOptions5f31c54ba705"
}
}
},
"TestProductTagOptionAssociationec68fcd0154fF6DAD979": {
"TestProductTagOptionAssociation7ee29452deef9CF31B28": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOption9b16df08f83d"
"Ref": "TagOptions8d263919cebb"
}
}
},
"TestProductTagOptionAssociation259ba31b62cc63D068F9": {
"TestProductTagOptionAssociation68313d1fb1a1175D9769": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptiondf34c1c83580"
"Ref": "TagOptionsa260cbbd99c4"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const portfolio = new servicecatalog.Portfolio(stack, 'TestPortfolio', {
portfolio.giveAccessToRole(role);
portfolio.giveAccessToGroup(group);

const tagOptions = new servicecatalog.TagOptions({
const tagOptions = new servicecatalog.TagOptions(stack, {
key1: ['value1', 'value2'],
key2: ['value1'],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,61 +219,58 @@
]
}
},
"TestProductTagOptionAssociation0d813eebb333DA3E2F21": {
"TestProductTagOptionAssociation6141ab9843c862231887": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptionab501c9aef99"
"Ref": "TagOptions5f31c54ba705"
}
}
},
"TestProductTagOptionAssociation5d93a5c977b4B664DD87": {
"TestProductTagOptionAssociation334c7d1b98adE56B2791": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptiona453ac93ee6f"
"Ref": "TagOptions8d263919cebb"
}
}
},
"TestProductTagOptionAssociationcfaf40b186a3E5FDECDC": {
"TestProductTagOptionAssociation0696c10763d05579CC98": {
"Type": "AWS::ServiceCatalog::TagOptionAssociation",
"Properties": {
"ResourceId": {
"Ref": "TestProduct7606930B"
},
"TagOptionId": {
"Ref": "TagOptiona006431604cb"
"Ref": "TagOptionsa260cbbd99c4"
}
}
},
"TagOptionab501c9aef99": {
"TagOptions5f31c54ba705": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key1",
"Value": "value1",
"Active": true
"Value": "value1"
}
},
"TagOptiona453ac93ee6f": {
"TagOptions8d263919cebb": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key1",
"Value": "value2",
"Active": true
"Value": "value2"
}
},
"TagOptiona006431604cb": {
"TagOptionsa260cbbd99c4": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
"Key": "key2",
"Value": "value1",
"Active": true
"Value": "value1"
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', {
],
});

const tagOptions = new servicecatalog.TagOptions({
const tagOptions = new servicecatalog.TagOptions(stack, {
key1: ['value1', 'value2'],
key2: ['value1'],
});
Expand Down
Loading