diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cfn-template-from-repo.lit.integ.snapshot/aws-cdk-codepipeline-cloudformation.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/cfn-template-from-repo.lit.integ.snapshot/aws-cdk-codepipeline-cloudformation.template.json index cb837eab62aa1..83869967f93dc 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cfn-template-from-repo.lit.integ.snapshot/aws-cdk-codepipeline-cloudformation.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cfn-template-from-repo.lit.integ.snapshot/aws-cdk-codepipeline-cloudformation.template.json @@ -44,7 +44,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinecloudformationpipeline7dbde619", + "AliasName": "alias/codepipeline-aws-cdk-codepipeline-cloudformation-pipeline-7dbde619", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKey01D58D69", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda-deployed-through-codepipeline.lit.integ.snapshot/PipelineStack.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda-deployed-through-codepipeline.lit.integ.snapshot/PipelineStack.template.json index e3da2128d9482..edce01a382b91 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda-deployed-through-codepipeline.lit.integ.snapshot/PipelineStack.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda-deployed-through-codepipeline.lit.integ.snapshot/PipelineStack.template.json @@ -38,7 +38,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-pipelinestackpipeline9db740af", + "AliasName": "alias/codepipeline-pipelinestack-pipeline-9db740af", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKey01D58D69", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda-pipeline.integ.snapshot/aws-cdk-codepipeline-lambda.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda-pipeline.integ.snapshot/aws-cdk-codepipeline-lambda.template.json index f19a0fc010bae..39ca1662ed0ec 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda-pipeline.integ.snapshot/aws-cdk-codepipeline-lambda.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda-pipeline.integ.snapshot/aws-cdk-codepipeline-lambda.template.json @@ -38,7 +38,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinelambdapipeline87a4b3d3", + "AliasName": "alias/codepipeline-aws-cdk-codepipeline-lambda-pipeline-87a4b3d3", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKey01D58D69", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-alexa-deploy.integ.snapshot/aws-cdk-codepipeline-alexa-deploy.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-alexa-deploy.integ.snapshot/aws-cdk-codepipeline-alexa-deploy.template.json index b1eb53d6c59cc..10688fe9721a6 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-alexa-deploy.integ.snapshot/aws-cdk-codepipeline-alexa-deploy.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-alexa-deploy.integ.snapshot/aws-cdk-codepipeline-alexa-deploy.template.json @@ -48,7 +48,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinealexadeploypipeline961107f5", + "AliasName": "alias/codepipeline-aws-cdk-codepipeline-alexa-deploy-pipeline-961107f5", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKey01D58D69", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-cfn.integ.snapshot/aws-cdk-codepipeline-cloudformation.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-cfn.integ.snapshot/aws-cdk-codepipeline-cloudformation.template.json index b671f21665464..fd3fafc21a418 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-cfn.integ.snapshot/aws-cdk-codepipeline-cloudformation.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-cfn.integ.snapshot/aws-cdk-codepipeline-cloudformation.template.json @@ -38,7 +38,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinecloudformationpipeline7dbde619", + "AliasName": "alias/codepipeline-aws-cdk-codepipeline-cloudformation-pipeline-7dbde619", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKey01D58D69", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-code-commit-build.integ.snapshot/aws-cdk-codepipeline-codecommit-codebuild.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-code-commit-build.integ.snapshot/aws-cdk-codepipeline-codecommit-codebuild.template.json index 7a9855fe36470..1c1241a8d23d6 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-code-commit-build.integ.snapshot/aws-cdk-codepipeline-codecommit-codebuild.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-code-commit-build.integ.snapshot/aws-cdk-codepipeline-codecommit-codebuild.template.json @@ -244,7 +244,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinecodecommitcodebuildpipeline9540e1f5", + "AliasName": "alias/codepipeline-aws-cdk-codepipeline-codecommit-codebuild-pipeline-9540e1f5", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKey01D58D69", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-code-commit.integ.snapshot/aws-cdk-codepipeline-codecommit.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-code-commit.integ.snapshot/aws-cdk-codepipeline-codecommit.template.json index cca5e3c5d5725..9ad15802f8bd6 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-code-commit.integ.snapshot/aws-cdk-codepipeline-codecommit.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-code-commit.integ.snapshot/aws-cdk-codepipeline-codecommit.template.json @@ -109,7 +109,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinecodecommitpipelinef780ca18", + "AliasName": "alias/codepipeline-aws-cdk-codepipeline-codecommit-pipeline-f780ca18", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKey01D58D69", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-events.integ.snapshot/aws-cdk-pipeline-event-target.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-events.integ.snapshot/aws-cdk-pipeline-event-target.template.json index 02a0628ba357a..38dec014dc488 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-events.integ.snapshot/aws-cdk-pipeline-event-target.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-events.integ.snapshot/aws-cdk-pipeline-event-target.template.json @@ -38,7 +38,7 @@ "MyPipelineArtifactsBucketEncryptionKeyAlias9D4F8C59": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-awscdkpipelineeventtargetmypipeline4ae5d407", + "AliasName": "alias/codepipeline-aws-cdk-pipeline-event-target-mypipeline-4ae5d407", "TargetKeyId": { "Fn::GetAtt": [ "MyPipelineArtifactsBucketEncryptionKey8BF0A7F3", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-stepfunctions.integ.snapshot/aws-cdk-codepipeline-stepfunctions.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-stepfunctions.integ.snapshot/aws-cdk-codepipeline-stepfunctions.template.json index f407e31285705..f8f998e639f18 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-stepfunctions.integ.snapshot/aws-cdk-codepipeline-stepfunctions.template.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline-stepfunctions.integ.snapshot/aws-cdk-codepipeline-stepfunctions.template.json @@ -78,7 +78,7 @@ "MyPipelineArtifactsBucketEncryptionKeyAlias9D4F8C59": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-awscdkcodepipelinestepfunctionsmypipelinece88aa28", + "AliasName": "alias/codepipeline-aws-cdk-codepipeline-stepfunctions-mypipeline-ce88aa28", "TargetKeyId": { "Fn::GetAtt": [ "MyPipelineArtifactsBucketEncryptionKey8BF0A7F3", diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index c4f1027069f59..f34c77a8cb607 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -7,6 +7,7 @@ import { ArnFormat, BootstraplessSynthesizer, DefaultStackSynthesizer, + FeatureFlags, IStackSynthesizer, Lazy, Names, @@ -17,6 +18,7 @@ import { Stage as CdkStage, Token, } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ActionCategory, IAction, IPipeline, IStage, PipelineNotificationEvents, PipelineNotifyOnOptions } from './action'; import { CfnPipeline } from './codepipeline.generated'; @@ -695,10 +697,19 @@ export class Pipeline extends PipelineBase { private generateNameForDefaultBucketKeyAlias(): string { const prefix = 'alias/codepipeline-'; const maxAliasLength = 256; - const uniqueId = Names.uniqueId(this); - // take the last 256 - (prefix length) characters of uniqueId - const startIndex = Math.max(0, uniqueId.length - (maxAliasLength - prefix.length)); - return prefix + uniqueId.substring(startIndex).toLowerCase(); + const maxResourceNameLength = maxAliasLength - prefix.length; + // Names.uniqueId() may have naming collisions when the IDs of resources are similar + // and/or when they are too long and sliced. We do not want to update this and + // automatically change the name of every KMS key already generated so we are putting + // this under a feature flag. + const uniqueId = FeatureFlags.of(this).isEnabled(cxapi.CODEPIPELINE_CROSS_ACCOUNT_KEY_ALIAS_STACK_SAFE_UNIQUE_ID) ? + Names.uniqueResourceName(this, { + separator: '-', + maxLength: maxResourceNameLength, + allowedSpecialCharacters: '/_-', + }) : + Names.uniqueId(this).slice(-maxResourceNameLength); + return prefix + uniqueId.toLowerCase(); } /** diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 3e0453cce710a..89f0519235440 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -89,7 +89,6 @@ "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", - "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^27.5.2", "jest": "^27.5.1" @@ -101,6 +100,7 @@ "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^10.0.0" }, "homepage": "https://github.com/aws/aws-cdk", @@ -111,6 +111,7 @@ "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^10.0.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts b/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts index 27db5eaf16629..463d91cf67e98 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts @@ -482,6 +482,284 @@ describe('', () => { }); }); }); + + describe('cross account key alias name tests', () => { + const kmsAliasResource = 'AWS::KMS::Alias'; + + test('cross account key alias is named with stack name instead of ID when feature flag is enabled', () => { + const stack = createPipelineStack({ + withFeatureFlag: true, + suffix: 'Name', + stackId: 'PipelineStack', + }); + + Template.fromStack(stack).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-name-pipeline-0a412eb5', + }); + }); + + test('cross account key alias is named with stack ID when feature flag is not enabled', () => { + const stack = createPipelineStack({ + suffix: 'Name', + stackId: 'PipelineStack', + }); + + Template.fromStack(stack).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-pipelinestackpipeline9db740af', + }); + }); + + test('cross account key alias is named with generated stack name when stack name is undefined and feature flag is enabled', () => { + const stack = createPipelineStack({ + withFeatureFlag: true, + suffix: 'Name', + stackId: 'PipelineStack', + undefinedStackName: true, + }); + + Template.fromStack(stack).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-pipelinestack-pipeline-9db740af', + }); + }); + + test('cross account key alias is named with stack ID when stack name is not present and feature flag is not enabled', () => { + const stack = createPipelineStack({ + suffix: 'Name', + stackId: 'PipelineStack', + undefinedStackName: true, + }); + + Template.fromStack(stack).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-pipelinestackpipeline9db740af', + }); + }); + + test('cross account key alias is named with stack name and nested stack ID when feature flag is enabled', () => { + const stack = createPipelineStack({ + withFeatureFlag: true, + suffix: 'Name', + stackId: 'TopLevelStack', + nestedStackId: 'NestedPipelineStack', + pipelineId: 'ActualPipeline', + }); + + Template.fromStack(stack.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-name-nestedpipelinestack-actualpipeline-23a98110', + }); + }); + + test('cross account key alias is named with stack ID and nested stack ID when stack name is present and feature flag is not enabled', () => { + const stack = createPipelineStack({ + suffix: 'Name', + stackId: 'TopLevelStack', + nestedStackId: 'NestedPipelineStack', + pipelineId: 'ActualPipeline', + }); + + Template.fromStack(stack.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-toplevelstacknestedpipelinestackactualpipeline3161a537', + }); + }); + + test('cross account key alias is named with generated stack name and nested stack ID when stack name is undefined and feature flag is enabled', () => { + const stack = createPipelineStack({ + withFeatureFlag: true, + suffix: 'Name', + stackId: 'TopLevelStack', + nestedStackId: 'NestedPipelineStack', + pipelineId: 'ActualPipeline', + undefinedStackName: true, + }); + + Template.fromStack(stack.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-toplevelstack-nestedpipelinestack-actualpipeline-3161a537', + }); + }); + + test('cross account key alias is named with stack ID and nested stack ID when stack name is not present and feature flag is not enabled', () => { + const stack = createPipelineStack({ + suffix: 'Name', + stackId: 'TopLevelStack', + nestedStackId: 'NestedPipelineStack', + pipelineId: 'ActualPipeline', + undefinedStackName: true, + }); + + Template.fromStack(stack.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-toplevelstacknestedpipelinestackactualpipeline3161a537', + }); + }); + + test('cross account key alias is properly shortened to 256 characters when stack name is too long and feature flag is enabled', () => { + const stack = createPipelineStack({ + withFeatureFlag: true, + suffix: 'NeedsToBeShortenedDueToTheLengthOfThisAbsurdNameThatNoOneShouldUseButItStillMightHappenSoWeMustTestForTheTestCase', + stackId: 'too-long', + pipelineId: 'ActualPipelineWithExtraSuperLongNameThatWillNeedToBeShortenedDueToTheAlsoVerySuperExtraLongNameOfTheStack-AlsoWithSomeDifferentCharactersAddedToTheEnd', + }); + + Template.fromStack(stack).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-needstobeshortenedduetothelengthofthisabsurdnamethatnooneshouldusebutitstillmighthappensowemusttestfohatwillneedtobeshortenedduetothealsoverysuperextralongnameofthestack-alsowithsomedifferentcharactersaddedtotheend-384b9343', + }); + }); + + test('cross account key alias is properly shortened to 256 characters when stack name is too long and feature flag is not enabled', () => { + const stack = createPipelineStack({ + suffix: 'too-long', + stackId: 'NeedsToBeShortenedDueToTheLengthOfThisAbsurdNameThatNoOneShouldUseButItStillMightHappenSoWeMustTestForTheTestCase', + pipelineId: 'ActualPipelineWithExtraSuperLongNameThatWillNeedToBeShortenedDueToTheAlsoVerySuperExtraLongNameOfTheStack-AlsoWithSomeDifferentCharactersAddedToTheEnd', + }); + + Template.fromStack(stack).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-ortenedduetothelengthofthisabsurdnamethatnooneshouldusebutitstillmighthappensowemusttestforthetestcaseactualpipelinewithextrasuperlongnamethatwillneedtobeshortenedduetothealsoverysuperextralongnameofthestackalsowithsomedifferentc498e0672', + }); + }); + + test('cross account key alias names do not conflict when the stack ID is the same and pipeline ID is the same and feature flag is enabled', () => { + const stack1 = createPipelineStack({ + withFeatureFlag: true, + suffix: '1', + stackId: 'STACK-ID', + }); + + const stack2 = createPipelineStack({ + withFeatureFlag: true, + suffix: '2', + stackId: 'STACK-ID', + }); + + expect(Template.fromStack(stack1).findResources(kmsAliasResource)).not.toEqual(Template.fromStack(stack2).findResources(kmsAliasResource)); + + Template.fromStack(stack1).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-1-pipeline-b09fefee', + }); + + Template.fromStack(stack2).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-2-pipeline-f46258fe', + }); + }); + + test('cross account key alias names do conflict when the stack ID is the same and pipeline ID is the same when feature flag is not enabled', () => { + const stack1 = createPipelineStack({ + suffix: '1', + stackId: 'STACK-ID', + }); + + const stack2 = createPipelineStack({ + suffix: '2', + stackId: 'STACK-ID', + }); + + expect(Template.fromStack(stack1).findResources(kmsAliasResource)).toEqual(Template.fromStack(stack2).findResources(kmsAliasResource)); + + Template.fromStack(stack1).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-stackidpipeline32fb88b3', + }); + + Template.fromStack(stack2).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-stackidpipeline32fb88b3', + }); + }); + + test('cross account key alias names do not conflict for nested stacks when pipeline ID is the same and nested stacks have the same ID when feature flag is enabled', () => { + const stack1 = createPipelineStack({ + withFeatureFlag: true, + suffix: 'Name-1', + stackId: 'STACK-ID', + nestedStackId: 'Nested', + pipelineId: 'PIPELINE-ID', + }); + const stack2 = createPipelineStack({ + withFeatureFlag: true, + suffix: 'Name-2', + stackId: 'STACK-ID', + nestedStackId: 'Nested', + pipelineId: 'PIPELINE-ID', + }); + + expect(Template.fromStack(stack1.nestedStack!).findResources(kmsAliasResource)) + .not.toEqual(Template.fromStack(stack2.nestedStack!).findResources(kmsAliasResource)); + + Template.fromStack(stack1.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-name-1-nested-pipeline-id-c8c9f252', + }); + + Template.fromStack(stack2.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-name-2-nested-pipeline-id-aff6dd63', + }); + }); + + test('cross account key alias names do conflict for nested stacks when pipeline ID is the same and nested stacks have the same ID when feature flag is not enabled', () => { + const stack1 = createPipelineStack({ + suffix: '1', + stackId: 'STACK-ID', + nestedStackId: 'Nested', + pipelineId: 'PIPELINE-ID', + }); + const stack2 = createPipelineStack({ + suffix: '2', + stackId: 'STACK-ID', + nestedStackId: 'Nested', + pipelineId: 'PIPELINE-ID', + }); + + expect(Template.fromStack(stack1.nestedStack!).findResources(kmsAliasResource)) + .toEqual(Template.fromStack(stack2.nestedStack!).findResources(kmsAliasResource)); + + Template.fromStack(stack1.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-stackidnestedpipelineid3e91360a', + }); + + Template.fromStack(stack2.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-stackidnestedpipelineid3e91360a', + }); + }); + + test('cross account key alias names do not conflict for nested stacks when in the same stack but nested stacks have different IDs when feature flag is enabled', () => { + const stack = createPipelineStack({ + withFeatureFlag: true, + suffix: 'Name-1', + stackId: 'STACK-ID', + nestedStackId: 'First', + pipelineId: 'PIPELINE-ID', + }); + const nestedStack2 = new cdk.NestedStack(stack, 'Second'); + createPipelineWithSourceAndBuildStages(nestedStack2, 'Actual-Pipeline-Name-2', 'PIPELINE-ID'); + + expect(Template.fromStack(stack.nestedStack!).findResources(kmsAliasResource)) + .not.toEqual(Template.fromStack(nestedStack2).findResources(kmsAliasResource)); + + Template.fromStack(stack.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-name-1-first-pipeline-id-3c59cb88', + }); + + Template.fromStack(nestedStack2).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-actual-stack-name-1-second-pipeline-id-16143d12', + }); + }); + + test('cross account key alias names do not conflict for nested stacks when in the same stack but nested stacks have different IDs when feature flag is not enabled', () => { + const stack = createPipelineStack({ + suffix: 'Name-1', + stackId: 'STACK-ID', + nestedStackId: 'First', + pipelineId: 'PIPELINE-ID', + }); + const nestedStack2 = new cdk.NestedStack(stack, 'Second'); + createPipelineWithSourceAndBuildStages(nestedStack2, 'Actual-Pipeline-Name-2', 'PIPELINE-ID'); + + expect(Template.fromStack(stack.nestedStack!).findResources(kmsAliasResource)) + .not.toEqual(Template.fromStack(nestedStack2).findResources(kmsAliasResource)); + + Template.fromStack(stack.nestedStack!).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-stackidfirstpipelineid5abca693', + }); + + Template.fromStack(nestedStack2).hasResourceProperties(kmsAliasResource, { + AliasName: 'alias/codepipeline-stackidsecondpipelineid288ce778', + }); + }); + }); }); describe('test with shared setup', () => { @@ -563,3 +841,59 @@ class ReusePipelineStack extends cdk.Stack { }); } } + +interface PipelineStackProps extends cdk.StackProps { + readonly nestedStackId?: string; + readonly pipelineName: string; + readonly pipelineId?: string; +} + +class PipelineStack extends cdk.Stack { + nestedStack?: cdk.NestedStack; + pipeline: codepipeline.Pipeline; + + constructor(scope?: Construct, id?: string, props?: PipelineStackProps) { + super (scope, id, props); + + props?.nestedStackId ? this.nestedStack = new cdk.NestedStack(this, props!.nestedStackId!) : undefined; + this.pipeline = createPipelineWithSourceAndBuildStages(this.nestedStack || this, props?.pipelineName, props?.pipelineId); + } +} + +function createPipelineWithSourceAndBuildStages(scope: Construct, pipelineName?: string, pipelineId: string = 'Pipeline') { + const artifact = new codepipeline.Artifact(); + return new codepipeline.Pipeline(scope, pipelineId, { + pipelineName, + crossAccountKeys: true, + reuseCrossRegionSupportStacks: false, + stages: [ + { + stageName: 'Source', + actions: [new FakeSourceAction({ actionName: 'Source', output: artifact })], + }, + { + stageName: 'Build', + actions: [new FakeBuildAction({ actionName: 'Build', input: artifact })], + }, + ], + }); +}; + +interface CreatePipelineStackOptions { + readonly withFeatureFlag?: boolean, + readonly suffix: string, + readonly stackId?: string, + readonly pipelineId?: string, + readonly undefinedStackName?: boolean, + readonly nestedStackId?: string, +} + +function createPipelineStack(options: CreatePipelineStackOptions): PipelineStack { + const context = options.withFeatureFlag ? { context: { [cxapi.CODEPIPELINE_CROSS_ACCOUNT_KEY_ALIAS_STACK_SAFE_UNIQUE_ID]: true } } : undefined; + return new PipelineStack(new cdk.App(context), options.stackId, { + stackName: options.undefinedStackName ? undefined : `Actual-Stack-${options.suffix}`, + nestedStackId: options.nestedStackId, + pipelineName: `Actual-Pipeline-${options.suffix}`.substring(0, 100), + pipelineId: options.pipelineId, + }); +}; diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline-event-target.integ.snapshot/pipeline-events.template.json b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline-event-target.integ.snapshot/pipeline-events.template.json index a11fb7f25fb7b..2a37807987742 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline-event-target.integ.snapshot/pipeline-events.template.json +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline-event-target.integ.snapshot/pipeline-events.template.json @@ -44,7 +44,7 @@ "pipelinePipeline22F2A91DArtifactsBucketEncryptionKeyAlias9530209A": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-pipelineeventspipelinepipeline22f2a91dfbb66895", + "AliasName": "alias/codepipeline-pipeline-events-pipelinepipeline22f2a91d-fbb66895", "TargetKeyId": { "Fn::GetAtt": [ "pipelinePipeline22F2A91DArtifactsBucketEncryptionKey87C796D2", diff --git a/packages/@aws-cdk/core/lib/names.ts b/packages/@aws-cdk/core/lib/names.ts index ea75927acb5ed..057ec37d3201c 100644 --- a/packages/@aws-cdk/core/lib/names.ts +++ b/packages/@aws-cdk/core/lib/names.ts @@ -1,5 +1,36 @@ import { IConstruct, Node } from 'constructs'; +import { unresolved } from './private/encoding'; +import { makeUniqueResourceName } from './private/unique-resource-name'; import { makeUniqueId } from './private/uniqueid'; +import { Stack } from './stack'; + + +/** + * Options for creating a unique resource name. +*/ +export interface UniqueResourceNameOptions { + + /** + * The maximum length of the unique resource name. + * + * @default - 256 + */ + readonly maxLength?: number; + + /** + * The separator used between the path components. + * + * @default - none + */ + readonly separator?: string; + + /** + * Non-alphanumeric characters allowed in the unique resource name. + * + * @default - none + */ + readonly allowedSpecialCharacters?: string; +} /** * Functions for devising unique names for constructs. For example, those can be @@ -9,7 +40,8 @@ export class Names { /** * Returns a CloudFormation-compatible unique identifier for a construct based * on its path. The identifier includes a human readable portion rendered - * from the path components and a hash suffix. + * from the path components and a hash suffix. uniqueId is not unique if multiple + * copies of the stack are deployed. Prefer using uniqueResourceName(). * * @param construct The construct * @returns a unique id based on the construct path @@ -35,5 +67,29 @@ export class Names { return components.length > 0 ? makeUniqueId(components) : ''; } + /** + * Returns a CloudFormation-compatible unique identifier for a construct based + * on its path. This function finds the stackName of the parent stack (non-nested) + * to the construct, and the ids of the components in the construct path. + * + * The user can define allowed special characters, a separator between the elements, + * and the maximum length of the resource name. The name includes a human readable portion rendered + * from the path components, with or without user defined separators, and a hash suffix. + * If the resource name is longer than the maximum length, it is trimmed in the middle. + * + * @param construct The construct + * @param options Options for defining the unique resource name + * @returns a unique resource name based on the construct path + */ + public static uniqueResourceName(construct: IConstruct, options: UniqueResourceNameOptions) { + const node = Node.of(construct); + + const componentsPath = node.scopes.slice(node.scopes.indexOf(node.scopes.reverse() + .find(component => (Stack.isStack(component) && !unresolved(component.stackName)))!, + )).map(component => Stack.isStack(component) && !unresolved(component.stackName) ? component.stackName : Node.of(component).id); + + return makeUniqueResourceName(componentsPath, options); + } + private constructor() {} } diff --git a/packages/@aws-cdk/core/lib/private/unique-resource-name.ts b/packages/@aws-cdk/core/lib/private/unique-resource-name.ts new file mode 100644 index 0000000000000..cf816dc9a5758 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/unique-resource-name.ts @@ -0,0 +1,122 @@ +import { createHash } from 'crypto'; +// import { unresolved } from './encoding'; + +/** + * Options for creating a unique resource name. +*/ +interface MakeUniqueResourceNameOptions { + + /** + * The maximum length of the unique resource name. + * + * @default - 256 + */ + readonly maxLength?: number; + + /** + * The separator used between the path components. + * + * @default - none + */ + readonly separator?: string; + + /** + * Non-alphanumeric characters allowed in the unique resource name. + * + * @default - none + */ + readonly allowedSpecialCharacters?: string; +} + +/** + * Resources with this ID are hidden from humans + * + * They do not appear in the human-readable part of the logical ID, + * but they are included in the hash calculation. + */ +const HIDDEN_FROM_HUMAN_ID = 'Resource'; + +/** +* Resources with this ID are complete hidden from the logical ID calculation. +*/ +const HIDDEN_ID = 'Default'; + +const PATH_SEP = '/'; + +const MAX_LEN = 256; + +const HASH_LEN = 8; + +export function makeUniqueResourceName(components: string[], options: MakeUniqueResourceNameOptions) { + const maxLength = options.maxLength ?? 256; + const separator = options.separator ?? ''; + components = components.filter(x => x !== HIDDEN_ID); + + if (components.length === 0) { + throw new Error('Unable to calculate a unique resource name for an empty set of components'); + } + + // top-level resources will simply use the `name` as-is if the name is also short enough + // in order to support transparent migration of cloudformation templates to the CDK without the + // need to rename all resources. + if (components.length === 1) { + const topLevelResource = removeNonAllowedSpecialCharacters(components[0], separator, options.allowedSpecialCharacters); + + if (topLevelResource.length <= maxLength) { + return topLevelResource; + } + } + + // Calculate the hash from the full path, included unresolved tokens so the hash value is always unique + const hash = pathHash(components); + const human = removeDupes(components) + .filter(pathElement => pathElement !== HIDDEN_FROM_HUMAN_ID) + .map(pathElement => removeNonAllowedSpecialCharacters(pathElement, separator, options.allowedSpecialCharacters)) + .filter(pathElement => pathElement) + .join(separator) + .concat(separator); + + const maxhumanLength = maxLength - HASH_LEN; + return human.length > maxhumanLength ? `${splitInMiddle(human, maxhumanLength)}${hash}`: `${human}${hash}`; +} + +/** + * Take a hash of the given path. + * + * The hash is limited in size. + */ +function pathHash(path: string[]): string { + const md5 = createHash('md5').update(path.join(PATH_SEP)).digest('hex'); + return md5.slice(0, HASH_LEN).toUpperCase(); +} + +/** + * Removes all non-allowed special characters in a string. + */ +function removeNonAllowedSpecialCharacters(s: string, _separator: string, allowedSpecialCharacters?: string) { + const pattern = allowedSpecialCharacters ? `[^A-Za-z0-9${allowedSpecialCharacters}]` : '[^A-Za-z0-9]'; + const regex = new RegExp(pattern, 'g'); + return s.replace(regex, ''); +} + +/** + * Remove duplicate "terms" from the path list + * + * If the previous path component name ends with this component name, skip the + * current component. + */ +function removeDupes(path: string[]): string[] { + const ret = new Array(); + + for (const component of path) { + if (ret.length === 0 || !ret[ret.length - 1].endsWith(component)) { + ret.push(component); + } + } + return ret; +} + +function splitInMiddle(s: string, maxLength: number = MAX_LEN - HASH_LEN) { + const half = maxLength / 2; + return s.slice(0, half) + s.slice(-half); +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/private/unique-resource-name.test.ts b/packages/@aws-cdk/core/test/private/unique-resource-name.test.ts new file mode 100644 index 0000000000000..c2dcdeff445bc --- /dev/null +++ b/packages/@aws-cdk/core/test/private/unique-resource-name.test.ts @@ -0,0 +1,101 @@ +import { createHash } from 'crypto'; +import { makeUniqueResourceName } from '../../lib/private/unique-resource-name'; + +const pathHash = (path: string[]): string => { + return createHash('md5').update(path.join('/')).digest('hex').slice(0, 8).toUpperCase(); +}; + +describe('makeUniqueResourceName tests', () => { + test('unique resource name is just resource name when the resource is top level, short enough, has no nonalphanumeric characters', () => { + const uniqueResourceName = makeUniqueResourceName(['toplevelresource'], {}); + expect(uniqueResourceName).toEqual('toplevelresource'); + }); + + test('unique resource name is shortened with a hash added when resource is top level and resource name is too long', () => { + const tooLongName = ['anamethatisslightlylongerthan256charactersthatisalsoatoplevelresourcesothereisonlyonevalueinthisarrayandapparentlybrevityisnotthestrongpointofthepersonwhonamedthisresourcewhichIbettheywillcometoregretlaterbutitiswhatitisanywhodlethisfunctionshouldshortenthis']; + const uniqueResourceName = makeUniqueResourceName(tooLongName, {}); + + const expectedName = `anamethatisslightlylongerthan256charactersthatisalsoatoplevelresourcesothereisonlyonevalueinthisarrayandapparentlybrevityisngpointofthepersonwhonamedthisresourcewhichIbettheywillcometoregretlaterbutitiswhatitisanywhodlethisfunctionshouldshortenthis${pathHash(tooLongName)}`; + expect(uniqueResourceName).toEqual(expectedName); + expect(uniqueResourceName.length).toEqual(256); + }); + + test('unique resource name removes special characters when resource is top level', () => { + const componentsPath = ['I-love-special-characters-¯\\\_(ツ)_/¯-for-real-though']; + const expectedName = 'Ilovespecialcharactersforrealthough'; + + expect(makeUniqueResourceName(componentsPath, {})).toEqual(expectedName); + }); + + test('unique resource name shortens from the middle and adds a hash when maxLength is defined, resource is top level, and resource name is longer than max', () => { + const componentsPath = ['ThisIsStillLongerThanTheAllowedLength']; + const expectedName = `ThisIsLength${pathHash(componentsPath)}`; + + expect(makeUniqueResourceName(componentsPath, { maxLength: 20 })).toEqual(expectedName); + }); + + test('unique resource name shortens from the middle and adds a hash when maxLength is defined, resource is top level, resource name is longer than max, and separator is provided', () => { + const componentsPath = ['ThisIsStillLongerThanTheAllowedLength']; + const expectedName = `ThisIsength-${pathHash(componentsPath)}`; + + expect(makeUniqueResourceName(componentsPath, { maxLength: 20, separator: '-' })).toEqual(expectedName); + }); + + test('unique resource name removes special characters and makes no other changes when resouce is top level and too long with special characters but proper length without', () => { + const tooLongName = ['a-name-that-is-slightly-longer-than-256-characters-that-is-also-a-top-level-resource-so-there-is-only-one-value-in-this-array-and-apparently-brevity-is-not-the-strong-point-of-the-person-who-named-this-resource-which-I-bet-they-will-come-to-regret-later-but-it-is-what-it-is']; + const expectedName = 'anamethatisslightlylongerthan256charactersthatisalsoatoplevelresourcesothereisonlyonevalueinthisarrayandapparentlybrevityisnotthestrongpointofthepersonwhonamedthisresourcewhichIbettheywillcometoregretlaterbutitiswhatitis'; + + expect(makeUniqueResourceName(tooLongName, {})).toEqual(expectedName); + }); + + test('unique resource name leaves in allowed special characters and adds no hash when resource is top level and resouce name is short enougn', () => { + const componentsPath = ['¯\\\_(ツ)_/¯-shruggie-gets-to-stay-¯\\\_(ツ)_/¯']; + const expectedName = '¯\_(ツ)_/¯shruggiegetstostay¯\_(ツ)_/¯'; + + expect(makeUniqueResourceName(componentsPath, { allowedSpecialCharacters: '¯\\\_(ツ)/', maxLength: 200 })).toEqual(expectedName); + }); + + test('unique resource name leaves in allowed special characters and adds no hash or separators when resource is top level and resouce name is short enougn', () => { + const componentsPath = ['¯\\\_(ツ)_/¯-shruggie-gets-to-stay-¯\\\_(ツ)_/¯']; + const expectedName = '¯\_(ツ)_/¯shruggiegetstostay¯\_(ツ)_/¯'; + + expect(makeUniqueResourceName(componentsPath, { allowedSpecialCharacters: '¯\\\_(ツ)/', maxLength: 200, separator: '-' })).toEqual(expectedName); + }); + + test('unique resource name is shortened with a hash and separator added when resource is top level, resource name is too long, and separator is provided', () => { + const tooLongName = ['anamethatisslightlylongerthan256charactersthatisalsoatoplevelresourcesothereisonlyonevalueinthisarrayandapparentlybrevityisnotthestrongpointofthepersonwhonamedthisresourcewhichIbettheywillcometoregretlaterbutitiswhatitisanywhodlethisfunctionshouldshortenthis']; + const uniqueResourceName = makeUniqueResourceName(tooLongName, { separator: '~' }); + + const expectedName = `anamethatisslightlylongerthan256charactersthatisalsoatoplevelresourcesothereisonlyonevalueinthisarrayandapparentlybrevityisnpointofthepersonwhonamedthisresourcewhichIbettheywillcometoregretlaterbutitiswhatitisanywhodlethisfunctionshouldshortenthis~${pathHash(tooLongName)}`; + expect(uniqueResourceName).toEqual(expectedName); + expect(uniqueResourceName.length).toEqual(256); + }); + + test('unique resource name removes special characters when they are included in the components names', () => { + const componentsPath = ['I', 'love', 'special', 'characters', '¯\\\_(ツ)_/¯', 'for', 'real', 'though']; + const expectedName = `Ilovespecialcharactersforrealthough${pathHash(componentsPath)}`; + + expect(makeUniqueResourceName(componentsPath, {})).toEqual(expectedName); + }); + + test('unique resource name removes special characters that are not allow listed and leaves the allowed ones', () => { + const componentsPath = ['I-love-special-characters-', '¯\\\_(ツ)_/¯', '-for-real-though-']; + const expectedName = `I-love-special-characters--for-real-though-${pathHash(componentsPath)}`; + + expect(makeUniqueResourceName(componentsPath, { allowedSpecialCharacters: '-' })).toEqual(expectedName); + }); + + test('unique resource name adds in separator and adds hash when separator is provided and name is not too long', () => { + const componentsPath = ['This', 'unique', 'resource', 'name', 'needs', 'a', 'separator']; + const expectedName = `This.*.unique.*.resource.*.name.*.needs.*.a.*.separator.*.${pathHash(componentsPath)}`; + + expect(makeUniqueResourceName(componentsPath, { separator: '.*.' })).toEqual(expectedName); + }); + + test('unique resource name adds in separator, adds hash, and shortens name when separator is provided and name too long', () => { + const componentsPath = ['This', 'unique', 'resource', 'name', 'is', 'longer', 'than', 'allowed']; + const expectedName = `This/unique/resourcelonger/than/allowed/${pathHash(componentsPath)}`; + + expect(makeUniqueResourceName(componentsPath, { maxLength: 48, separator: '/' })).toEqual(expectedName); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 1bae09a0759c7..08daf2c22d25d 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -255,6 +255,16 @@ export const IAM_MINIMIZE_POLICIES = '@aws-cdk/aws-iam:minimizePolicies'; */ export const VALIDATE_SNAPSHOT_REMOVAL_POLICY = '@aws-cdk/core:validateSnapshotRemovalPolicy'; +/** + * Enable this feature flag to have CodePipeline generate a unique cross account key alias name using the stack name. + * + * Previously, when creating multiple pipelines with similar naming conventions and when crossAccountKeys is true, + * the KMS key alias name created for these pipelines may be the same due to how the uniqueId is generated. + * + * This new implementation creates a stack safe uniqueId for the alias name using the stack name instead of the stack ID. + */ +export const CODEPIPELINE_CROSS_ACCOUNT_KEY_ALIAS_STACK_SAFE_UNIQUE_ID = '@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeUniqueId'; + /** * Flag values that should apply for new projects * @@ -284,6 +294,7 @@ export const FUTURE_FLAGS: { [key: string]: boolean } = { [CHECK_SECRET_USAGE]: true, [IAM_MINIMIZE_POLICIES]: true, [VALIDATE_SNAPSHOT_REMOVAL_POLICY]: true, + [CODEPIPELINE_CROSS_ACCOUNT_KEY_ALIAS_STACK_SAFE_UNIQUE_ID]: true, }; /** diff --git a/packages/@aws-cdk/pipelines/test/pipeline-security.integ.snapshot/PipelineSecurityStack.template.json b/packages/@aws-cdk/pipelines/test/pipeline-security.integ.snapshot/PipelineSecurityStack.template.json index 575b93e6806be..4a198bda45898 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-security.integ.snapshot/PipelineSecurityStack.template.json +++ b/packages/@aws-cdk/pipelines/test/pipeline-security.integ.snapshot/PipelineSecurityStack.template.json @@ -212,7 +212,7 @@ "TestPipelineArtifactsBucketEncryptionKeyAliasE8D86DD3": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-pipelinesecuritystacktestpipelinef7060861", + "AliasName": "alias/codepipeline-pipelinesecuritystack-testpipeline-f7060861", "TargetKeyId": { "Fn::GetAtt": [ "TestPipelineArtifactsBucketEncryptionKey13258842", diff --git a/packages/@aws-cdk/pipelines/test/pipeline-with-assets-single-upload.integ.snapshot/PipelineStack.template.json b/packages/@aws-cdk/pipelines/test/pipeline-with-assets-single-upload.integ.snapshot/PipelineStack.template.json index 4bc78e4f6e723..00b5ffe04a975 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-with-assets-single-upload.integ.snapshot/PipelineStack.template.json +++ b/packages/@aws-cdk/pipelines/test/pipeline-with-assets-single-upload.integ.snapshot/PipelineStack.template.json @@ -212,7 +212,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias94A07392": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-pipelinestackpipelinee95eedaa", + "AliasName": "alias/codepipeline-pipelinestack-pipeline-e95eedaa", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKeyF5BF0670", diff --git a/packages/@aws-cdk/pipelines/test/pipeline-with-assets.integ.snapshot/PipelineStack.template.json b/packages/@aws-cdk/pipelines/test/pipeline-with-assets.integ.snapshot/PipelineStack.template.json index 3b4a804b2bf66..e0d29148d1a94 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-with-assets.integ.snapshot/PipelineStack.template.json +++ b/packages/@aws-cdk/pipelines/test/pipeline-with-assets.integ.snapshot/PipelineStack.template.json @@ -212,7 +212,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias94A07392": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-pipelinestackpipelinee95eedaa", + "AliasName": "alias/codepipeline-pipelinestack-pipeline-e95eedaa", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKeyF5BF0670", diff --git a/packages/@aws-cdk/pipelines/test/pipeline.integ.snapshot/PipelineStack.template.json b/packages/@aws-cdk/pipelines/test/pipeline.integ.snapshot/PipelineStack.template.json index 5765c36030f37..973c97e05586a 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline.integ.snapshot/PipelineStack.template.json +++ b/packages/@aws-cdk/pipelines/test/pipeline.integ.snapshot/PipelineStack.template.json @@ -212,7 +212,7 @@ "PipelineArtifactsBucketEncryptionKeyAlias94A07392": { "Type": "AWS::KMS::Alias", "Properties": { - "AliasName": "alias/codepipeline-pipelinestackpipelinee95eedaa", + "AliasName": "alias/codepipeline-pipelinestack-pipeline-e95eedaa", "TargetKeyId": { "Fn::GetAtt": [ "PipelineArtifactsBucketEncryptionKeyF5BF0670",