Skip to content

Commit 903c4b6

Browse files
authored
feat(servicecatalog): Create TagOptions Construct (#18314)
Fixes: [#17753](#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*
1 parent 9bea576 commit 903c4b6

12 files changed

+351
-127
lines changed

packages/@aws-cdk/aws-servicecatalog/README.md

+10-7
Original file line numberDiff line numberDiff line change
@@ -201,21 +201,24 @@ portfolio.addProduct(product);
201201
## Tag Options
202202

203203
TagOptions allow administrators to easily manage tags on provisioned products by creating a selection of tags for end users to choose from.
204-
For example, an end user can choose an `ec2` for the instance type size.
205-
TagOptions are created by specifying a key with a selection of values and can be associated with both portfolios and products.
204+
TagOptions are created by specifying a tag key with a selection of allowed values and can be associated with both portfolios and products.
206205
When launching a product, both the TagOptions associated with the product and the containing portfolio are made available.
207206

208207
At the moment, TagOptions can only be disabled in the console.
209208

210209
```ts fixture=portfolio-product
211-
const tagOptionsForPortfolio = new servicecatalog.TagOptions({
212-
costCenter: ['Data Insights', 'Marketing'],
210+
const tagOptionsForPortfolio = new servicecatalog.TagOptions(this, 'OrgTagOptions', {
211+
allowedValuesForTags: {
212+
Group: ['finance', 'engineering', 'marketing', 'research'],
213+
CostCenter: ['01', '02','03'],
214+
},
213215
});
214216
portfolio.associateTagOptions(tagOptionsForPortfolio);
215217

216-
const tagOptionsForProduct = new servicecatalog.TagOptions({
217-
ec2InstanceType: ['A1', 'M4'],
218-
ec2InstanceSize: ['medium', 'large'],
218+
const tagOptionsForProduct = new servicecatalog.TagOptions(this, 'ProductTagOptions', {
219+
allowedValuesForTags: {
220+
Environment: ['dev', 'alpha', 'prod'],
221+
},
219222
});
220223
product.associateTagOptions(tagOptionsForProduct);
221224
```

packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts

+10-27
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { IPortfolio } from '../portfolio';
99
import { IProduct } from '../product';
1010
import {
1111
CfnLaunchNotificationConstraint, CfnLaunchRoleConstraint, CfnLaunchTemplateConstraint, CfnPortfolioProductAssociation,
12-
CfnResourceUpdateConstraint, CfnStackSetConstraint, CfnTagOption, CfnTagOptionAssociation,
12+
CfnResourceUpdateConstraint, CfnStackSetConstraint, CfnTagOptionAssociation,
1313
} from '../servicecatalog.generated';
1414
import { TagOptions } from '../tag-options';
1515
import { hashValues } from './util';
@@ -139,33 +139,16 @@ export class AssociationManager {
139139
}
140140
}
141141

142-
143142
public static associateTagOptions(resource: cdk.IResource, resourceId: string, tagOptions: TagOptions): void {
144-
const resourceStack = cdk.Stack.of(resource);
145-
for (const [key, tagOptionsList] of Object.entries(tagOptions.tagOptionsMap)) {
146-
InputValidator.validateLength(resource.node.addr, 'TagOption key', 1, 128, key);
147-
tagOptionsList.forEach((value: string) => {
148-
InputValidator.validateLength(resource.node.addr, 'TagOption value', 1, 256, value);
149-
const tagOptionKey = hashValues(key, value, resourceStack.node.addr);
150-
const tagOptionConstructId = `TagOption${tagOptionKey}`;
151-
let cfnTagOption = resourceStack.node.tryFindChild(tagOptionConstructId) as CfnTagOption;
152-
if (!cfnTagOption) {
153-
cfnTagOption = new CfnTagOption(resourceStack, tagOptionConstructId, {
154-
key: key,
155-
value: value,
156-
active: true,
157-
});
158-
}
159-
const tagAssocationKey = hashValues(key, value, resource.node.addr);
160-
const tagAssocationConstructId = `TagOptionAssociation${tagAssocationKey}`;
161-
if (!resource.node.tryFindChild(tagAssocationConstructId)) {
162-
new CfnTagOptionAssociation(resource as cdk.Resource, tagAssocationConstructId, {
163-
resourceId: resourceId,
164-
tagOptionId: cfnTagOption.ref,
165-
});
166-
}
167-
});
168-
};
143+
for (const cfnTagOption of tagOptions._cfnTagOptions) {
144+
const tagAssocationConstructId = `TagOptionAssociation${hashValues(cfnTagOption.key, cfnTagOption.value, resource.node.addr)}`;
145+
if (!resource.node.tryFindChild(tagAssocationConstructId)) {
146+
new CfnTagOptionAssociation(resource as cdk.Resource, tagAssocationConstructId, {
147+
resourceId: resourceId,
148+
tagOptionId: cfnTagOption.ref,
149+
});
150+
}
151+
}
169152
}
170153

171154
private static setLaunchRoleConstraint(

packages/@aws-cdk/aws-servicecatalog/lib/product.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { ArnFormat, IResource, Resource, Stack } from '@aws-cdk/core';
22
import { Construct } from 'constructs';
3-
import { TagOptions } from '.';
43
import { CloudFormationTemplate } from './cloudformation-template';
54
import { MessageLanguage } from './common';
65
import { AssociationManager } from './private/association-manager';
76
import { InputValidator } from './private/validation';
87
import { CfnCloudFormationProduct } from './servicecatalog.generated';
8+
import { TagOptions } from './tag-options';
99

1010
/**
1111
* A Service Catalog product, currently only supports type CloudFormationProduct
@@ -137,7 +137,7 @@ export interface CloudFormationProductProps {
137137
*
138138
* @default - No tagOptions provided
139139
*/
140-
readonly tagOptions?: TagOptions
140+
readonly tagOptions?: TagOptions;
141141
}
142142

143143
/**
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,70 @@
1+
import * as cdk from '@aws-cdk/core';
2+
import { hashValues } from './private/util';
3+
import { InputValidator } from './private/validation';
4+
import { CfnTagOption } from './servicecatalog.generated';
5+
6+
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
7+
// eslint-disable-next-line no-duplicate-imports, import/order
8+
import { Construct } from 'constructs';
9+
10+
/**
11+
* Properties for TagOptions.
12+
*/
13+
export interface TagOptionsProps {
14+
/**
15+
* The values that are allowed to be set for specific tags.
16+
* The keys of the map represent the tag keys,
17+
* and the values of the map are a list of allowed values for that particular tag key.
18+
*/
19+
readonly allowedValuesForTags: { [tagKey: string]: string[] };
20+
}
21+
122
/**
2-
* Defines a Tag Option, which are similar to tags
3-
* but have multiple values per key.
23+
* Defines a set of TagOptions, which are a list of key-value pairs managed in AWS Service Catalog.
24+
* It is not an AWS tag, but serves as a template for creating an AWS tag based on the TagOption.
25+
* See https://docs.aws.amazon.com/servicecatalog/latest/adminguide/tagoptions.html
26+
*
27+
* @resource AWS::ServiceCatalog::TagOption
428
*/
5-
export class TagOptions {
29+
export class TagOptions extends cdk.Resource {
630
/**
7-
* List of CfnTagOption
8-
*/
9-
public readonly tagOptionsMap: { [key: string]: string[] };
31+
* List of underlying CfnTagOption resources.
32+
*
33+
* @internal
34+
*/
35+
public _cfnTagOptions: CfnTagOption[];
1036

11-
constructor(tagOptionsMap: { [key: string]: string[]} ) {
12-
this.tagOptionsMap = { ...tagOptionsMap };
37+
constructor(scope: Construct, id: string, props: TagOptionsProps) {
38+
super(scope, id);
39+
40+
this._cfnTagOptions = this.createUnderlyingTagOptions(props.allowedValuesForTags);
41+
}
42+
43+
private createUnderlyingTagOptions(allowedValuesForTags: { [tagKey: string]: string[] }): CfnTagOption[] {
44+
if (Object.keys(allowedValuesForTags).length === 0) {
45+
throw new Error(`No tag option keys or values were provided for resource ${this.node.path}`);
46+
}
47+
var tagOptions: CfnTagOption[] = [];
48+
49+
for (const [tagKey, tagValues] of Object.entries(allowedValuesForTags)) {
50+
InputValidator.validateLength(this.node.addr, 'TagOption key', 1, 128, tagKey);
51+
52+
const uniqueTagValues = new Set(tagValues);
53+
if (uniqueTagValues.size === 0) {
54+
throw new Error(`No tag option values were provided for tag option key ${tagKey} for resource ${this.node.path}`);
55+
}
56+
uniqueTagValues.forEach((tagValue: string) => {
57+
InputValidator.validateLength(this.node.addr, 'TagOption value', 1, 256, tagValue);
58+
const tagOptionIdentifier = hashValues(tagKey, tagValue);
59+
const tagOption = new CfnTagOption(this, tagOptionIdentifier, {
60+
key: tagKey,
61+
value: tagValue,
62+
active: true,
63+
});
64+
tagOptions.push(tagOption);
65+
});
66+
}
67+
return tagOptions;
1368
}
1469
}
70+

packages/@aws-cdk/aws-servicecatalog/package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@
105105
"props-physical-name:@aws-cdk/aws-servicecatalog.CloudFormationProductProps",
106106
"resource-attribute:@aws-cdk/aws-servicecatalog.Portfolio.portfolioName",
107107
"props-physical-name:@aws-cdk/aws-servicecatalog.PortfolioProps",
108-
"props-physical-name:@aws-cdk/aws-servicecatalog.ProductStack"
108+
"props-physical-name:@aws-cdk/aws-servicecatalog.ProductStack",
109+
"props-struct-name:@aws-cdk/aws-servicecatalog.ITagOptions",
110+
"props-physical-name:@aws-cdk/aws-servicecatalog.TagOptionsProps",
111+
"ref-via-interface:@aws-cdk/aws-servicecatalog.CloudFormationProductProps.tagOptions",
112+
"ref-via-interface:@aws-cdk/aws-servicecatalog.IProduct.associateTagOptions.tagOptions",
113+
"ref-via-interface:@aws-cdk/aws-servicecatalog.IPortfolio.associateTagOptions.tagOptions",
114+
"ref-via-interface:@aws-cdk/aws-servicecatalog.PortfolioProps.tagOptions"
109115
]
110116
},
111117
"maturity": "experimental",

packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json

+9-9
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
"Ref": "TestPortfolio4AC794EB"
8282
},
8383
"TagOptionId": {
84-
"Ref": "TagOptionc0d88a3c4b8b"
84+
"Ref": "TagOptions5f31c54ba705F110F743"
8585
}
8686
}
8787
},
@@ -92,7 +92,7 @@
9292
"Ref": "TestPortfolio4AC794EB"
9393
},
9494
"TagOptionId": {
95-
"Ref": "TagOption9b16df08f83d"
95+
"Ref": "TagOptions8d263919cebb6764AC10"
9696
}
9797
}
9898
},
@@ -103,7 +103,7 @@
103103
"Ref": "TestPortfolio4AC794EB"
104104
},
105105
"TagOptionId": {
106-
"Ref": "TagOptiondf34c1c83580"
106+
"Ref": "TagOptionsa260cbbd99c416C40F73"
107107
}
108108
}
109109
},
@@ -217,23 +217,23 @@
217217
"TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7"
218218
]
219219
},
220-
"TagOptionc0d88a3c4b8b": {
220+
"TagOptions5f31c54ba705F110F743": {
221221
"Type": "AWS::ServiceCatalog::TagOption",
222222
"Properties": {
223223
"Key": "key1",
224224
"Value": "value1",
225225
"Active": true
226226
}
227227
},
228-
"TagOption9b16df08f83d": {
228+
"TagOptions8d263919cebb6764AC10": {
229229
"Type": "AWS::ServiceCatalog::TagOption",
230230
"Properties": {
231231
"Key": "key1",
232232
"Value": "value2",
233233
"Active": true
234234
}
235235
},
236-
"TagOptiondf34c1c83580": {
236+
"TagOptionsa260cbbd99c416C40F73": {
237237
"Type": "AWS::ServiceCatalog::TagOption",
238238
"Properties": {
239239
"Key": "key2",
@@ -263,7 +263,7 @@
263263
"Ref": "TestProduct7606930B"
264264
},
265265
"TagOptionId": {
266-
"Ref": "TagOptionc0d88a3c4b8b"
266+
"Ref": "TagOptions5f31c54ba705F110F743"
267267
}
268268
}
269269
},
@@ -274,7 +274,7 @@
274274
"Ref": "TestProduct7606930B"
275275
},
276276
"TagOptionId": {
277-
"Ref": "TagOption9b16df08f83d"
277+
"Ref": "TagOptions8d263919cebb6764AC10"
278278
}
279279
}
280280
},
@@ -285,7 +285,7 @@
285285
"Ref": "TestProduct7606930B"
286286
},
287287
"TagOptionId": {
288-
"Ref": "TagOptiondf34c1c83580"
288+
"Ref": "TagOptionsa260cbbd99c416C40F73"
289289
}
290290
}
291291
},

packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ const portfolio = new servicecatalog.Portfolio(stack, 'TestPortfolio', {
2222
portfolio.giveAccessToRole(role);
2323
portfolio.giveAccessToGroup(group);
2424

25-
const tagOptions = new servicecatalog.TagOptions({
26-
key1: ['value1', 'value2'],
27-
key2: ['value1'],
25+
const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', {
26+
allowedValuesForTags: {
27+
key1: ['value1', 'value2'],
28+
key2: ['value1'],
29+
},
2830
});
2931
portfolio.associateTagOptions(tagOptions);
3032

packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@
226226
"Ref": "TestProduct7606930B"
227227
},
228228
"TagOptionId": {
229-
"Ref": "TagOptionab501c9aef99"
229+
"Ref": "TagOptions5f31c54ba705F110F743"
230230
}
231231
}
232232
},
@@ -237,7 +237,7 @@
237237
"Ref": "TestProduct7606930B"
238238
},
239239
"TagOptionId": {
240-
"Ref": "TagOptiona453ac93ee6f"
240+
"Ref": "TagOptions8d263919cebb6764AC10"
241241
}
242242
}
243243
},
@@ -248,27 +248,27 @@
248248
"Ref": "TestProduct7606930B"
249249
},
250250
"TagOptionId": {
251-
"Ref": "TagOptiona006431604cb"
251+
"Ref": "TagOptionsa260cbbd99c416C40F73"
252252
}
253253
}
254254
},
255-
"TagOptionab501c9aef99": {
255+
"TagOptions5f31c54ba705F110F743": {
256256
"Type": "AWS::ServiceCatalog::TagOption",
257257
"Properties": {
258258
"Key": "key1",
259259
"Value": "value1",
260260
"Active": true
261261
}
262262
},
263-
"TagOptiona453ac93ee6f": {
263+
"TagOptions8d263919cebb6764AC10": {
264264
"Type": "AWS::ServiceCatalog::TagOption",
265265
"Properties": {
266266
"Key": "key1",
267267
"Value": "value2",
268268
"Active": true
269269
}
270270
},
271-
"TagOptiona006431604cb": {
271+
"TagOptionsa260cbbd99c416C40F73": {
272272
"Type": "AWS::ServiceCatalog::TagOption",
273273
"Properties": {
274274
"Key": "key2",

packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', {
3838
],
3939
});
4040

41-
const tagOptions = new servicecatalog.TagOptions({
42-
key1: ['value1', 'value2'],
43-
key2: ['value1'],
41+
const tagOptions = new servicecatalog.TagOptions(stack, 'TagOptions', {
42+
allowedValuesForTags: {
43+
key1: ['value1', 'value2'],
44+
key2: ['value1'],
45+
},
4446
});
4547

4648
product.associateTagOptions(tagOptions);

0 commit comments

Comments
 (0)