-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Conversation
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.
This should address: #17753 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First round of comments. We definitely need to minimize the amount of changes we are making to the tests in this PR, as we're changing production code at the same time. I also thing TagOptions
should become a full Construct.
packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts
Outdated
Show resolved
Hide resolved
Let's add a |
Pull request has been modified.
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', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are also not really valid but we can modify it slightly to still work, unfortunately still a lot of git churn. Moving the creation away from the association means that early failures happen on creation not via association calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. If these now fail on creation, let's modify the test and its description to reflect that (being careful to minimize the changes still).
I understand this will mean these tests are now in the wrong file, but we'll live with that for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated with modified wording and minimized changes.
*/ | ||
public readonly _tagOptionsMap: { [key: string]: CfnTagOption }; | ||
|
||
constructor(scope: Construct, id: string, tagOptions: {[key: string]: string[] }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cdk linting doesn't like this setup (no interface and no props struct) but I don't particularly see value of having classes that are just pass throughs. I don't know if making it props with a tagOptions
field is better since it seems redundant to do
const tagO = new servicecatalog.TagOptions(this, 'TagOptions', {
tagOptions : {
key1: ['v1', 'v2', 'v3'],
}
});
Only if its a little bit of future proofing in case we want to add another field to props then it would be easier to make change.
I will defer to you though on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think linting rules have become more stringent? Build is failing since you apparently need to disable interface ref in js docs ( I had hoped I could just do it in package.json).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just introduce a TagOptionsProps
interface as the third argument, like you suggested in #18314 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that works, leaves more wiggle room to not break things in future should changes happen and we want to modify props.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are getting closer, but I think this needs a couple more iterations before we can call it "Done".
* List of CfnTagOption | ||
*/ | ||
public readonly tagOptionsMap: { [key: string]: string[] }; | ||
* TagOption keys and associated list of possible values. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like these docs need some love. How about something like:
* TagOption keys and associated list of possible values. | |
* The values that are allowed to be set for specific tags. | |
* The keys of the map are the names of the tags, | |
* and the values of the map are a list of allowed values of that particular tag. |
Feel free to add more as you see fit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be confusing with keys in a map struct and keys to a tag, but I think we should stick with using tag key as the wording. I don't know if I've seen tags referenced with a name and not just key-value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine, then use the name "tag key". That's why I said "something like that" and "feel free to add more as you see fit" 🙂.
|
||
constructor(scope: Construct, id: string, props: TagOptionsProps) { | ||
super(scope, id); | ||
this._tagOptionsMap = this.createUnderlyingTagOptions(props.tagOptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it valid to have an empty map for props.tagOptions
? I would guess 'no', right? Perhaps we need to validate that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we iterate over the underlying values for creation and associating, if you do an empty one it will not fail and it won't create anything. I can add a test for this case as well, or since this it's mostly useless we could validate and fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better for this to fail since it wouldn't align with other constructs to instantiate one with a certain (empty) configuration that wouldn't yield anything in template. I've therefore added validation to make sure that the struct is not empty, and then that for each key the tag values are not empty, along with two tests for this. There is not a max length for these lists since I believe that these can be altered by the customer.
Pull request has been modified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great @arcrank. I basically have one last major question, and after that one is resolved, we can merge this one in.
}); | ||
}; | ||
for (const [tagOptionIdentifier, cfnTagOption] of Object.entries(tagOptions._tagOptionsMap)) { | ||
const tagAssocationConstructId = `TagOptionAssociation${hashValues(resource.node.addr, tagOptionIdentifier)}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, this is interesting - we are actually changing the algorithm used for generating the identifier of the CfnTagOptionAssociation
resource. Previously, it was `TagOptionAssociation${hashValues(key, value, resource.node.addr)}`
- now, it's `TagOptionAssociation${hashValues(resource.node.addr, tagOptionIdentifier)}`
.
I wonder why this change? Wouldn't keeping the same algorithm reduce the number of changes in the template for customers upgrading to a new CDK version that will include these changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been able to maintain current behavior here. I didn't try to maintain this before because from my OOP life it felt wrong to add a more coupling between the two resources, and I more or less tracked the impact of changes that would happen as all or nothing - since we could expect to see the TagOption resources changing I thought it might just go hand in hand to expect associations to change as well.
If this new version is fine, my understanding is now better for it.
value: tagValue, | ||
active: true, | ||
}); | ||
tagOptionMap[tagOptionIdentifier] = tagOption; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since tagOptionIdentifier
is also the identifier of the CfnTagOption
resource, is there actually any reason for this map? I think we can just have an array of CfnTagOption
, and then retrieve its identifier with cfnTagOption.node.id
(or possibly Node.of(cfnTagOption).id
if it uses the new construct API).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can just make this a list, the map part was a relic of an earlier point where we considered adding lookup access to an underlying tagOption(s), and a map felt more efficient than searching list.
|
||
Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOption', 4); //Generates a resource for each unique key-value pair | ||
Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalog::TagOptionAssociation', 6); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you instead keep key1
as having 2 values, and this way, we won't have to touch the assertions in this test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good call out.
key1: ['value1'.repeat(1000), 'value2'], | ||
key2: ['value1'], | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add an empty line after this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks beautiful @arcrank, awesome job!
Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork). |
AWS CodeBuild CI Report
Powered by github-codebuild-logs, available on the AWS Serverless Application Repository |
Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork). |
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*
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*
Fixes: #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 havescope
andprops
argument in constructor, and data is now passed via aallowedValueForTags
field in propsBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license