-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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): ProductStackHistory can retain old ProductStack iterations #20244
Conversation
28059cb
to
522ed7f
Compare
This PR cannot be merged because it has conflicts. Please resolve them. The PR will be considered stale and closed if it remains in an unmergeable state. |
Hi i am not sure how to fix the conflicting files. I tried both force updating and creating a separate commit but it does not seem to help. The second commit has no conflict with the first commit. It should be as simple as taking the latest |
This PR cannot be merged because it has conflicts. Please resolve them. The PR will be considered stale and closed if it remains in an unmergeable state. |
And port your changes from |
48166c3
to
b4e67c6
Compare
…loyed ProductStacks.
b4e67c6
to
6ab53e6
Compare
@wanjacki Looks good. 👍 |
@robertd Yes got it working. Thanks alot |
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.
This is a good first attempt! Some clarification needs to be added around the versioningStrategy
and how it works.
We can also define a `versioningStrategy` for your `ProductStack` deployment. | ||
With the `Default` strategy, a new `ProductStack` is needed for each `productVersion` | ||
and each `productVersion` will get overwritten with the latest changes to your `ProductStack`. | ||
With the `RetainPreviousVersions` strategy, a local template will be retained for each `ProductStack` deployed. | ||
Subsequently, any `ProductStack` changes will not overwrite the current version. | ||
A new `productVersionName` must be specified in order for `ProductStack` changes to be deployed. | ||
When the `ProductStack` is updated with a new version, the previous version can still be deployed. | ||
The template of previously deployed `ProductStack` can be referenced using `fromProductStackContext` | ||
and passing the corresponding `ProductStack` id. |
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.
what happens if a user specifies the Default
strategy, adds several new productVersion
s, then changes the strategy to RetainPreviousVersions
? What happens in the inverse (specifying RetainPreviousVersions
, and then switching to Default
)? Are the retained versions then lost, if the strategy changes to Default
?
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.
This might be a sign of poor naming.
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.
If a user specifies the Default
strategy then switches to RetainPreviousVersions
. Only versions deployed after the switch will be retained.
If they do the reverse, then all further deployed versions will no longer be retained. Previously retained versions will not be deleted.
Do you have a suggestion on how we can name this behavior better?
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 RetainPreviousVersions
and Default
only apply to the version on which they are attached. I find this confusing because we have the concept of a Version referring to both a version of a product and a version of a version. I understand it's up to the user to attach semantics to these terms, but I'd prefer to see versions of a product called variants (or something better). @rix0rrr thoughts?
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.
Are you not trying to say something like:
const product = new CloudFormationProduct(this, 'Product', {
productVersions: [
{
productVersionName: "v1",
cloudFormationTemplate: CloudFormationTemplate.fromHistoricalSnapshot('v1'),
},
],
});
?
The fact that it uses fromHistoricalSnapshot
implies RETAIN_PREVIOUS_VERSIONS
(because what else would you do).
{ | ||
productVersionName: "v2", | ||
cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new S3BucketProduct(this, 'S3BucketProduct')), | ||
versioningStrategy: servicecatalog.VersioningStrategy.RETAIN_PREVIOUS_VERSIONS |
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.
this is a bit confusing. Why can the versioningStrategy
be specified for each member of productVersions
, instead of being set for the product? Setting v2
to have RETAIN_PREVIOUS_VERSIONS
, and having v1
be DEFAULT
, is confusing because RETAIN_PREVIOUS_VERSIONS
implies that v1
will be retained, and I don't think that's the intent here. This would make more sense to me if the versioningStrategy
was specified for the entire product, and not for each version.
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.
versioningStrategy
only applies to fromProductStack
looks like... so shouldn't it be part of the fromProductStack
constructor?
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.
@rix0rrr is correct. The versioningStrategy
currently only applies to ProductStacks.
The reason we cannot set it directly on the product is because we may have two separate productVersions for a single product.
For example a product may have:
v1_blue
and v1_green
, which can both be updated to a v2
version. We could have a strategy for the entire product, but that will enforce that strategy for each version. Making it tied to each version allows the user to have one use DEFAULT
and another use RETAIN_PREVIOUS_VERSIONS
Setting a versioningStrategy on v1
would not do anything since it using fromProductStackContext
. Think of the versioningStrategy as applying to that specific ProductStack rather than to a specific version.
The reason we choose not to make it apart of the fromProductStack
constructor is because we want it leave it open to have future versioningStrategies that don't apply to just ProductStack
. For example, we could add the RETAIN_PREVIOUS_VERSIONS
or another strategy to fromAsset
if there is a useful use case for it. However, I did originally have it in the constructor, but was suggested by Adam (left CDK team) to move it.
I can see how it is confusing, so if you think we should have it tied only to the fromProductStack
constructor we can change this and we should still be able to make it more flexible in the future by adding an optional field to the other constructors.
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'm not sure I agree versioningStrategy
applies only to fromProductStack()
. A Product Version from an Asset (created using fromAsset()
), or referencing some file uploaded somewhere (created using fromUrl()
) can be changed in the same way one created from ProductStack
can be, and so you might want to prevent those changes from being deployed the same way you want to prevent changes to ProductStack
from being deployed.
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.
Right we could extend the same functionality to fromAsset()
or fromUrl()
, however this PR itself is focusing on ProductStack since that was the issue a customer brought up with us.
With the other two the user can version their templates themselves either with additoinal local asset or from ie. git (and they pass in the additional urls).
The only way to version fromProductStack()
is to create an entirely new ProductStack in code, which doesn't make sense the more versions we have and could result in a lot of duplicate code. We are trying to make it easier for them to do this by automatically saving their generated templates instead and making it easy to reference those templates.
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.
Not sure I understand. If you store the URL, you would also pass the URL for the Version for that Product Version.
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 mean when we try to read it from our local context. The user would only be passing in the productVersionName they would like to retrieve. We would need to determine if this productVersionName was stored from a productStack or from a URL (url would be stored in file instead of actual contents) as they would take different logic.
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.
No one is asking us to retain Assets or retain URLs, and it seems like its will add more complexity, edge cases and possibly unintended behavior for the user.
I think its better to focus on the customer ask here and only retain Product Stacks. ChangeVersioningStrategy
to RetentionStrategy
as suggested by Adam and scope it down to a field in fromProductStack()
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 mean when we try to read it from our local context. The user would only be passing in the productVersionName they would like to retrieve. We would need to determine if this productVersionName was stored from a productStack or from a URL (url would be stored in file instead of actual contents) as they would take different logic.
Of course, but that should be easy, right? We are the ones who control the format of these files, and the logic that both writes, and reads them. So I don't think there should be any issues with implementing 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 we are trying to design in too much flexibility here. All the customer was asking for is a way for them to update their ProductStack
and make sure that their previously deployed copies of that ProductStack
don't get overwritten.
Since this whole "retain versions" feature is not implemented in the Service, but layered on top by CDK, we're naturally going to be limited in what we can do. Whether assets should be able to be snapshotted can be debated, I definitely do not intend to be able to snapshot arbitrary URLs. If it was necessary to snapshot arbitrary URLs, then users can wrap a script that downloads to a file and proceeds to use assets, or something else.
But honestly, I think a new API makes more sense, rather than trying to shoehorn this behavior into the existing API with flags.
* The strategy to use for a ProductStack deployment. | ||
* Determines how a productVersion is saved and deployed. | ||
*/ |
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.
minor style comment, but can these opening comments (/**
and */
) line up?
/** | ||
* Default Strategy for ProductStack deployment. | ||
* This strategy will overwrite existing versions when deployed. |
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 don't understand these comments if a VersioningStrategy
is defined for each version. This comment makes me think that this is defined per product; if one version has a retain strategy, and another version has the overwrite strategy, how do we decide which versions are retained? Are there versions of versions?
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.
There can be versions of a versions.
It is more like one ProductStack is using a retain strategy, and another ProductStack has the overwrite strategy. Both ProductStacks are "versions" of the product.
I know its a bit confusing, but there currently is not implementation of versioning in Service Catalog and ProductStacks are a CDK-only concept. So we are attempting to kind of version ProductStacks in CDK code, but once its deployed to Service Catalog, each template is just a version of the Product.
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 see. Effectively we have a product version A, B, C and then A, B and C can each have versions. I'd prefer we call product versions something other than versions, if we're also going to call the versions of versions "versions".
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 the mistake here is the VersioningStrategy
name, as it re-uses the term "version" to mean something else than does it now.
How about calling it RetentionStrategy
instead, with two values: OVERRIDE
(the default), and RETAIN
?
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.
If we do this then let's call it OVERWRITE
. But I favor a different type of API in general.
} | ||
break; | ||
case TemplateType.PRODUCT_STACK_CONTEXT: | ||
const templateFileKey = `${this.productPathUniqueId}.${template.productVersionDetails?.productStackId}.${productVersion.productVersionName}.product.template.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.
what happens if template.productVersionDetails
is undefined
here?
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't be undefined since we determined that that this is the case TemplateType is PRODUCT_STACK_CONTEXT
.
In the constructor for CloudFormationProductStackContextTemplate
we create the productVersionDetails, so it will always be defined.
class CloudFormationProductStackContextTemplate extends CloudFormationTemplate {
private readonly productVersionDetails: ProductVersionDetails;
/**
* @param baseProductStackId The id of the product stack where the version was deployed from.
*/
constructor(public readonly baseProductStackId: string) {
super();
this.productVersionDetails = new ProductVersionDetails();
this.productVersionDetails.productStackId = this.baseProductStackId;
}
}
The reason there's an optional check here is because productVersionDetails
can be optional as in its not required for other TemplateType.
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.
ahh okay, this makes sense. Thanks for clarifying. @rix0rrr should we use instanceof
here instead? That would remove the ?
operator here.
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.
Never instanceof
! If you see instanceof
it's a bug.
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.
@rix0rrr is that because inheritance should be used instead? Could you elaborate?
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 because instanceof
for type testing of custom classes is unreliable. It's too big of a topic to go in here, but we can talk about it in person if you want.
productVersions.push( | ||
{ |
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.
productVersions.push( | |
{ | |
productVersions.push({ |
); | ||
} |
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.
); | |
} | |
}); |
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.
ESLint is not happy and suggesting the original braces here.
Also the suggestion is moving the bracket before the parenthesis which doesn't make sense.
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.
ahh woops that was the wrong pair
@@ -189,6 +191,66 @@ describe('Product', () => { | |||
expect(assembly.stacks[0].assets.length).toBe(1); | |||
}), | |||
|
|||
test('product test from product stack with versioning strategy retain', () => { |
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 need a test for the default
case as well.
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.
This is a tough feature, and deviates A LOT from how CDK normally works.
Normally the service itself keeps product version histories (Lambda, CloudFormation) so CDK can just reason about the "current" state of the world and have the service keep track of changes.
I would urge you to reconsider implementing this in the service: not sure CDK is the place for it. If you want to push through, spend some more effort on naming and explaining the feature very well to users who haven't gone through the same design process you have and are intimately familiar with every idiosyncracity. Again: this is very different from how CDK usually behaves: be cognizant of that while explaining to users who are used to the usual way.
@@ -184,6 +184,48 @@ const product = new servicecatalog.CloudFormationProduct(this, 'Product', { | |||
}); | |||
``` | |||
|
|||
We can also define a `versioningStrategy` for your `ProductStack` deployment. |
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.
Lead with the use case instead of the mechanism. WHY would a user want to read this section? Also explain how it works in general, because this has implications on the user's workflow, not just on the API they need to call. Add a section header.
I'd recommend something like:
## Immutable products
The default behavior of Service Catalog is to overwrite each product version upon deployment.
If instead you want to never overwrite existing versions, but only add new versions... (etc)
...all product templates will be written to disk, so that they will still be available in the future
as the definition of the `ProductStack` subclass changes over time. **It is very important** that you commit these
old versions to source control...
We can also define a `versioningStrategy` for your `ProductStack` deployment. | ||
With the `Default` strategy, a new `ProductStack` is needed for each `productVersion` | ||
and each `productVersion` will get overwritten with the latest changes to your `ProductStack`. | ||
With the `RetainPreviousVersions` strategy, a local template will be retained for each `ProductStack` deployed. | ||
Subsequently, any `ProductStack` changes will not overwrite the current version. | ||
A new `productVersionName` must be specified in order for `ProductStack` changes to be deployed. | ||
When the `ProductStack` is updated with a new version, the previous version can still be deployed. | ||
The template of previously deployed `ProductStack` can be referenced using `fromProductStackContext` | ||
and passing the corresponding `ProductStack` id. |
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.
This might be a sign of poor naming.
With the `Default` strategy, a new `ProductStack` is needed for each `productVersion` | ||
and each `productVersion` will get overwritten with the latest changes to your `ProductStack`. | ||
With the `RetainPreviousVersions` strategy, a local template will be retained for each `ProductStack` deployed. | ||
Subsequently, any `ProductStack` changes will not overwrite the current version. |
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.
What does the workflow look while you are developing/testing the latest ProductStack
?
- Are you supposed to keep the most recent version at
Default
while iterating on it, and then flip it toRetainPreviousVersions
as you are done? - From the code I'm reading, you must then remember to
synth
one final time and then commit. - And then, for the next version, change it to a
fromTemplate
, add a newfromProductStack
withRetainPreviousVersions
and repeat?
If that's what you are thinking, best to explain that here as well, because I'm not sure that's obvious to readers.
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.
(As you are explaining the process to users, I hope you will then internally go "hmm that seems like a sharp edge that I can shave off by redesigning/guard against with validation")
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 workflow would ideally have user stay with Default
or stay with RetainPreviousVersions
the whole way and not be constantly switching between the strategies.
If they are using RetainPreviousVersions
. They would just update the version on their current fromProductStack ie. from
v1to
v2.
Then if they want the previous version to be deployed as well, they would have to add another version with fromProductStackContext
The end result would be the same as you explained, but I guess the workflow for the user would be to update their "current" version, then add any "previous" versions if they choose to, rather than change their "current" to use fromProductStackContext
then add a new version.
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 you are making the assumption that a user gets their new version of the ProductStack
exactly right on the first attempt. Remember that if we have RetainPreviousVersions
enabled, what happens on synth
is:
- If no snapshot of the stack exists, we create it
- If a snapshot already exists, the current stack definition must be exactly equal to the snapshot
That means:
- If a user changes the product version name FIRST before changing the
ProductStack
and (accidentally or to check something) runcdk synth
, their stack definition is now locked in. They can't change it anymore! (And they haven't even begun to make their changes yet)- If a user is using
cdk watch
, synthing is done continuously on their behalf. They may be synthing (and locking in their template) without even consciously taking an action.
- If a user is using
- Even if they made the changes to the
ProductStack
before changing the version, and synth it, and potentially evendeploy
it to their developer account... their changes are now ALSO locked in. Iteration is effectively impossible, hope they got it right on the first try!
That means that in a realistic development workflow, people will have to switch back and forth between those versioning strategies to iterate.
💡 Here's a tip that will last you the rest of your career: Blog post driven development. Look at the AWS Blog and read a couple of posts. Notice how they all take the customer by the hand and say: "to use this feature, you first do X, then Y, then Z", and they don't skip any steps or handwave. For every feature you design henceforth, try to do the same. Pretend you are writing an AWS Blog Post, write in a friendly voice to the customer that has a particular problem or use case, telling them how awesome your feature is and that they "only" need to do X, Y and Z in that order (and don't skip anything). This will force you to be explicit about certain things you are probably handwaving away right now, and potentially cause you to scratch your chin and internally go "oh that isn't as easy as I thought after all".
{ | ||
productVersionName: "v2", | ||
cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new S3BucketProduct(this, 'S3BucketProduct')), | ||
versioningStrategy: servicecatalog.VersioningStrategy.RETAIN_PREVIOUS_VERSIONS |
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.
versioningStrategy
only applies to fromProductStack
looks like... so shouldn't it be part of the fromProductStack
constructor?
}, | ||
{ | ||
productVersionName: "v1", | ||
cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStackContext('S3BucketProduct'), |
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.
Why is this called Context
? This is confusing because it's not like CDK context, plus context is a vague term.
Why not fromProductStackSnapshot
?
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 can change this.
/** | ||
* Constant for the context directory to store retained ProductStack templates. | ||
*/ | ||
export const PRODUCT_STACK_CONTEXT_DIRECTORY = 'product-stack-context'; |
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.
export const PRODUCT_STACK_CONTEXT_DIRECTORY = 'product-stack-context'; | |
export const PRODUCT_STACK_CONTEXT_DIRECTORY = 'product-stack-history'; |
?
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.
If we are changing it to fromProductStackSnapshot
. Shouldn't this be product-stack-snapshots
as well?
"This is a tough feature, and deviates A LOT from how CDK normally works. Normally the service itself keeps product version histories (Lambda, CloudFormation) so CDK can just reason about the "current" state of the world and have the service keep track of changes. I would urge you to reconsider implementing this in the service: not sure CDK is the place for it. If you want to push through, spend some more effort on naming and explaining the feature very well to users who haven't gone through the same design process you have and are intimately familiar with every idiosyncracity. Again: this is very different from how CDK usually behaves: be cognizant of that while explaining to users who are used to the usual way." @rix0rrr Sorry I should of included this design document I worked on with Adam Ruka that might help explain a bit more on the customer ask, motivations and some of the design choices we made: The reason we decided to use |
|
I see your point. This would depend on how Versioning is implemented in Service Catalog but it likely can be integrated with ProductStack. This is probably a big effort on Service Catalog side and would take time. Implementing it on CDK would be a quick win and would still allow the additional feature of disabling deployment from automatically overwriting of existing versions. To add on to this. It still is a uniquely CDK issue as in Service Catalog versions are not overwritten at all. You can only just keep adding new versions or delete old versions manually. However in CDK, if we make code changes and deploy. The current state reflected in CDK will end up overwriting the current state of versions in Service Catalog. |
Is it possible that the versioning in service catalog will break the versioning in CDK? |
|
}); | ||
}).toThrowError('Template MyProduct.ProductStack.v2.product.template.json cannot be found in product stack context'); | ||
}), | ||
|
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 we're missing a test here for when versioningStrategy
is RETAIN
, but the ProductStack
code chages between the time when the template was saved on disk, and then synthesized again? (That should result in an exception being thrown)
Pull request has been modified.
Since there was no update on comments, I went ahead with a commit that updated naming and documentation, moved the renamed |
d2e2af0
to
90b18de
Compare
It's up to us to decide what a good workflow is. We are developers, we can put ourselves in the shoes of another developer. A developer workflow typically looks like this:
(Obviously there will probably be 2 such loops: one with the code on your developer desktop, and then another one with the code going through CI and running through the test environments). Here's my take:
That seems to imply to me: yes we deploy the current version from the |
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.
Thanks for rolling with my proposal! I like this a lot better. I think we can move more logic into the History
class though, and keep the other ones simple.
public static fromProductStackHistory(productStack: ProductStack, locked: boolean, directory?: string ): CloudFormationTemplate { | ||
return new CloudFormationProductStackTemplate(productStack, locked, directory); | ||
} | ||
|
||
/** | ||
* Creates a product from a previously deployed product stack snapshot. | ||
*/ | ||
public static fromProductStackSnapshot(productStack: ProductStack, directory?: string ): CloudFormationTemplate { | ||
return new CloudFormationProductStackSnapshotTemplate(productStack, directory); | ||
} |
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 both of these are not necessary anymore, as long as productStackHistory.versionFromSnapshot('v1')
can return a subclass of CloudFormationTemplate
.
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.
Okay I think we should be able to do that
const productStackHistory = new servicecatalog.ProductStackHistory(this, 'ProductStackHistory', { | ||
productStack: new S3BucketProduct(this, 'S3BucketProduct'), | ||
currentVersionName: 'v2', | ||
locked: true |
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.
After some consideration, I think currentVersionLocked
would be a better name? WDYT?
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.
Sounds good we can change it to be more clear.
* Creates a product with the resources defined in the given product stack and retains all previously deployed product stack versions. | ||
*/ | ||
public static fromProductStackHistory(productStack: ProductStack, locked: boolean, directory?: string ): CloudFormationTemplate { | ||
return new CloudFormationProductStackTemplate(productStack, locked, directory); |
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.
locked
and directory
don't have to go in here. Either the current ProductStack
is valid (in which case it's not necessary here), or it's not (in which case the build will fail anyway).
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 the issue is we use the locked and directory to determine if we need to write to snapshot and determine if we should deploy from snapshot as an asset or deploy it from our productStack.
If we are able to move the write logic (and possibly more logic) to the product-stack-history we might be able to remove this.
constructor(public readonly productStack: ProductStack, | ||
public readonly locked?: boolean, public readonly directory?: string) { | ||
super(); |
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.
This class doesn't have to change.
productVersionDetails.locked = this.locked; | ||
productVersionDetails.directory = this.directory; |
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'm only now noticing you are doing this with mutability as intent.
Relying on side effects is a bad idea: it will make the data flow and dependencies between pieces of code VERY hard to reason about. Make as much as you can readonly
please, and don't do this.
A better way to do this would have been below:
public bind(_scope: Construct): CloudFormationTemplateConfig {
const productDetails = this.productStack._getProductVersionDetails();
return {
httpUrl: this.productStack._getTemplateUrl(),
productVersionDetails: {
...productDetails,
locked: this.locked,
directory: this.directory,
},
templateType: TemplateType.PRODUCT_STACK,
};
}
That's a general advice. In this particular case, don't modify this class at all.
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'll remove as many of these as possible. There is one case of productPathUniqueId
that is only available when the Product is created and this value needs to be passed to ProductStack
or ProductStackHistory
to create a unique key for the file. The only link between the two is through cloudFormationTemplate
, so I am unable to remove it in that instance (unless you have any ideas).
* ProductStackSnapshotTemplate | ||
*/ | ||
PRODUCT_STACK_SNAPSHOT = 'ProductStackSnapshotTemplate' |
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 this still relevant? Isn't the snapshot template ultimately the same as an ASSET template?
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.
Yes, there is a bit more logic in the SNAP_STACK_SNAPSHOT
like a more detailed error when file is not found. Also currently ASSET
(actual asset creation) is implemented in the bind method CloudFormationTemplate (path is passed by user).
I need to create the asset in the Product
as we don't have access to the details to form the path in CloudFormationTemplate
.
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.
Yes, there is a bit more logic in the SNAP_STACK_SNAPSHOT like a more detailed error when file is not found.
What I'm saying is that if you do:
history.snapshotVersion('v1')
Then the implementation can look like:
class History {
public snapshotVersion(version: string) {
const snapshotFile = this.fileForVersion(version);
if (!fileExists(snapshotFile)) {
throw new Error('oh no you should have used this differently');
}
return Template.fromAsset(snapshotFile);
}
}
The error message becomes part of the History
class and can be as targeted and explicit as we need.
packages/@aws-cdk/aws-servicecatalog/lib/product-stack-history.ts
Outdated
Show resolved
Hide resolved
*/ | ||
readonly productStack: ProductStack; | ||
/** |
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.
Empty lines between the property and the start of the next comment block please. Look at other code and follow the style there.
*/ | ||
export interface ProductStackHistoryProps { | ||
/** | ||
* The base Product Stack. |
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 base" ?
What does "base" mean? Please talk in terms that customer using your API will care and think about. Something like:
The ProductStack whose history will be snapshotted to files.
Or something to that effect.
if (this._productVersionDetails.locked !== undefined) { | ||
this.writeTemplateToSnapshot(cfn); | ||
} |
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.
Why doesn't the History
class write and validate the snapshots?
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 it could, it would involve giving the productStack access to it's History
class and calling a method from it, I'll try to implement it.
} | ||
break; | ||
case TemplateType.PRODUCT_STACK_CONTEXT: | ||
const templateFileKey = `${this.productPathUniqueId}.${template.productVersionDetails?.productStackId}.${productVersion.productVersionName}.product.template.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.
@rix0rrr is that because inheritance should be used instead? Could you elaborate?
packages/@aws-cdk/aws-servicecatalog/lib/product-stack-history.ts
Outdated
Show resolved
Hide resolved
Pull request has been modified.
@rix0rrr |
f13f4d4
to
e04d282
Compare
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.
Again, very close! I'd prefer the change to be additive as much as possible.
Rather than changing and exposing implementation details of existing classes, I'd prefer to introduce new classes that conform to existing public contracts. Potentially it's okay to extend the existing contracts if new features need to be added, and it's definitely possible that we may need to change something in existing classes... but it shouldn't be the default option.
/** | ||
* Template from a previously deployed product stack. | ||
*/ | ||
export class CloudFormationProductStackSnapshotTemplate extends CloudFormationTemplate { |
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.
Please don't expose this class publicly. I feel it can go into product-stack-history.ts
as a private class.
/** | ||
* Fetch the product version details. | ||
* | ||
* @internal | ||
*/ | ||
public _getProductVersionDetails(): ProductVersionDetails { | ||
return this._productVersionDetails; | ||
} | ||
|
||
/** | ||
* Set the parent product stack history | ||
* | ||
* @internal | ||
*/ | ||
public _setParentProductStackHistory(parentProductStackHistory: ProductStackHistory) { | ||
return this._parentProductStackHistory = parentProductStackHistory; | ||
} | ||
|
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.
Please try to find a way to implement this feature without changing ProductStack
(or convince me it's not possible).
} | ||
break; | ||
case TemplateType.PRODUCT_STACK_CONTEXT: | ||
const templateFileKey = `${this.productPathUniqueId}.${template.productVersionDetails?.productStackId}.${productVersion.productVersionName}.product.template.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.
It's because instanceof
for type testing of custom classes is unreliable. It's too big of a topic to go in here, but we can talk about it in person if you want.
const templateFilePath = path.join(productStackSnapshotDirectory, templateFileKey); | ||
if (!fs.existsSync(templateFilePath)) { | ||
throw new Error(`Template ${templateFileKey} cannot be found in ${productStackSnapshotDirectory}`); | ||
} | ||
httpUrl = new s3_assets.Asset(this, `Template${hashValues(templateFileKey)}`, { | ||
path: templateFilePath, | ||
}).httpUrl; |
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.
This is tying the contents of the ProductStack
class a little too much to where it's being used, in my mind.
This code could go into the bind()
of CloudFormationProductStackSnapshotTemplate
, right? Wouldn't that behave exactly the same, except we don't need to change this class so much?
What I'm looking for is roughly that the output of bind()
is ~what goes into the productVersions
list (modulo some information that may come from this construct or some simple transformation).
if (this._parentProductStackHistory) { | ||
this._parentProductStackHistory._writeTemplateToSnapshot(cfn); | ||
} |
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 this can be replaced with:
class ProductStackHistory {
constructor(...) {
Aspects.of(this).addAspect({
visit: (c) => {
if (c === this) {
this.writeTemplateToSnapshot(props.currentProductStack);
}
}
});
}
}
And that would get rid of most of the poking at the innards of existing classes.
@rix0rrr There are now no changes to |
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.
👍
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). |
@wanjacki |
@padaszewski |
@wanjacki |
@padaszewski Yes, you should be able to downgrade using one stack (if I understand your scenario correctly). |
@wanjacki And about the naming conventions: the requirement is hat we have very specific and static bucket names in a certain account. For now this cannot be created in a dynamic manner. Thats why two stacks would fail to deploy. |
@wanjacki |
@padaszewski |
Adding enhancement to ProductStack to allow the specification of a VersioningStrategy.
VersioningStrategy
RetainPreviousVersions
added to save previously deployed ProductStacks templates in a local context directory. These productVersions can then be easily be deployed using the stored templates.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license