diff --git a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts index ffef076b292f6..199594ef95bf0 100644 --- a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts +++ b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts @@ -1,4 +1,4 @@ -import { arrayWith, ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import { ResourcePart, SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; @@ -124,31 +124,6 @@ test('"readers" or "grantRead" can be used to grant read permissions on the asse }); }); -test('"grantRead" also gives KMS permissions when using the new bootstrap stack', () => { - const stack = new cdk.Stack(undefined, undefined, { - synthesizer: new cdk.DefaultStackSynthesizer(), - }); - const group = new iam.Group(stack, 'MyGroup'); - - const asset = new Asset(stack, 'MyAsset', { - path: path.join(__dirname, 'sample-asset-directory'), - readers: [group], - }); - - asset.grantRead(group); - - expect(stack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: arrayWith({ - Action: ['kms:Decrypt', 'kms:DescribeKey'], - Effect: 'Allow', - Resource: { 'Fn::ImportValue': 'CdkBootstrap-hnb659fds-FileAssetKeyArn' }, - }), - }, - }); -}); - test('fails if directory not found', () => { const stack = new cdk.Stack(); expect(() => new Asset(stack, 'MyDirectory', { diff --git a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts index 0859347c66c5f..09b169298c0f3 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts +++ b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts @@ -1,4 +1,3 @@ -import { arrayWith } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as iam from '@aws-cdk/aws-iam'; @@ -621,30 +620,4 @@ test('deploy without deleting missing files from destination', () => { expect(stack).toHaveResourceLike('Custom::CDKBucketDeployment', { Prune: false, }); -}); - -test('Deployment role gets KMS permissions when using assets from new style synthesizer', () => { - const stack = new cdk.Stack(undefined, undefined, { - synthesizer: new cdk.DefaultStackSynthesizer(), - }); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], - destinationBucket: bucket, - }); - - // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: arrayWith({ - Action: ['kms:Decrypt', 'kms:DescribeKey'], - Effect: 'Allow', - Resource: { 'Fn::ImportValue': 'CdkBootstrap-hnb659fds-FileAssetKeyArn' }, - }), - }, - }); - -}); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index 50dbcddbcaf5f..012b7da4d489c 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -223,6 +223,8 @@ export interface FileAssetLocation { * can be used as an example for how to configure the key properly. * * @default - Asset bucket is not encrypted + * @deprecated Since bootstrap bucket v4, the key policy properly allows use of the + * key via the bucket and no additional parameters have to be granted anymore. */ readonly kmsKeyArn?: string; } diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index 5a373cd3ed061..cac58e7461528 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -4,6 +4,8 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetPackaging, FileAssetSource } from '../assets'; import { Fn } from '../cfn-fn'; +import { CfnParameter } from '../cfn-parameter'; +import { CfnRule } from '../cfn-rule'; import { ISynthesisSession } from '../construct-compat'; import { Stack } from '../stack'; import { Token } from '../token'; @@ -17,7 +19,7 @@ export const BOOTSTRAP_QUALIFIER_CONTEXT = '@aws-cdk/core:bootstrapQualifier'; /** * The minimum bootstrap stack version required by this app. */ -const MIN_BOOTSTRAP_STACK_VERSION = 3; +const MIN_BOOTSTRAP_STACK_VERSION = 4; /** * Configuration properties for DefaultStackSynthesizer @@ -233,6 +235,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { this.imageAssetPublishingRoleArn = specialize(this.props.imageAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN); this._kmsKeyArnExportName = specialize(this.props.fileAssetKeyArnExportName ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_KEY_ARN_EXPORT_NAME); /* eslint-enable max-len */ + + addBootstrapVersionRule(stack, MIN_BOOTSTRAP_STACK_VERSION, qualifier); } public addFileAsset(asset: FileAssetSource): FileAssetLocation { @@ -270,7 +274,6 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { httpUrl, s3ObjectUrl, s3Url: httpUrl, - kmsKeyArn: Fn.importValue(cfnify(this._kmsKeyArnExportName)), }; } @@ -462,3 +465,38 @@ function stackLocationOrInstrinsics(stack: Stack) { urlSuffix: resolvedOr(stack.urlSuffix, '${AWS::URLSuffix}'), }; } + +/** + * Add a CfnRule to the Stack which checks the current version of the bootstrap stack this template is targeting + * + * The CLI normally checks this, but in a pipeline the CLI is not involved + * so we encode this rule into the template in a way that CloudFormation will check it. + */ +function addBootstrapVersionRule(stack: Stack, requiredVersion: number, qualifier: string) { + const param = new CfnParameter(stack, 'BootstrapVersion', { + type: 'AWS::SSM::Parameter::Value', + description: 'Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store.', + default: `/aws-cdk-bootstrap/${qualifier}/version`, + }); + + // There is no >= check in CloudFormation, so we have to check the number + // is NOT in [1, 2, 3, ... - 1] + const oldVersions = range(1, requiredVersion).map(n => `${n}`); + + new CfnRule(stack, 'CheckBootstrapVersion', { + assertions: [ + { + assert: Fn.conditionNot(Fn.conditionContains(oldVersions, param.valueAsString)), + assertDescription: `CDK bootstrap stack version ${requiredVersion} required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.`, + }, + ], + }); +} + +function range(startIncl: number, endExcl: number) { + const ret = new Array(); + for (let i = startIncl; i < endExcl; i++) { + ret.push(i); + } + return ret; +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts b/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts index 6ed4c1a52a360..79c852d494247 100644 --- a/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts +++ b/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts @@ -35,7 +35,10 @@ export = { // THEN -- the S3 url is advertised on the stack artifact const stackArtifact = asm.getStackArtifact('Stack'); - test.equals(stackArtifact.stackTemplateAssetObjectUrl, 's3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/4bdae6e3b1b15f08c889d6c9133f24731ee14827a9a9ab9b6b6a9b42b6d34910'); + + const templateHash = '19e1e8612660f79362e091714ab7b3583961936d762c75be8b8083c3af40850a'; + + test.equals(stackArtifact.stackTemplateAssetObjectUrl, `s3://cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}/${templateHash}`); // THEN - the template is in the asset manifest const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; @@ -49,7 +52,7 @@ export = { destinations: { 'current_account-current_region': { bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', - objectKey: '4bdae6e3b1b15f08c889d6c9133f24731ee14827a9a9ab9b6b6a9b42b6d34910', + objectKey: templateHash, assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}', }, }, @@ -58,6 +61,28 @@ export = { test.done(); }, + 'version check is added to template'(test: Test) { + // GIVEN + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // THEN + const template = app.synth().getStackByName('Stack').template; + test.deepEqual(template?.Parameters?.BootstrapVersion?.Type, 'AWS::SSM::Parameter::Value'); + test.deepEqual(template?.Parameters?.BootstrapVersion?.Default, '/aws-cdk-bootstrap/hnb659fds/version'); + + const assertions = template?.Rules?.CheckBootstrapVersion?.Assertions ?? []; + test.deepEqual(assertions.length, 1); + test.deepEqual(assertions[0].Assert, { + 'Fn::Not': [ + { 'Fn::Contains': [['1', '2', '3'], { Ref: 'BootstrapVersion' }] }, + ], + }); + + test.done(); + }, + 'add file asset'(test: Test) { // WHEN const location = stack.synthesizer.addFileAsset({ diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json index 67229ae806bb0..0f87f89024dbb 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json @@ -1,4 +1,36 @@ { + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws-cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 4 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + }, "Resources": { "PipelineUpdatePipelineSelfMutationRole57E559E8": { "Type": "AWS::IAM::Role", diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json index 78e618e62ae12..336c2494b3f00 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json @@ -1,4 +1,36 @@ { + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws-cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 4 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + }, "Resources": { "PipelineUpdatePipelineSelfMutationRole57E559E8": { "Type": "AWS::IAM::Role", diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts index ff91b55227c75..99f879358bda6 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts @@ -8,6 +8,8 @@ export const REPOSITORY_NAME_OUTPUT = 'RepositoryName'; export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName'; /** @experimental */ export const BOOTSTRAP_VERSION_OUTPUT = 'BootstrapVersion'; +/** @experimental */ +export const BOOTSTRAP_VERSION_RESOURCE = 'CdkBootstrapVersion'; /** * Options for the bootstrapEnvironment operation(s) diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index c3b744c83d9e2..8adc6f447dc82 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -65,7 +65,7 @@ Conditions: UsePublicAccessBlockConfiguration: Fn::Equals: - 'true' - - Ref: PublicAccessBlockConfiguration + - Ref: PublicAccessBlockConfiguration Resources: FileAssetsBucketEncryptionKey: Type: AWS::KMS::Key @@ -99,11 +99,14 @@ Resources: - kms:GenerateDataKey* Effect: Allow Principal: - AWS: - Ref: AWS::AccountId + # Not actually everyone -- see below for Conditions + AWS: "*" Resource: "*" Condition: StringEquals: + # See https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-caller-account + kms:CallerAccount: + Ref: AWS::AccountId kms:ViaService: - Fn::Sub: s3.${AWS::Region}.amazonaws.com - Action: @@ -143,7 +146,7 @@ Resources: BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true - - Ref: AWS::NoValue + - Ref: AWS::NoValue UpdateReplacePolicy: Retain StagingBucketPolicy: Type: 'AWS::S3::BucketPolicy' @@ -350,6 +353,15 @@ Resources: - Ref: AWS::NoValue RoleName: Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + # The SSM parameter is used in pipeline-deployed templates to verify the version + # of the bootstrap resources. + CdkBootstrapVersion: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: + Fn::Sub: '/aws-cdk-bootstrap/${Qualifier}/version' + Value: 4 Outputs: BucketName: Description: The name of the S3 bucket owned by the CDK toolkit stack @@ -359,8 +371,11 @@ Outputs: Description: The domain name of the S3 bucket owned by the CDK toolkit stack Value: Fn::Sub: "${StagingBucket.RegionalDomainName}" + # @deprecated - This Export can be removed at some future point in time. + # We can't do it today because if there are stacks that use it, the bootstrap + # stack cannot be updated. Not used anymore by apps >= 1.60.0 FileAssetKeyArn: - Description: The ARN of the KMS key used to encrypt the asset bucket + Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated) Value: Fn::If: - CreateNewKey @@ -373,10 +388,9 @@ Outputs: Description: The name of the ECR repository which hosts docker image assets Value: Fn::Sub: "${ContainerAssetsRepository}" + # The Output is used by the CLI to verify the version of the bootstrap resources. BootstrapVersion: Description: The version of the bootstrap resources that are currently mastered in this stack - Value: '3' - Export: - Name: - Fn::Sub: CdkBootstrap-${Qualifier}-Version \ No newline at end of file + Value: + Fn::GetAtt: [CdkBootstrapVersion, Value] \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts index c30f9cb6138d8..9182e291fee17 100644 --- a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts +++ b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts @@ -59,5 +59,16 @@ export async function deployBootstrapStack( } function bootstrapVersionFromTemplate(template: any): number { - return parseInt(template.Outputs?.[BOOTSTRAP_VERSION_OUTPUT]?.Value ?? '0', 10); + const versionSources = [ + template.Outputs?.[BOOTSTRAP_VERSION_OUTPUT]?.Value, + template.Resources?.[BOOTSTRAP_VERSION_OUTPUT]?.Properties?.Value, + ]; + + for (const vs of versionSources) { + if (typeof vs === 'number') { return vs; } + if (typeof vs === 'string' && !isNaN(parseInt(vs, 10))) { + return parseInt(vs, 10); + } + } + return 0; } \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts index a553d103da403..3a50677f0373a 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts @@ -383,7 +383,7 @@ abstract class ActivityPrinterBase implements IActivityPrinter { this.resourcesInProgress[activity.event.LogicalResourceId] = activity; } - if (status.endsWith('_FAILED')) { + if (hasErrorMessage(status)) { const isCancelled = (activity.event.ResourceStatusReason ?? '').indexOf('cancelled') > -1; // Cancelled is not an interesting failure reason @@ -630,7 +630,7 @@ export class CurrentActivityPrinter extends ActivityPrinterBase { } private failureReasonOnNextLine(activity: StackActivity) { - return (activity.event.ResourceStatus ?? '').endsWith('_FAILED') + return hasErrorMessage(activity.event.ResourceStatus ?? '') ? `\n${' '.repeat(TIMESTAMP_WIDTH + STATUS_WIDTH + 6)}${colors.red(activity.event.ResourceStatusReason ?? '')}` : ''; } @@ -642,6 +642,10 @@ const MAX_PROGRESSBAR_WIDTH = 60; const MIN_PROGRESSBAR_WIDTH = 10; const PROGRESSBAR_EXTRA_SPACE = 2 /* leading spaces */ + 2 /* brackets */ + 4 /* progress number decoration */ + 6 /* 2 progress numbers up to 999 */; +function hasErrorMessage(status: string) { + return status.endsWith('_FAILED') || status === 'ROLLBACK_IN_PROGRESS' || status === 'UPDATE_ROLLBACK_IN_PROGRESS'; +} + function colorFromStatusResult(status?: string) { if (!status) { return colors.reset; @@ -672,7 +676,8 @@ function colorFromStatusActivity(status?: string) { if (status.startsWith('CREATE_') || status.startsWith('UPDATE_')) { return colors.green; } - if (status.startsWith('ROLLBACK_')) { + // For stacks, it may also be 'UPDDATE_ROLLBACK_IN_PROGRESS' + if (status.indexOf('ROLLBACK_') !== -1) { return colors.yellow; } if (status.startsWith('DELETE_')) { diff --git a/packages/aws-cdk/test/api/bootstrap2.test.ts b/packages/aws-cdk/test/api/bootstrap2.test.ts index 94dbccb2d2a42..c66fa70918b11 100644 --- a/packages/aws-cdk/test/api/bootstrap2.test.ts +++ b/packages/aws-cdk/test/api/bootstrap2.test.ts @@ -108,11 +108,8 @@ describe('Bootstrapping v2', () => { .map((o: any) => o.Export.Name); expect(exports).toEqual([ - // This is used by aws-s3-assets + // This used to be used by aws-s3-assets { 'Fn::Sub': 'CdkBootstrap-${Qualifier}-FileAssetKeyArn' }, - // This is used by the CLI to verify the bootstrap stack version, - // and could also be used by templates which are deployed through pipelines. - { 'Fn::Sub': 'CdkBootstrap-${Qualifier}-Version' }, ]); });