From 211671e48c4b9bdd57e32fe36c396e52c159872d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 7 Dec 2020 15:06:48 +0100 Subject: [PATCH] fix(core): autogenerated exports do not account for stack name length Autogenerated IDs are limited to 248 characters. However, for exports these are combined with a stack name, which may make the combination exceed the maximum length of 255 characters. Account for the stack name length while generating these IDs. Fixes #9733. --- packages/@aws-cdk/core/lib/cfn-output.ts | 8 ++++++ packages/@aws-cdk/core/lib/private/refs.ts | 5 ++-- packages/@aws-cdk/core/test/output.test.ts | 14 ++++++++- packages/@aws-cdk/core/test/stack.test.ts | 33 ++++++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/core/lib/cfn-output.ts b/packages/@aws-cdk/core/lib/cfn-output.ts index 480de577db06c..e296ecce34375 100644 --- a/packages/@aws-cdk/core/lib/cfn-output.ts +++ b/packages/@aws-cdk/core/lib/cfn-output.ts @@ -163,10 +163,18 @@ export class CfnOutput extends CfnElement { }, }; } + + protected validate(): string[] { + if (this._exportName && !Token.isUnresolved(this._exportName) && this._exportName.length > 255) { + return [`Export name cannot exceed 255 characters (got ${this._exportName.length} characters)`]; + } + return []; + } } import { CfnCondition } from './cfn-condition'; import { Fn } from './cfn-fn'; import { Lazy } from './lazy'; import { Stack } from './stack'; +import { Token } from './token'; diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 05bb7930bc34f..46d44563b4a96 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -213,8 +213,9 @@ function generateExportName(stackExports: Construct, id: string) { id, ]; const prefix = stack.stackName ? stack.stackName + ':' : ''; - const exportName = prefix + makeUniqueId(components); - return exportName; + const localPart = makeUniqueId(components); + const maxLength = 255; + return prefix + localPart.slice(Math.max(0, localPart.length - maxLength + prefix.length)); } // ------------------------------------------------------------------------------------------------ diff --git a/packages/@aws-cdk/core/test/output.test.ts b/packages/@aws-cdk/core/test/output.test.ts index 1179c3111c0c9..0359049756946 100644 --- a/packages/@aws-cdk/core/test/output.test.ts +++ b/packages/@aws-cdk/core/test/output.test.ts @@ -1,5 +1,5 @@ import { nodeunitShim, Test } from 'nodeunit-shim'; -import { App, CfnOutput, CfnResource, Stack } from '../lib'; +import { App, CfnOutput, CfnResource, ConstructNode, Stack, ValidationError } from '../lib'; import { toCloudFormation } from './util'; let app: App; @@ -113,4 +113,16 @@ nodeunitShim({ test.done(); }, + + 'Verify maximum length of export name'(test: Test) { + new CfnOutput(stack, 'SomeOutput', { value: 'x', exportName: 'x'.repeat(260) }); + + const errors = ConstructNode.validate(stack.node).map((v: ValidationError) => v.message); + + expect(errors).toEqual([ + expect.stringContaining('Export name cannot exceed 255 characters'), + ]); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index dad2a64b3235c..63c04be2e81de 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -261,6 +261,39 @@ nodeunitShim({ test.done(); }, + 'Cross-stack export names account for stack name lengths'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1', { + stackName: 'SoThisCouldPotentiallyBeAVeryLongStackName', + }); + let scope: Construct = stack1; + + // WHEN - deeply nested + for (let i = 0; i < 50; i++) { + scope = new Construct(scope, `ChildConstruct${i}`); + } + + const resource1 = new CfnResource(scope, 'Resource', { type: 'BLA' }); + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another resource + new CfnResource(stack2, 'SomeResource', { + type: 'AWS::Some::Resource', + properties: { + someProperty: new Intrinsic(resource1.ref), + }, + }); + + // THEN + const assembly = app.synth(); + const template1 = assembly.getStackByName(stack1.stackName).template; + + const theOutput = template1.Outputs[Object.keys(template1.Outputs)[0]]; + expect(theOutput.Export.Name.length).toEqual(255); + test.done(); + }, + 'Cross-stack reference export names are relative to the stack (when the flag is set)'(test: Test) { // GIVEN const app = new App({