diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 13710b6c53b09..b3f98c979c575 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -659,7 +659,9 @@ export async function installNpmPackages(fixture: TestFixture, packages: Record< }, undefined, 2), { encoding: 'utf-8' }); // Now install that `package.json` using NPM7 - await fixture.shell(['node', require.resolve('npm'), 'install']); + const isRepoMode = !!process.env.REPO_ROOT; + const npmPath = isRepoMode ? path.join(__dirname, 'package-sources/repo-tools/npm') : require.resolve('npm'); + await fixture.shell(['node', npmPath, 'install']); } const ALREADY_BOOTSTRAPPED_IN_THIS_RUN = new Set(); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index dd2797823198f..148098e54f6a4 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -721,6 +721,18 @@ class BuiltinLambdaStack extends cdk.Stack { } } +class IamRolesStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + for(let i = 1; i <= Number(process.env.NUMBER_OF_ROLES) ; i++) { + new iam.Role(this, `Role${i}`, { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + } + } +} + class NotificationArnPropStack extends cdk.Stack { constructor(parent, id, props) { super(parent, id, props); @@ -807,6 +819,8 @@ switch (stackSet) { new DockerStack(app, `${stackPrefix}-docker`); new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`); + new IamRolesStack(app, `${stackPrefix}-iam-roles`); + new NotificationArnPropStack(app, `${stackPrefix}-notification-arn-prop`, { notificationArns: [`arn:aws:sns:${defaultEnv.region}:${defaultEnv.account}:${stackPrefix}-test-topic-prop`], }); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index a38580d10714a..33a951249edd2 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -957,6 +957,26 @@ integTest( }), ); +integTest('cdk diff with a large template', withDefaultFixture(async (fixture) => { + // In this test, we confirm that the changeset role can upload assets to the bucket and create a changeset + await fixture.cdkDeploy('iam-roles', { + modEnv: { + NUMBER_OF_ROLES: '1', + }, + }); + const diff = await fixture.cdk(['diff', fixture.fullStackName('iam-roles')], { + modEnv: { + NUMBER_OF_ROLES: '200', + }, + }); + expect(diff).not.toContain('changeset role does not exist, hence was not assumed'); + expect(diff).not.toContain('Could not create a change set'); + expect(diff).not.toContain('deploy-role'); + expect(diff).toContain('cfn-changeset-review-role'); + expect(diff).toContain('success: Published'); + expect(diff).toContain('Number of stacks with differences'); +})); + integTest( 'enableDiffNoFail', withDefaultFixture(async (fixture) => { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs-patterns/test/fargate/integ.alb-fargate-service-ipv6.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs-patterns/test/fargate/integ.alb-fargate-service-ipv6.js.snapshot/cdk.out index 1f0068d32659a..079dd58c72d69 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs-patterns/test/fargate/integ.alb-fargate-service-ipv6.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs-patterns/test/fargate/integ.alb-fargate-service-ipv6.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"36.0.0"} \ No newline at end of file +{"version":"37.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md b/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md index 1e45d14d5a2f7..cdf18869fac76 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md @@ -159,6 +159,7 @@ const app = new App({ cloudFormationExecutionRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Execute'), deploymentRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Deploy'), lookupRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Lookup'), + changesetRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Changeset'), }), }), }); @@ -167,8 +168,8 @@ const app = new App({ Or, you can ask to use the CLI credentials that exist at deploy-time. These credentials must have the ability to perform CloudFormation calls, lookup resources in your account, and perform CloudFormation deployment. -For a full list of what is necessary, see `LookupRole`, `DeploymentActionRole`, -and `CloudFormationExecutionRole` in the +For a full list of what is necessary, see `LookupRole`, `DeploymentActionRole`, `CloudFormationExecutionRole`, +and `CloudFormationChangeSetReviewRole` in the [bootstrap template](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml). ```ts diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts index 7e926d00e6ad0..40d2a5f434d2f 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts @@ -118,6 +118,11 @@ export class AppStagingSynthesizer extends StackSynthesizer implements IReusable */ public static readonly DEFAULT_LOOKUP_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region}'; + /** + * Default changeset role ARN for missing values. + */ + public static readonly DEFAULT_CHANGESET_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-cfn-changeset-review-role-${AWS::AccountId}-${AWS::Region}'; + /** * Use the Default Staging Resources, creating a single stack per environment this app is deployed in */ @@ -177,7 +182,8 @@ export class AppStagingSynthesizer extends StackSynthesizer implements IReusable this.roles = { deploymentRole: identities.deploymentRole ?? defaultIdentities.deploymentRole!, cloudFormationExecutionRole: identities.cloudFormationExecutionRole ?? defaultIdentities.cloudFormationExecutionRole!, - lookupRole: identities.lookupRole ?? identities.lookupRole!, + lookupRole: identities.lookupRole ?? defaultIdentities.lookupRole!, + changesetRole: identities.changesetRole ?? defaultIdentities.changesetRole!, }; } @@ -207,6 +213,7 @@ export class AppStagingSynthesizer extends StackSynthesizer implements IReusable deployRole, cloudFormationExecutionRole: this.roles.cloudFormationExecutionRole._specialize(spec), lookupRole: this.roles.lookupRole._specialize(spec), + changesetRole: this.roles.changesetRole._specialize(spec), qualifier, }); } @@ -292,6 +299,11 @@ interface BoundAppStagingSynthesizerProps { * Lookup Role */ readonly lookupRole: BootstrapRole; + + /** + * Changeset Role + */ + readonly changesetRole: BootstrapRole; } class BoundAppStagingSynthesizer extends StackSynthesizer implements IBoundAppStagingSynthesizer { @@ -323,12 +335,14 @@ class BoundAppStagingSynthesizer extends StackSynthesizer implements IBoundAppSt const assetManifestId = this.assetManifest.emitManifest(this.boundStack, session, {}, dependencies); const lookupRoleArn = this.props.lookupRole?._arnForCloudAssembly(); + const changesetRoleArn = this.props.changesetRole?._arnForCloudAssembly(); this.emitArtifact(session, { assumeRoleArn: this.props.deployRole?._arnForCloudAssembly(), additionalDependencies: [assetManifestId], stackTemplateAssetObjectUrl: templateAsset.s3ObjectUrlWithPlaceholders, cloudFormationExecutionRoleArn: this.props.cloudFormationExecutionRole?._arnForCloudAssembly(), + changesetRole: changesetRoleArn ? { arn: changesetRoleArn } : undefined, lookupRole: lookupRoleArn ? { arn: lookupRoleArn } : undefined, }); } diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts index a91311e4660f9..f9b44dfa49508 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts @@ -67,6 +67,7 @@ export class DeploymentIdentities { cloudFormationExecutionRole: BootstrapRole.cliCredentials(), deploymentRole: BootstrapRole.cliCredentials(), lookupRole: BootstrapRole.cliCredentials(), + changesetRole: BootstrapRole.cliCredentials(), }); } @@ -93,6 +94,7 @@ export class DeploymentIdentities { deploymentRole: BootstrapRole.fromRoleArn(replacePlaceholders(AppStagingSynthesizer.DEFAULT_DEPLOY_ROLE_ARN)), cloudFormationExecutionRole: BootstrapRole.fromRoleArn(replacePlaceholders(AppStagingSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN)), lookupRole: BootstrapRole.fromRoleArn(replacePlaceholders(AppStagingSynthesizer.DEFAULT_LOOKUP_ROLE_ARN)), + changesetRole: BootstrapRole.fromRoleArn(replacePlaceholders(AppStagingSynthesizer.DEFAULT_CHANGESET_ROLE_ARN)), }); } @@ -112,6 +114,12 @@ export class DeploymentIdentities { */ public readonly lookupRole?: BootstrapRole; + /** + * Changeset Role + @default - use bootstrapped role + */ + public readonly changesetRole?: BootstrapRole; + private constructor( /** roles that are bootstrapped to your account. */ roles: BootstrapRoles, @@ -119,6 +127,7 @@ export class DeploymentIdentities { this.cloudFormationExecutionRole = roles.cloudFormationExecutionRole; this.deploymentRole = roles.deploymentRole; this.lookupRole = roles.lookupRole; + this.changesetRole = roles.changesetRole; } } @@ -160,6 +169,13 @@ export interface BootstrapRoles { * @default - use bootstrapped role */ readonly lookupRole?: BootstrapRole; + + /** + * Changeset Role + * + * @default - use bootstrapped role + */ + readonly changesetRole?: BootstrapRole; } /** diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts index 022e4a2cb603a..4c1c228f3cf54 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts @@ -8,6 +8,7 @@ import { AppStagingSynthesizer, BootstrapRole, DeploymentIdentities } from '../l const CLOUDFORMATION_EXECUTION_ROLE = 'cloudformation-execution-role'; const DEPLOY_ACTION_ROLE = 'deploy-action-role'; const LOOKUP_ROLE = 'lookup-role'; +const CHANG_SET_ROLE = 'changeset-role'; describe('Boostrap Roles', () => { test('default bootstrap role name is always under 64 characters', () => { @@ -48,6 +49,7 @@ describe('Boostrap Roles', () => { cloudFormationExecutionRole: BootstrapRole.fromRoleArn(CLOUDFORMATION_EXECUTION_ROLE), lookupRole: BootstrapRole.fromRoleArn(LOOKUP_ROLE), deploymentRole: BootstrapRole.fromRoleArn(DEPLOY_ACTION_ROLE), + changesetRole: BootstrapRole.fromRoleArn(CHANG_SET_ROLE), }), stagingBucketEncryption: BucketEncryption.S3_MANAGED, }), @@ -72,6 +74,7 @@ describe('Boostrap Roles', () => { expect(stackArtifact.cloudFormationExecutionRoleArn).toEqual(CLOUDFORMATION_EXECUTION_ROLE); expect(stackArtifact.lookupRole).toEqual({ arn: LOOKUP_ROLE }); expect(stackArtifact.assumeRoleArn).toEqual(DEPLOY_ACTION_ROLE); + expect(stackArtifact.changesetRole).toEqual({ arn: CHANG_SET_ROLE }); }); test('can request other bootstrap region', () => { @@ -93,6 +96,7 @@ describe('Boostrap Roles', () => { expect(stackArtifact.cloudFormationExecutionRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-hnb659fds-cfn-exec-role-000000000000-us-west-2'); expect(stackArtifact.lookupRole).toEqual({ arn: 'arn:${AWS::Partition}:iam::000000000000:role/cdk-hnb659fds-lookup-role-000000000000-us-west-2' }); expect(stackArtifact.assumeRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-hnb659fds-deploy-role-000000000000-us-west-2'); + expect(stackArtifact.changesetRole).toEqual({ arn: 'arn:${AWS::Partition}:iam::000000000000:role/cdk-hnb659fds-cfn-changeset-review-role-000000000000-us-west-2' }); }); test('can request other qualifier', () => { @@ -115,6 +119,7 @@ describe('Boostrap Roles', () => { expect(stackArtifact.cloudFormationExecutionRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-Q-cfn-exec-role-000000000000-us-west-2'); expect(stackArtifact.lookupRole).toEqual({ arn: 'arn:${AWS::Partition}:iam::000000000000:role/cdk-Q-lookup-role-000000000000-us-west-2' }); expect(stackArtifact.assumeRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-Q-deploy-role-000000000000-us-west-2'); + expect(stackArtifact.changesetRole).toEqual({ arn: 'arn:${AWS::Partition}:iam::000000000000:role/cdk-Q-cfn-changeset-review-role-000000000000-us-west-2' }); }); test('can supply existing arn for bucket staging role', () => { @@ -210,6 +215,7 @@ describe('Boostrap Roles', () => { expect(stackArtifact.cloudFormationExecutionRoleArn).toBeUndefined(); expect(stackArtifact.lookupRole).toBeUndefined(); expect(stackArtifact.assumeRoleArn).toBeUndefined(); + expect(stackArtifact.changesetRole).toBeUndefined(); }); test('qualifier is resolved in the synthesizer', () => { diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out index 1f0068d32659a..079dd58c72d69 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"36.0.0"} \ No newline at end of file +{"version":"37.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json index a6814ac222f55..24545051ba3c0 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json @@ -1,10 +1,11 @@ { - "version": "36.0.0", + "version": "37.0.0", "testCases": { "integ-tests/DefaultTest": { "stacks": [ "synthesize-default-resources", - "StagingStack-default-resourcesmax-ACCOUNT-REGION" + "StagingStack-default-resourcesmax-ACCOUNT-REGION", + "synthesize-custom-roles" ], "assertionStack": "integ-tests/DefaultTest/DeployAssert", "assertionStackName": "integtestsDefaultTestDeployAssert44C8D370" diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json index 50121024f8d99..650f1b1e5f1b3 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "37.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/manifest.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/manifest.json index e1f6dae8b56b0..14987abbc8819 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/manifest.json +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "37.0.0", "artifacts": { "synthesize-default-resources.assets": { "type": "cdk:asset-manifest", @@ -21,8 +21,11 @@ "additionalDependencies": [ "synthesize-default-resources.assets" ], - "stackTemplateAssetObjectUrl": "s3://cdk-default-resourcesmax-staging-${AWS::AccountId}-${AWS::Region}/deploy-time/60f7c2dac879a9de34a1b00340c5ad12c3ffd444e6c91049832d893f3631b8c4.json", + "stackTemplateAssetObjectUrl": "s3://cdk-default-resourcesmax-staging-${AWS::AccountId}-${AWS::Region}/deploy-time/f8f281289f79da2c3a0b6831ce6cb7c7f701b0083d846e3820a1847793b668d1.json", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "changesetRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-changeset-review-role-${AWS::AccountId}-${AWS::Region}" + }, "lookupRole": { "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}" } @@ -44,6 +47,18 @@ "data": "lambdas342CE2BBD" } ], + "/synthesize-default-resources/lambda-ecr-1/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdaecr1ServiceRoleA6BBC49F" + } + ], + "/synthesize-default-resources/lambda-ecr-1/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdaecr1B33A3D15" + } + ], "/synthesize-default-resources/lambda-ecr-1-copy/ServiceRole/Resource": [ { "type": "aws:cdk:logicalId", @@ -170,6 +185,41 @@ }, "displayName": "StagingStack-default-resourcesmax-ACCOUNT-REGION" }, + "synthesize-custom-roles.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "synthesize-custom-roles.assets.json" + }, + "dependencies": [ + "StagingStack-default-resourcesmax-ACCOUNT-REGION" + ] + }, + "synthesize-custom-roles": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "synthesize-custom-roles.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:aws:iam::123456789012:role/Execute", + "additionalDependencies": [ + "synthesize-custom-roles.assets" + ], + "stackTemplateAssetObjectUrl": "s3://cdk-default-resourcesmax-staging-${AWS::AccountId}-${AWS::Region}/deploy-time/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a.json", + "cloudFormationExecutionRoleArn": "arn:aws:iam::123456789012:role/Execute", + "changesetRole": { + "arn": "arn:aws:iam::123456789012:role/Execute" + }, + "lookupRole": { + "arn": "arn:aws:iam::123456789012:role/Execute" + } + }, + "dependencies": [ + "StagingStack-default-resourcesmax-ACCOUNT-REGION", + "synthesize-custom-roles.assets" + ], + "displayName": "synthesize-custom-roles" + }, "integtestsDefaultTestDeployAssert44C8D370.assets": { "type": "cdk:asset-manifest", "properties": { @@ -197,6 +247,11 @@ "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", "requiresBootstrapStackVersion": 8, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + }, + "changesetRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-changeset-review-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 21, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" } }, "dependencies": [ diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-custom-roles.assets.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-custom-roles.assets.json new file mode 100644 index 0000000000000..400c8663279b5 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-custom-roles.assets.json @@ -0,0 +1,19 @@ +{ + "version": "37.0.0", + "files": { + "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a": { + "source": { + "path": "synthesize-custom-roles.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-default-resourcesmax-staging-${AWS::AccountId}-${AWS::Region}", + "objectKey": "deploy-time/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-default-resourcesmax-file-role-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-custom-roles.template.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-custom-roles.template.json new file mode 100644 index 0000000000000..9e26dfeeb6e64 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-custom-roles.template.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.assets.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.assets.json index 9d7431524f2cc..0c9bf3e0aa942 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.assets.json +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.assets.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "37.0.0", "files": { "68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650": { "source": { @@ -14,7 +14,7 @@ } } }, - "60f7c2dac879a9de34a1b00340c5ad12c3ffd444e6c91049832d893f3631b8c4": { + "f8f281289f79da2c3a0b6831ce6cb7c7f701b0083d846e3820a1847793b668d1": { "source": { "path": "synthesize-default-resources.template.json", "packaging": "file" @@ -22,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-default-resourcesmax-staging-${AWS::AccountId}-${AWS::Region}", - "objectKey": "deploy-time/60f7c2dac879a9de34a1b00340c5ad12c3ffd444e6c91049832d893f3631b8c4.json", + "objectKey": "deploy-time/f8f281289f79da2c3a0b6831ce6cb7c7f701b0083d846e3820a1847793b668d1.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-default-resourcesmax-file-role-${AWS::Region}" } } diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.template.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.template.json index 3277a2e2b53c9..4cc2c33d957e2 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.template.json +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.template.json @@ -53,6 +53,57 @@ "lambdas3ServiceRoleC9EDE33A" ] }, + "lambdaecr1ServiceRoleA6BBC49F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "lambdaecr1B33A3D15": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ImageUri": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/default-resourcesmax/ecr-asset/1:16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + } + }, + "PackageType": "Image", + "Role": { + "Fn::GetAtt": [ + "lambdaecr1ServiceRoleA6BBC49F", + "Arn" + ] + } + }, + "DependsOn": [ + "lambdaecr1ServiceRoleA6BBC49F" + ] + }, "lambdaecr1copyServiceRole2A9FAF5F": { "Type": "AWS::IAM::Role", "Properties": { diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/tree.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/tree.json index 8161eea617b54..fd5a1512778a5 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/tree.json +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/tree.json @@ -136,6 +136,124 @@ "version": "0.0.0" } }, + "lambda-ecr-1": { + "id": "lambda-ecr-1", + "path": "synthesize-default-resources/lambda-ecr-1", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "synthesize-default-resources/lambda-ecr-1/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "synthesize-default-resources/lambda-ecr-1/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-ecr-1/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "AssetImage": { + "id": "AssetImage", + "path": "synthesize-default-resources/lambda-ecr-1/AssetImage", + "children": { + "Staging": { + "id": "Staging", + "path": "synthesize-default-resources/lambda-ecr-1/AssetImage/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "synthesize-default-resources/lambda-ecr-1/AssetImage/Repository", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr.RepositoryBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr_assets.DockerImageAsset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-ecr-1/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "imageUri": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/default-resourcesmax/ecr-asset/1:16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + } + }, + "packageType": "Image", + "role": { + "Fn::GetAtt": [ + "lambdaecr1ServiceRoleA6BBC49F", + "Arn" + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.Function", + "version": "0.0.0" + } + }, "lambda-ecr-1-copy": { "id": "lambda-ecr-1-copy", "path": "synthesize-default-resources/lambda-ecr-1-copy", @@ -1145,6 +1263,14 @@ "version": "0.0.0" } }, + "synthesize-custom-roles": { + "id": "synthesize-custom-roles", + "path": "synthesize-custom-roles", + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, "integ-tests": { "id": "integ-tests", "path": "integ-tests", diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts index 5e21a52cf1464..8cfb764bc27bb 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts @@ -4,10 +4,10 @@ import { App, Stack } from 'aws-cdk-lib'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; import { APP_ID_MAX } from './util'; -import { AppStagingSynthesizer } from '../lib'; +import { AppStagingSynthesizer, BootstrapRole, DeploymentIdentities } from '../lib'; // IMAGE_COPIES env variable is used to test maximum number of ECR repositories allowed. -const IMAGE_COPIES = Number(process.env.IMAGE_COPIES) ?? 1; +const IMAGE_COPIES = Number(process.env.IMAGE_COPIES) || 1; const app = new App({ context: { @@ -61,8 +61,21 @@ if (!defaultStagingStack) { throw new Error('Default Staging Stack not found'); } +const customRolesStack = new Stack(app, 'synthesize-custom-roles', { + synthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID_MAX, + stagingBucketEncryption: BucketEncryption.KMS, + deploymentIdentities: DeploymentIdentities.specifyRoles({ + cloudFormationExecutionRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Execute'), + deploymentRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Execute'), + lookupRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Execute'), + changesetRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Execute'), + }), + }), +}); + new integ.IntegTest(app, 'integ-tests', { - testCases: [stack, defaultStagingStack], + testCases: [stack, defaultStagingStack, customRolesStack], }); app.synth(); diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/util.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/util.ts index e624f371d6ca4..bb1117385c60e 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/util.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/util.ts @@ -18,12 +18,11 @@ export function last(xs?: A[]): A | undefined { } export function testWithXRepos(x: number): boolean { - // set env variable - process.env.IMAGE_COPIES = String(x); - // execute cdk synth requesting 'copies' number of ecr repos try { - execSync("npx cdk synth --app='node test/integ.synth-default-resources.js' --all", { + // Envs passed to the child process must be passed directly if it is executed using jest + // see https://github.com/jestjs/jest/issues/9264 + execSync(`IMAGE_COPIES=${String(x)} npx cdk synth --app='node test/integ.synth-default-resources.js' --all`, { stdio: 'pipe', }); } catch (error: any) { @@ -34,4 +33,4 @@ export function testWithXRepos(x: number): boolean { throw error; } return true; -} \ No newline at end of file +} diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/default-synthesizer.ts index bdbbdc21bfa47..62644bbbb7930 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/default-synthesizer.ts @@ -29,7 +29,7 @@ const MIN_LOOKUP_ROLE_BOOTSTRAP_STACK_VERSION = 8; * * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_permissions-required */ -const MIN_SESSION_TAGS_BOOTSTRAP_STACK_VERSION = 22; +const MIN_SESSION_TAGS_BOOTSTRAP_STACK_VERSION = 23; /** * Configuration properties for DefaultStackSynthesizer @@ -160,6 +160,13 @@ export interface DefaultStackSynthesizerProps { */ readonly cloudFormationExecutionRole?: string; + /** + * The role to use to perform operations on the changeset other than execution + * + * @default - None + */ + readonly changesetRoleArn?: string; + /** * Name of the CloudFormation Export with the asset key name * @@ -315,6 +322,11 @@ export class DefaultStackSynthesizer extends StackSynthesizer implements IReusab */ public static readonly DEFAULT_LOOKUP_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region}'; + /** + * Default changeset role ARN for missing values. + */ + public static readonly DEFAULT_CHANGESET_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-cfn-changeset-review-role-${AWS::AccountId}-${AWS::Region}'; + /** * Default image assets repository name */ @@ -351,6 +363,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer implements IReusab private fileAssetPublishingRoleArn?: string; private imageAssetPublishingRoleArn?: string; private lookupRoleArn?: string; + private changesetRoleArn?: string; private useLookupRoleForStackOperations: boolean; private qualifier?: string; private bucketPrefix?: string; @@ -423,6 +436,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer implements IReusab this.fileAssetPublishingRoleArn = spec.specialize(this.props.fileAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN); this.imageAssetPublishingRoleArn = spec.specialize(this.props.imageAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN); this.lookupRoleArn = spec.specialize(this.props.lookupRoleArn ?? DefaultStackSynthesizer.DEFAULT_LOOKUP_ROLE_ARN); + this.changesetRoleArn = spec.specialize(this.props.changesetRoleArn ?? DefaultStackSynthesizer.DEFAULT_CHANGESET_ROLE_ARN); this.bucketPrefix = spec.specialize(this.props.bucketPrefix ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PREFIX); this.dockerTagPrefix = spec.specialize(this.props.dockerTagPrefix ?? DefaultStackSynthesizer.DEFAULT_DOCKER_ASSET_PREFIX); this.bootstrapStackVersionSsmParameter = spec.qualifierOnly(this.props.bootstrapStackVersionSsmParameter ?? DefaultStackSynthesizer.DEFAULT_BOOTSTRAP_STACK_VERSION_SSM_PARAMETER); @@ -515,6 +529,11 @@ export class DefaultStackSynthesizer extends StackSynthesizer implements IReusab requiresBootstrapStackVersion: this._requiredBootstrapVersionForLookup, bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter, } : undefined, + changesetRole: this.changesetRoleArn ? { + arn: this.changesetRoleArn, + requiresBootstrapStackVersion: MIN_CHANGESET_ROLE_BOOTSTRAP_STACK_VERSION, + bootstrapStackVersionSsmParameter: this.bootstrapStackVersionSsmParameter, + } : undefined, }); } diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/stack-synthesizer.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/stack-synthesizer.ts index 4d4d01d0af039..3291f87c87b38 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/stack-synthesizer.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/stack-synthesizer.ts @@ -269,6 +269,13 @@ export interface SynthesizeStackArtifactOptions { */ readonly lookupRole?: cxschema.BootstrapRole; + /** + * The role that needs to be assumed to perform operations on the changeset other than execution + * + * @default - None + */ + readonly changesetRole?: cxschema.BootstrapRole; + /** * If the stack template has already been included in the asset manifest, its asset URL * diff --git a/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts b/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts index 8fcea525b4907..8c355805a387e 100644 --- a/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts +++ b/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -115,6 +115,13 @@ export class CloudFormationStackArtifact extends CloudArtifact { */ public readonly cloudFormationExecutionRoleArn?: string; + /** + * The role that needs to be assumed to perform operations on the changeset other than execution + * + * @default - No role is assumed (current credentials are used) + */ + readonly changesetRole?: cxschema.BootstrapRole; + /** * The role to use to look up values from the target AWS account * @@ -185,6 +192,7 @@ export class CloudFormationStackArtifact extends CloudArtifact { this.terminationProtection = properties.terminationProtection; this.validateOnSynth = properties.validateOnSynth; this.lookupRole = properties.lookupRole; + this.changesetRole = properties.changesetRole; this.stackName = properties.stackName || artifactId; this.assets = this.findMetadataByType(cxschema.ArtifactMetadataEntryType.ASSET).map(e => e.data as cxschema.AssetMetadataEntry); diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index 8ed4bb8595446..ae21752db5af0 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -588,6 +588,58 @@ Resources: - PermissionsBoundarySet - Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}' - Ref: AWS::NoValue + CloudFormationChangeSetReviewRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + Version: '2012-10-17' + Policies: + - PolicyDocument: + Statement: + - Sid: CloudFormationPermissions + Effect: Allow + Action: + - cloudformation:CreateChangeSet + - cloudformation:DeleteChangeSet + - cloudformation:DescribeChangeSet + - cloudformation:DescribeStacks + Resource: "*" + - Sid: UploadAssets + Effect: Allow + Action: + - s3:PutObject* + - s3:GetObject* # cfn needs to download the template from the bucket + Resource: + - Fn::Sub: "${StagingBucket.Arn}/cdk/*" # upload assets to the cdk folder + - Sid: KmsPermission + Effect: Allow + Action: + - kms:GenerateDataKey* + Resource: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} + PolicyName: default + RoleName: + Fn::Sub: cdk-${Qualifier}-cfn-changeset-review-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: changeset CdkBoostrapPermissionsBoundaryPolicy: # Edit the template prior to boostrap in order to have this example policy created Condition: ShouldCreatePermissionsBoundary @@ -651,7 +703,7 @@ Resources: Type: String Name: Fn::Sub: '/cdk-bootstrap/${Qualifier}/version' - Value: '22' + Value: '23' Outputs: BucketName: Description: The name of the S3 bucket owned by the CDK toolkit stack diff --git a/packages/aws-cdk/lib/api/deployments.ts b/packages/aws-cdk/lib/api/deployments.ts index 285dba5d29114..bc6df6de59cb6 100644 --- a/packages/aws-cdk/lib/api/deployments.ts +++ b/packages/aws-cdk/lib/api/deployments.ts @@ -48,6 +48,36 @@ export interface PreparedSdkWithLookupRoleForEnvironment { readonly envResources: EnvironmentResources; } +/** + * SDK obtained by assuming the changeset role + * for a given environment + */ +export interface PreparedSdkWithChangesetRoleForEnvironment { + /** + * The SDK for the given environment + */ + readonly sdk: ISDK; + + /** + * The resolved environment for the stack + * (no more 'unknown-account/unknown-region') + */ + readonly resolvedEnvironment: cxapi.Environment; + + /** + * Whether or not the assume role was successful. + * If the assume role was not successful (false) + * then that means that the 'sdk' returned contains + * the default credentials (not the assume role credentials) + */ + readonly didAssumeRole: boolean; + + /** + * An object for accessing the bootstrap resources in this environment + */ + readonly envResources: EnvironmentResources; +} + export interface DeployStackOptions { /** * Stack to deploy @@ -443,8 +473,18 @@ export class Deployments { return stack.exists; } - public async prepareSdkWithDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise { - return this.prepareSdkFor(stackArtifact, undefined, Mode.ForWriting); + public async prepareSdkWithChangesetOrDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise { + // try to assume the lookup role + const result = await this.prepareSdkWithChangesetRoleFor(stackArtifact); + if (result.didAssumeRole) { + return { + resolvedEnvironment: result.resolvedEnvironment, + stackSdk: result.sdk, + envResources: result.envResources, + }; + } + // fall back to the deploy role + return this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading); } private async prepareSdkWithLookupOrDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise { @@ -574,6 +614,41 @@ export class Deployments { } } + /** + * Try to use the bootstrap changesetRole. + * There's a case where the changesetRole may not exist (it was added in bootstrap stack version 21). + * + * if the changesetRole does not exist, `cachedSdkForEnvironment` will either: + * 1. Return the default credentials if the default credentials are for the stack account + * 2. Throw an error if the default credentials are not for the stack account. + * + * If we do not successfully assume the changesetRole, but do get back the default credentials + * then return those and note that we are returning the default credentials. The calling + * function can then decide to use them or fallback to another role. + */ + public async prepareSdkWithChangesetRoleFor( + stack: cxapi.CloudFormationStackArtifact, + ): Promise { + const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(stack.environment); + + // Substitute any placeholders with information about the current environment + const arns = await replaceEnvPlaceholders({ + changesetRoleArn: stack.changesetRole?.arn, + }, resolvedEnvironment, this.sdkProvider); + + // Trying to assume changeset role and cache the sdk for the environment + const stackSdk = await this.cachedSdkForEnvironment(resolvedEnvironment, Mode.ForWriting, { + assumeRoleArn: arns.changesetRoleArn, + }); + + const envResources = this.environmentResources.for(resolvedEnvironment, stackSdk.sdk); + + if (!stackSdk.didAssumeRole) { + warning(`changeset role ${ stack.changesetRole ? 'exists but' : 'does not exist, hence'} was not assumed. Proceeding with default credentials.`); + } + return { ...stackSdk, resolvedEnvironment, envResources }; + } + private async prepareAndValidateAssets(asset: cxapi.AssetManifestArtifact, options: AssetOptions) { const { envResources } = await this.prepareSdkFor(options.stack, options.roleArn, Mode.ForWriting); diff --git a/packages/aws-cdk/lib/api/util/cloudformation.ts b/packages/aws-cdk/lib/api/util/cloudformation.ts index 27822ca4c8378..9e8eec7f34e13 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation.ts @@ -381,7 +381,7 @@ async function uploadBodyParameterAndCreateChangeSet(options: PrepareChangeSetOp }); } } - const preparedSdk = (await options.deployments.prepareSdkWithDeployRole(options.stack)); + const preparedSdk = (await options.deployments.prepareSdkWithChangesetOrDeployRole(options.stack)); const bodyParameter = await makeBodyParameter( options.stack, @@ -393,7 +393,6 @@ async function uploadBodyParameterAndCreateChangeSet(options: PrepareChangeSetOp const cfn = preparedSdk.stackSdk.cloudFormation(); const exists = (await CloudFormationStack.lookup(cfn, options.stack.stackName, false)).exists; - const executionRoleArn = preparedSdk.cloudFormationRoleArn; options.stream.write('Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)\n'); return await createChangeSet({ @@ -406,7 +405,6 @@ async function uploadBodyParameterAndCreateChangeSet(options: PrepareChangeSetOp bodyParameter, parameters: options.parameters, resourcesToImport: options.resourcesToImport, - role: executionRoleArn, }); } catch (e: any) { debug(e.message); @@ -434,7 +432,6 @@ async function createChangeSet(options: CreateChangeSetOptions): Promise { // THEN expect(exitCode).toBe(0); expect(cloudFormation.stackExists).not.toHaveBeenCalled(); + expect(cfn.createDiffChangeSet).not.toHaveBeenCalled(); }); test('diff falls back to classic diff when stack does not exist', async () => {