Skip to content

Commit e44c2c4

Browse files
authored
fix(core): logicalId is consumed prior to being overridden (#20560)
When using `Stack.exportValue` to manually create a CloudFormation export, the logicalId of the referenced resource is used to generate the logicalId of the `CfnExport`. Because `exportValue` creates a `CfnExport` _and_ returns an `importValue` it needs to _resolve_ the logicalId at call time. If the user later overrides the logicalId of the referenced resource, that override is reflected in the export/import that was created earlier. There doesn't seem to be a way to solve this without incurring a breaking change so this PR attempts to smooth a rough edge by "locking" the `logicalId` when `exportValue` is called. If the user attempts to override the id _after_ that point, an error message will be thrown closes #14335 ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent f19ecef commit e44c2c4

File tree

3 files changed

+93
-1
lines changed

3 files changed

+93
-1
lines changed

packages/@aws-cdk/core/lib/cfn-element.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ export abstract class CfnElement extends CoreConstruct {
4747
*/
4848
private _logicalIdOverride?: string;
4949

50+
/**
51+
* If the logicalId is locked then it can no longer be overridden.
52+
* This is needed for cases where the logicalId is consumed prior to synthesis
53+
* (i.e. Stack.exportValue).
54+
*/
55+
private _logicalIdLocked?: boolean;
56+
57+
5058
/**
5159
* Creates an entity and binds it to a tree.
5260
* Note that the root of the tree must be a Stack object (not just any Root).
@@ -75,7 +83,37 @@ export abstract class CfnElement extends CoreConstruct {
7583
* @param newLogicalId The new logical ID to use for this stack element.
7684
*/
7785
public overrideLogicalId(newLogicalId: string) {
78-
this._logicalIdOverride = newLogicalId;
86+
if (this._logicalIdLocked) {
87+
throw new Error(`The logicalId for resource at path ${Node.of(this).path} has been locked and cannot be overridden\n` +
88+
'Make sure you are calling "overrideLogicalId" before Stack.exportValue');
89+
} else {
90+
this._logicalIdOverride = newLogicalId;
91+
}
92+
}
93+
94+
/**
95+
* Lock the logicalId of the element and do not allow
96+
* any updates (e.g. via overrideLogicalId)
97+
*
98+
* This is needed in cases where you are consuming the LogicalID
99+
* of an element prior to synthesis and you need to not allow future
100+
* changes to the id since doing so would cause the value you just
101+
* consumed to differ from the synth time value of the logicalId.
102+
*
103+
* For example:
104+
*
105+
* const bucket = new Bucket(stack, 'Bucket');
106+
* stack.exportValue(bucket.bucketArn) <--- consuming the logicalId
107+
* bucket.overrideLogicalId('NewLogicalId') <--- updating logicalId
108+
*
109+
* You should most likely never need to use this method, and if
110+
* you are implementing a feature that requires this, make sure
111+
* you actually require it.
112+
*
113+
* @internal
114+
*/
115+
public _lockLogicalId(): void {
116+
this._logicalIdLocked = true;
79117
}
80118

81119
/**

packages/@aws-cdk/core/lib/stack.ts

+8
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,14 @@ export class Stack extends CoreConstruct implements ITaggable {
907907
throw new Error('exportValue: either supply \'name\' or make sure to export a resource attribute (like \'bucket.bucketName\')');
908908
}
909909

910+
// if exportValue is being called manually (which is pre onPrepare) then the logicalId
911+
// could potentially be changed by a call to overrideLogicalId. This would cause our Export/Import
912+
// to have an incorrect id. For a better user experience, lock the logicalId and throw an error
913+
// if the user tries to override the id _after_ calling exportValue
914+
if (CfnElement.isCfnElement(resolvable.target)) {
915+
resolvable.target._lockLogicalId();
916+
}
917+
910918
// "teleport" the value here, in case it comes from a nested stack. This will also
911919
// ensure the value is from our own scope.
912920
const exportable = referenceNestedStackValueInParent(resolvable, this);

packages/@aws-cdk/core/test/stack.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,52 @@ describe('stack', () => {
576576
expect(templateA).toEqual(templateM);
577577
});
578578

579+
test('throw error if overrideLogicalId is used and logicalId is locked', () => {
580+
// GIVEN: manual
581+
const appM = new App();
582+
const producerM = new Stack(appM, 'Producer');
583+
const resourceM = new CfnResource(producerM, 'ResourceXXX', { type: 'AWS::Resource' });
584+
producerM.exportValue(resourceM.getAtt('Att'));
585+
586+
// THEN - producers are the same
587+
expect(() => {
588+
resourceM.overrideLogicalId('OVERRIDE_LOGICAL_ID');
589+
}).toThrow(/The logicalId for resource at path Producer\/ResourceXXX has been locked and cannot be overridden/);
590+
});
591+
592+
test('do not throw error if overrideLogicalId is used and logicalId is not locked', () => {
593+
// GIVEN: manual
594+
const appM = new App();
595+
const producerM = new Stack(appM, 'Producer');
596+
const resourceM = new CfnResource(producerM, 'ResourceXXX', { type: 'AWS::Resource' });
597+
598+
// THEN - producers are the same
599+
resourceM.overrideLogicalId('OVERRIDE_LOGICAL_ID');
600+
producerM.exportValue(resourceM.getAtt('Att'));
601+
602+
const template = appM.synth().getStackByName(producerM.stackName).template;
603+
expect(template).toEqual({
604+
Outputs: {
605+
ExportsOutputFnGetAttOVERRIDELOGICALIDAtt2DD28019: {
606+
Export: {
607+
Name: 'Producer:ExportsOutputFnGetAttOVERRIDELOGICALIDAtt2DD28019',
608+
},
609+
Value: {
610+
'Fn::GetAtt': [
611+
'OVERRIDE_LOGICAL_ID',
612+
'Att',
613+
],
614+
},
615+
},
616+
},
617+
Resources: {
618+
OVERRIDE_LOGICAL_ID: {
619+
Type: 'AWS::Resource',
620+
},
621+
},
622+
});
623+
});
624+
579625
test('automatic cross-stack references and manual exports look the same: nested stack edition', () => {
580626
// GIVEN: automatic
581627
const appA = new App();

0 commit comments

Comments
 (0)