Skip to content

Commit 79c58ed

Browse files
fix(core): Add stage prefix to stack name shortening process (#25359)
There is an issue in the stack name generation process where the prefix generated from assembly's stage name is not taken into account when shortening a stack name to meet the requirement of being equal or less than 128 characters longs. This can lead to generating an invalid stack name greater than 128 characters: since the stack name is shortened to 128 characters, when the prefix is added, the limit is exceeded. Current solution: - Adding a feature flag - With the feature flag on, the prefix is processed within the generateUniqueName function. - With the feature flag off, stack name generation is not changed Fixes #23628 NOTE: This PR was previously opened, but it was merged before I was able to add a feature flag, which ended up adding breaking changes and the changes of the PR were rolled back. Old PR: #24443
1 parent a7ae5a4 commit 79c58ed

File tree

6 files changed

+153
-9
lines changed

6 files changed

+153
-9
lines changed

packages/aws-cdk-lib/core/lib/private/unique-resource-name.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ interface MakeUniqueResourceNameOptions {
2525
* @default - none
2626
*/
2727
readonly allowedSpecialCharacters?: string;
28+
29+
/**
30+
* Prefix to be added into the stack name
31+
*
32+
* @default - none
33+
*/
34+
readonly prefix?: string;
2835
}
2936

3037
/**
@@ -49,6 +56,7 @@ const HASH_LEN = 8;
4956
export function makeUniqueResourceName(components: string[], options: MakeUniqueResourceNameOptions) {
5057
const maxLength = options.maxLength ?? 256;
5158
const separator = options.separator ?? '';
59+
const prefix = options.prefix ?? '';
5260
components = components.filter(x => x !== HIDDEN_ID);
5361

5462
if (components.length === 0) {
@@ -59,7 +67,7 @@ export function makeUniqueResourceName(components: string[], options: MakeUnique
5967
// in order to support transparent migration of cloudformation templates to the CDK without the
6068
// need to rename all resources.
6169
if (components.length === 1) {
62-
const topLevelResource = removeNonAllowedSpecialCharacters(components[0], separator, options.allowedSpecialCharacters);
70+
const topLevelResource = prefix + removeNonAllowedSpecialCharacters(components[0], separator, options.allowedSpecialCharacters);
6371

6472
if (topLevelResource.length <= maxLength) {
6573
return topLevelResource;
@@ -68,7 +76,7 @@ export function makeUniqueResourceName(components: string[], options: MakeUnique
6876

6977
// Calculate the hash from the full path, included unresolved tokens so the hash value is always unique
7078
const hash = pathHash(components);
71-
const human = removeDupes(components)
79+
const human = prefix + removeDupes(components)
7280
.filter(pathElement => pathElement !== HIDDEN_FROM_HUMAN_ID)
7381
.map(pathElement => removeNonAllowedSpecialCharacters(pathElement, separator, options.allowedSpecialCharacters))
7482
.filter(pathElement => pathElement)

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

+16-6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { LogicalIDs } from './private/logical-id';
2020
import { resolve } from './private/resolve';
2121
import { makeUniqueId } from './private/uniqueid';
2222
import * as cxschema from '../../cloud-assembly-schema';
23+
import { INCLUDE_PREFIX_IN_UNIQUE_NAME_GENERATION } from '../../cx-api';
2324
import * as cxapi from '../../cx-api';
2425

2526
const STACK_SYMBOL = Symbol.for('@aws-cdk/core.Stack');
@@ -1432,7 +1433,11 @@ export class Stack extends Construct implements ITaggable {
14321433
private generateStackName() {
14331434
const assembly = Stage.of(this);
14341435
const prefix = (assembly && assembly.stageName) ? `${assembly.stageName}-` : '';
1435-
return `${prefix}${this.generateStackId(assembly)}`;
1436+
if (FeatureFlags.of(this).isEnabled(INCLUDE_PREFIX_IN_UNIQUE_NAME_GENERATION)) {
1437+
return `${this.generateStackId(assembly, prefix)}`;
1438+
} else {
1439+
return `${prefix}${this.generateStackId(assembly)}`;
1440+
}
14361441
}
14371442

14381443
/**
@@ -1447,7 +1452,7 @@ export class Stack extends Construct implements ITaggable {
14471452
/**
14481453
* Generate an ID with respect to the given container construct.
14491454
*/
1450-
private generateStackId(container: IConstruct | undefined) {
1455+
private generateStackId(container: IConstruct | undefined, prefix: string='') {
14511456
const rootPath = rootPathTo(this, container);
14521457
const ids = rootPath.map(c => Node.of(c).id);
14531458

@@ -1457,7 +1462,7 @@ export class Stack extends Construct implements ITaggable {
14571462
throw new Error('unexpected: stack id must always be defined');
14581463
}
14591464

1460-
return makeStackName(ids);
1465+
return makeStackName(ids, prefix);
14611466
}
14621467

14631468
private resolveExportedValue(exportedValue: any): ResolvedExport {
@@ -1643,9 +1648,14 @@ export function rootPathTo(construct: IConstruct, ancestor?: IConstruct): IConst
16431648
* has only one component. Otherwise we fall back to the regular "makeUniqueId"
16441649
* behavior.
16451650
*/
1646-
function makeStackName(components: string[]) {
1647-
if (components.length === 1) { return components[0]; }
1648-
return makeUniqueResourceName(components, { maxLength: 128 });
1651+
function makeStackName(components: string[], prefix: string='') {
1652+
if (components.length === 1) {
1653+
const stack_name = prefix + components[0];
1654+
if (stack_name.length <= 128) {
1655+
return stack_name;
1656+
}
1657+
}
1658+
return makeUniqueResourceName(components, { maxLength: 128, prefix: prefix });
16491659
}
16501660

16511661
function getCreateExportsScope(stack: Stack) {

packages/aws-cdk-lib/core/test/stage.test.ts

+62
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,68 @@ describe('stage', () => {
103103
expect(stack.stackName).toEqual('MyStage-MyStack');
104104
});
105105

106+
test('FF include prefix: Prefix and stack names not exceeding 128 characters are not shortened', () => {
107+
// WHEN
108+
const app = new App({
109+
context: {
110+
'@aws-cdk/core:includePrefixInUniqueNameGeneration': true,
111+
},
112+
});
113+
const stage = new Stage(app, 'ShortPrefix');
114+
const stack = new BogusStack(stage, 'Short-Stack-Name');
115+
116+
// THEN
117+
expect(stack.stackName.length).toEqual(28);
118+
expect(stack.stackName).toEqual('ShortPrefix-Short-Stack-Name');
119+
});
120+
121+
test('FF include prefix: Stacks with more than one component and a prefix hashed even if short enough', () => {
122+
// WHEN
123+
const app = new App({
124+
context: {
125+
'@aws-cdk/core:includePrefixInUniqueNameGeneration': true,
126+
},
127+
});
128+
const stage = new Stage(app, 'ThePrefix');
129+
const rootStack = new Stack(stage, 'Prod');
130+
const stack = new Stack(rootStack, 'MyStack');
131+
132+
// THEN
133+
expect(stack.stackName.length).toEqual(29);
134+
expect(stack.stackName).toEqual('ThePrefix-ProdMyStackFEA60919');
135+
});
136+
137+
test('FF include prefix: Stacks with more than one component and a prefix shortened if too big', () => {
138+
// WHEN
139+
const app = new App({
140+
context: {
141+
'@aws-cdk/core:includePrefixInUniqueNameGeneration': true,
142+
},
143+
});
144+
const stage = new Stage(app, 'ThePrefixIsLongEnoughToExceedTheMaxLenght');
145+
const construct = new Construct(stage, 'ReallyReallyLoooooooongConstructName');
146+
const stack = new BogusStack(construct, 'ThisStageNameIsVeryLongButWillOnlyBeTooLongWhenCombinedWithTheStackName');
147+
148+
// THEN
149+
expect(stack.stackName.length).toEqual(128);
150+
expect(stack.stackName).toEqual('ThePrefixIsLongEnoughToExceedTheMaxLenght-ReallyReallyLooooomeIsVeryLongButWillOnlyBeTooLongWhenCombinedWithTheStackName1E474FCA');
151+
});
152+
153+
test('generated stack names will not exceed 128 characters when using prefixes', () => {
154+
// WHEN
155+
const app = new App({
156+
context: {
157+
'@aws-cdk/core:includePrefixInUniqueNameGeneration': true,
158+
},
159+
});
160+
const stage = new Stage(app, 'ThisStageNameIsVeryLongButWillOnlyBeTooLongWhenCombinedWithTheStackName');
161+
const stack = new BogusStack(stage, 'ThisStackNameIsVeryLongButItWillOnlyBeTooLongWhenCombinedWithTheLongPrefix');
162+
163+
// THEN
164+
expect(stack.stackName.length).toEqual(128);
165+
expect(stack.stackName).toEqual('ThisStageNameIsVeryLongButWillOnlyBeTooLongWhenCombinedWithTsVeryLongButItWillOnlyBeTooLongWhenCombinedWithTheLongPrefix4CA9F65B');
166+
});
167+
106168
test('Can not have dependencies to stacks outside the nested asm', () => {
107169
// GIVEN
108170
const app = new App();

packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Flags come in three types:
5353
| [@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments](#aws-cdkaws-secretsmanageruseattachedsecretresourcepolicyforsecrettargetattachments) | SecretTargetAttachments uses the ResourcePolicy of the attached Secret. | 2.67.0 | (fix) |
5454
| [@aws-cdk/aws-redshift:columnId](#aws-cdkaws-redshiftcolumnid) | Whether to use an ID to track Redshift column changes | 2.68.0 | (fix) |
5555
| [@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2](#aws-cdkaws-stepfunctions-tasksenableemrservicepolicyv2) | Enable AmazonEMRServicePolicy_v2 managed policies | 2.72.0 | (fix) |
56+
| [@aws-cdk/core:includePrefixInUniqueNameGeneration](#aws-cdkcoreincludeprefixinuniquenamegeneration) | Include the stack prefix in the stack name generation process | V2NEXT | (fix) |
5657

5758
<!-- END table -->
5859

@@ -96,7 +97,8 @@ The following json shows the current recommended set of flags, as `cdk init` wou
9697
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
9798
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
9899
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
99-
"@aws-cdk/aws-kms:aliasNameRef": true
100+
"@aws-cdk/aws-kms:aliasNameRef": true,
101+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true
100102
}
101103
}
102104
```
@@ -986,4 +988,24 @@ intervention since they might not have the appropriate tags propagated automatic
986988
| 2.72.0 | `false` | `true` |
987989

988990

991+
### @aws-cdk/core:includePrefixInUniqueNameGeneration
992+
993+
*Include the stack prefix in the stack name generation process* (fix)
994+
995+
This flag prevents the prefix of a stack from making the stack's name longer than the 128 character limit.
996+
997+
If the flag is set, the prefix is included in the stack name generation process.
998+
If the flag is not set, then the prefix of the stack is prepended to the generated stack name.
999+
1000+
**NOTE** - Enabling this flag comes at a **risk**. If you have already deployed stacks, changing the status of this
1001+
feature flag can lead to a change in stacks' name. Changing a stack name mean recreating the whole stack, which
1002+
is not viable in some productive setups.
1003+
1004+
1005+
| Since | Default | Recommended |
1006+
| ----- | ----- | ----- |
1007+
| (not in v1) | | |
1008+
| V2NEXT | `false` | `true` |
1009+
1010+
9891011
<!-- END details -->

packages/aws-cdk-lib/cx-api/README.md

+22
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,25 @@ _cdk.json_
183183
}
184184
}
185185
```
186+
187+
* `@aws-cdk/core:includePrefixInUniqueNameGeneration`
188+
189+
Enable this feature flag to include the stack's prefixes to the name generation process.
190+
191+
Not doing so can cause the name of stack to exceed 128 characters:
192+
- The name generation ensures it doesn't exceed 128 characters
193+
- Without this feature flag, the prefix is prepended to the generated name, which result can exceed 128 characters
194+
195+
This is a feature flag as it changes the name generated for stacks. Any CDK application deployed prior this fix will
196+
most likely be generated with a new name, causing the stack to be recreated with the new name, and then deleting the old one.
197+
For applications running on production environments this can be unmanageable.
198+
199+
_cdk.json_
200+
201+
```json
202+
{
203+
"context": {
204+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true
205+
}
206+
}
207+
```

packages/aws-cdk-lib/cx-api/lib/features.ts

+20
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export const REDSHIFT_COLUMN_ID = '@aws-cdk/aws-redshift:columnId';
8686
export const ENABLE_EMR_SERVICE_POLICY_V2 = '@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2';
8787
export const EC2_RESTRICT_DEFAULT_SECURITY_GROUP = '@aws-cdk/aws-ec2:restrictDefaultSecurityGroup';
8888
export const APIGATEWAY_REQUEST_VALIDATOR_UNIQUE_ID = '@aws-cdk/aws-apigateway:requestValidatorUniqueId';
89+
export const INCLUDE_PREFIX_IN_UNIQUE_NAME_GENERATION = '@aws-cdk/core:includePrefixInUniqueNameGeneration';
8990
export const KMS_ALIAS_NAME_REF = '@aws-cdk/aws-kms:aliasNameRef';
9091

9192
export const FLAGS: Record<string, FlagInfo> = {
@@ -804,6 +805,25 @@ export const FLAGS: Record<string, FlagInfo> = {
804805
introducedIn: { v2: 'V2·NEXT' },
805806
recommendedValue: true,
806807
},
808+
809+
//////////////////////////////////////////////////////////////////////
810+
[INCLUDE_PREFIX_IN_UNIQUE_NAME_GENERATION]: {
811+
type: FlagType.BugFix,
812+
summary: 'Include the stack prefix in the stack name generation process',
813+
detailsMd: `
814+
This flag prevents the prefix of a stack from making the stack's name longer than the 128 character limit.
815+
816+
If the flag is set, the prefix is included in the stack name generation process.
817+
If the flag is not set, then the prefix of the stack is prepended to the generated stack name.
818+
819+
**NOTE** - Enabling this flag comes at a **risk**. If you have already deployed stacks, changing the status of this
820+
feature flag can lead to a change in stacks' name. Changing a stack name mean recreating the whole stack, which
821+
is not viable in some productive setups.
822+
`,
823+
introducedIn: { v2: 'V2NEXT' },
824+
recommendedValue: true,
825+
},
826+
807827
};
808828

809829
const CURRENT_MV = 'v2';

0 commit comments

Comments
 (0)