-
Notifications
You must be signed in to change notification settings - Fork 89
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
RFC 201: Refactoring support #455
Conversation
* Moving a construct from the current scope into a deeper scope, or vice versa. | ||
* Moving a construct between two deeper scopes. | ||
|
||
It is not possible to move a construct into a bigger scope, or reference a construct from a bigger scope. |
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.
Any particular reason this is the case?
super(scope, id, props); | ||
|
||
new MyConstruct(this, 'MyConstruct'); | ||
this.node.refactor(this, 'MyBucket', 'MyConstruct/MyBucket'); |
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.
API thoughts:
- The term "refactor" doesn't necessarily mean "rename". Perhaps be a bit more explicit about the name of this method.
- It might be useful to also support directly referencing the target object (less potential for mistakes based on string matching)
- Why do we need
this
as the first argument if we are already in thenode
?
this.node.moveToPath("MyBucket", "MyConstruct/MyBucket");
this.node.moveTo("MyBucket", myConstruct.myBucket);
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 refactorings in the other direction could also be useful -- for example, if I have an app structured like this, and I wanted to move ChildA to be part of the root (while keeping its old ID at synthesis-time).
- root
- Parent1
- ChildA
- ChildB
side note: Reading the RFC, I wondered if we would gain any expressive power in constructs by taking a page from the world of filesystems and introducing a symlink-like concept. This would be a special construct that redirects to some other path in the construct tree, and would be handled specially during construct tree traversal. Something like:
new Link(this, "MyBucket", "MyConstruct/MyBucket");
new Link(this, "MyBucket", myConstruct.myBucket);
new Portal(this, "MyBucket", myConstruct.myBucket);
But I'm not sure if this API as much sense 🤷
- There is not any construct already named `MyBucket`. | ||
- No new construct named `MyBucket` will be created after this call. | ||
|
||
To that end, the `refactor` call will error if the source construct path refers to an existing resource, and drop a new type of construct called `RefactoredNode` in the location of the moved-away resource[1]. That way, users will not be able to create a new construct in its place. |
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.
How do you imagine this error message might look like?
We will add misuse protection. It is important that when a construct is "virtually" moved into the location of another, that that location is not reused by a meaningful construct, as their identifiers would start to collide. In the example given above: | ||
|
||
```ts | ||
this.node.refactor(this, 'MyBucket', 'MyConstruct/MyBucket'); |
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 the API is a method call and not "new"-ing something, it reads to me like an operation, not a declaration, so as a user I'd expect I could call this multiple times. Would something like this be possible?
this.node.refactor(this, 'MyBucket', 'MyConstructV1/MyBucket');
this.node.refactor(this, 'MyConstructV1/MyBucket', 'MyConstructV2/MyBucket');
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, thank you so much for thinking about this problem. As someone who owns several large scale application managed by the CDK, I can attest to how painful hitting this can be, especially how risky it is, as sometimes, the only way out (beside reverting the code) is to delete resources from the console 😱.
The CDK default behavior for allocation logical ids is unsafe, its a good getting starting experience, but it comes at the expense of long term maintenance pain. For that reason I think the solution should be on the getting started path, and not after the user hits this sharp edge (thinking of an emergency deployment, not so fun realizing you cant deploy your app). Having said that, I realize this might not be possible.
This RFC propose a solution in the construct
level, but the problem mostly impacts the AWS CDK users, due to how CloudFormation handles logical ids, so maybe it will be easier to solve it on the Stack level.
The "easiest" solution from the framework perspective would be to simply use the user provided id, and let them deal with the consequence of collisions. In case of a collisions, a detailed error message will be thrown, and the user will have to fix it. I agree that this might not be a great getting started experience, but this has the advantage of letting the user make a decision when they build a production app. Working backward from a (imaginary) CDK best practices section:
Things to consider when building a production CDK application
By default the CDK will allocate logical ids based on the construct path in the construct tree. This means that if you move your construct around the code, it might result in a change to its logical id, that will trigger a resource replacement, if you use "named resources", e.g.
BucketName
, the deployment will fail when trying to create a new resource. If this is indeed an issue for you, you can opt out of this CDK default behavior by setting the context key:
construct-id:no-default-generation
This will override the default behavior and use the user provided id (the second arg when initializing a construct). Note that this means that you will have to manage the Ids uniqueness yourself.
While not perfect it will allow the user to make a concise decision before deploying a full application.
Also worth noting that there is support for refactoring today, by using the Default
id on the added layer, from constructs
/**
* Resources with this ID are complete hidden from the logical ID calculation.
*/
const HIDDEN_ID = 'Default';
/**
* Calculates the construct uid based on path components.
*
* Components named `Default` (case sensitive) are excluded from uid calculation
* to allow tree refactorings.
*
I was just wondering about this and tried to ask in discussions (aws/aws-cdk#22016) My suggestion was to avoid using construct classes (avoid inheriting Construct) and instead arrange your code with regular classes which takes the scope of the stack/parent construct and use this as the scope for all child resources. Pros:
Cons:
Let me expand on this a bit - i'm not suggesting to have everything as a flat stack, but do something which i'll call "top level domain structuring" in which i'll create top level constructs in which it is highly unlikely for things to move between such top level constructs. and in them, I will avoid using construct, lose the internal hierarchy but be able to refactor the code without issues. What do you think about this approach? do you think it goes against the "spirit" of cdk/cloudformation, or does it seem like a legitimate approach? Thank you. |
How different is this from renaming logical ids? Is the difference that relocating can happen at some intermediate construct level therefore might save multiple logical Id renames? |
Was this ever merged? |
This is a request for comments about refactoring support. See #201 for
additional details.
APIs are signed off by @RomainMuller.
Rendered Link
By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache-2.0 license