Skip to content

fix: stack definitions for refactor are incorrect when matching resources are different #555

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

Closed
wants to merge 4 commits into from

Conversation

otaviomacedo
Copy link
Contributor

In most cases, when a resource exists both in the deployed stacks and the local stacks, they will be identical (up to references). But if a resource has a physical ID defined, we take use it to identify the resource, instead of computing a hash from the resource's contents. This allows the toolkit to detect correspondence between these resources even if users changed some properties while refactoring.

When it comes to generating the stacks to send to the refactor API, we need to take this into account. This means taking the deployed resource as the source of truth, and only updating its references, taking them from the local stacks. To do this, we traverse the deployed resource, and every time we find a reference (either within the same stack or cross-stack), transform it to the corresponding local one (also either within- or cross-stack), using the set of resource mappings we already have.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

…different

In most cases, when a resource exists both in the deployed stacks and the local stacks, they will be identical (up to references). But if a resource has a physical ID defined, we take use it to identify the resource, instead of computing a hash from the resource's contents. This allows the toolkit to detect correspondence between these resources even if users changed some properties while refactoring.

When it comes to generating the stacks to send to the refactor API, we need to take this into account. This means taking the deployed resource as the source of truth, and only updating its references, taking them from the local stacks. To do this, we traverse the deployed resource, and every time we find a reference (either within the same stack or cross-stack), transform it to the corresponding local one (also either within- or cross-stack), using the set of resource mappings we already have.
@aws-cdk-automation aws-cdk-automation requested a review from a team May 29, 2025 13:04
@github-actions github-actions bot added the p2 label May 29, 2025
@codecov-commenter
Copy link

codecov-commenter commented May 29, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 79.01%. Comparing base (dda5e82) to head (380948a).
Report is 17 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #555      +/-   ##
==========================================
- Coverage   79.09%   79.01%   -0.08%     
==========================================
  Files          47       47              
  Lines        7074     7086      +12     
  Branches      790      790              
==========================================
+ Hits         5595     5599       +4     
- Misses       1460     1468       +8     
  Partials       19       19              
Flag Coverage Δ
suite.unit 79.01% <ø> (-0.08%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

import type * as cxapi from '@aws-cdk/cx-api';
import { ToolkitError } from '../../toolkit/toolkit-error';

export interface CloudFormationResource {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not intended to be complete, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. Just enough for what we need here.

return new Ref(stackName, value.Ref);
}

constructor(public readonly stackName: string, public readonly logicalResourceId: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You might also consider modeling it as a reference to an object that has a logicalID as an attribute.

Then the rename operation is as simple as changing the logicalID field of the resource, and re-rendering. You then do not need the map and replace operations anymore.

image

}
const att = value['Fn::GetAtt'];
if (typeof att === 'string') {
const [id, attributeName] = att.split('.');
Copy link
Contributor

Choose a reason for hiding this comment

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

I think attributeName can contain . as well, so you need to make sure to max split once.

constructor(private readonly stackName: string, private readonly logicalIds: string[]) {
}

public equals(other: ResourceReference): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this what we need to say? That the entire array is the same?

other instanceof DependsOn &&
this.stackName === other.stackName &&
other.logicalIds.length === this.logicalIds.length &&
this.logicalIds.every((id) => other.logicalIds.includes(id))
Copy link
Contributor

Choose a reason for hiding this comment

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

But we do allow reordering?


const newInputString = this.inputString.replace(/\${([a-zA-Z0-9_.]+)}/g, (_: any, varName: string) => {
if (varName.includes('.')) {
const [logicalId, attr] = varName.split('.');
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing

}
}

function replaceFirst(mappings: ResourceMapping[], stackName: string, logicalId: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

This name makes me think there can be more than one replacement; but that seems like it would be illegal anyway?

return resolveLocalReference(fnSub);
}

if ('DependsOn' in value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

DependsOn only has a special meaning if it is at the top-level of a resource definition, not anywhere in the template. It might not be happening (yet), but I still wouldn't want to rely on it never happening.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, apparently UpdatePolicy can contain references as well. But since you're processing the entire resource's JSON object that should all automatically work already.

} else {
// We've got a deployed resource matching a local resource.
// The final template should contain the deployed resource, but with local references.
localTemplates[stackName].Resources![logicalResourceId] = updateReferences(
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't forget references in the Output block.

.Resources?.[location.logicalResourceId];
const deployedResource = deployedStacks.find((s) => s.stackName === location.stackName)?.template.Resources?.[
location.logicalResourceId
];

if (deployedResource == null) {
delete localTemplates[stackName].Resources?.[logicalResourceId];
Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose we just assume/assert there will be no references to this resource when we are done with the loop, eh? Worth a comment somewhere?

BTW, thinking about this some more:

  • We now take the "new" template, removing all "new" resources. So effectively, we will be left with the "old" template + modifications.
  • But, any modifications aren't allowed and will lead to a deployment error.
  • So if the deployment were to succeed, we would have produced the "old" template, given the new template.

If that's true, why don't we take the "old" template and apply the renames there?


// After copying (an updated version of) the deployed resource,
// we may end up with dangling dependencies. Sanitize them.
sanitizeDependencies(localTemplates[stackName]);
Copy link
Contributor

Choose a reason for hiding this comment

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

How is it possible that we have problems with dangling DependsOns, but we don't have any problems with dangling Refs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If a resource that was pointed at via a Ref gets moved to another stack, that Ref will be converted into a Fn::ImportValue, which doesn't happen for DependsOn.

auto-merge was automatically disabled June 9, 2025 11:07

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants