From 2bb8362224dc242692fd2bb75164041d19ad4809 Mon Sep 17 00:00:00 2001 From: spensireli Date: Thu, 23 Jul 2026 19:58:06 -0400 Subject: [PATCH] feat: support GovCloud and other partitions via partition-aware ARNs Replace hardcoded 'arn:aws:' / 'amazonaws.com' with Stack.formatArn and Stack.urlSuffix so self-managed StackSets resolve the correct partition (e.g. aws-us-gov) at deployment time. Also fix asset staging in StackSetStackSynthesizer: delegate staging to the parent stack's synthesizer and reference the staged object via Source.bucket() instead of Source.asset() on a not-yet-existing local path, and derive the per-region asset bucket name from Fn.ref('AWS::Region') so it resolves in each target region at deploy time. Add aws-us-gov to the integ test target-partitions, regenerate the integ snapshot, and add unit tests covering the partition-aware ARN / URL suffix. Co-Authored-By: Claude Opus 4.5 --- src/stackset-stack.ts | 32 +- src/stackset.ts | 11 +- test/integ.stack-set.ts | 2 +- .../index.js | 3 - .../integ-stackset-asset-test.assets.json | 26 +- .../integ-stackset-asset-test.template.json | 526 +++++++++--------- .../integ-stackset-test.assets.json | 4 +- .../integ-stackset-test.template.json | 13 +- test/integ.stack-set.ts.snapshot/integ.json | 7 +- ...setstacksetB1BE16AD.stackset.template.json | 12 +- .../integ.stack-set.ts.snapshot/manifest.json | 18 +- test/integ.stack-set.ts.snapshot/tree.json | 89 +-- test/stack-set.test.ts | 148 ++++- 13 files changed, 542 insertions(+), 349 deletions(-) delete mode 100644 test/integ.stack-set.ts.snapshot/asset.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690/index.js diff --git a/src/stackset-stack.ts b/src/stackset-stack.ts index e68b209..f341560 100644 --- a/src/stackset-stack.ts +++ b/src/stackset-stack.ts @@ -12,12 +12,11 @@ import { Names, Lazy, FileAssetPackaging, - App, Resource, Annotations, Fn, } from 'aws-cdk-lib'; -import { IBucket } from 'aws-cdk-lib/aws-s3'; +import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3'; import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; import { Construct } from 'constructs'; @@ -52,6 +51,7 @@ export class StackSetStackSynthesizer extends StackSynthesizer { */ readonly assetBucketPrefix?: string; private bucketDeployments: { [key: string]: AssetBucketDeploymentProperties }; + private parentAssetBucket?: IBucket; /** * Creates a new StackSetStackSynthesizer. @@ -82,16 +82,28 @@ export class StackSetStackSynthesizer extends StackSynthesizer { throw new Error('Asset filename is undefined'); } - const outdir = App.of(this.boundStack)?.outdir ?? 'cdk.out'; - const assetPath = `${outdir}/${asset.fileName}`; + const parentStack = (this.boundStack as StackSetStack)._getParentStack(); + + // Delegate to the parent stack's synthesizer to handle asset staging + const parentLocation = parentStack.synthesizer.addFileAsset(asset); + + // Create a reference to the parent's asset bucket (lazily, once) + if (!this.parentAssetBucket) { + this.parentAssetBucket = Bucket.fromBucketName( + this.boundStack, + 'ParentAssetBucket', + parentLocation.bucketName, + ); + } + + // Use Source.bucket() to reference the asset from S3 (avoids timing issue with local files) + const source = Source.bucket(this.parentAssetBucket, parentLocation.objectKey); for (const assetBucket of this.assetBuckets) { const index = this.assetBuckets.indexOf(assetBucket); const assetDeployment = this.bucketDeployments[assetBucket.bucketName]; if (!assetDeployment.bucketDeployment) { - const parentStack = (this.boundStack as StackSetStack)._getParentStack(); - if (!Resource.isOwnedResource(assetDeployment.assetBucket)) { Annotations.of(parentStack).addWarning('[WARNING] Bucket Policy Permissions cannot be added to' + ' referenced Bucket. Please make sure your bucket has the correct permissions'); @@ -105,7 +117,7 @@ export class StackSetStackSynthesizer extends StackSynthesizer { parentStack, bucketDeploymentConstructName, { - sources: [Source.asset(assetPath)], + sources: [source], destinationBucket: assetDeployment.assetBucket, extract: false, prune: false, @@ -114,11 +126,13 @@ export class StackSetStackSynthesizer extends StackSynthesizer { assetDeployment.bucketDeployment = bucketDeployment; } else { - assetDeployment.bucketDeployment.addSource(Source.asset(assetPath)); + assetDeployment.bucketDeployment.addSource(source); } } - const bucketName = Fn.join('-', [this.assetBucketPrefix, this.boundStack.region]); + // Use Fn.ref('AWS::Region') so the bucket name resolves dynamically at deployment time + // in each target region, not at synthesis time (which would hardcode the parent stack's region) + const bucketName = Fn.join('-', [this.assetBucketPrefix, Fn.ref('AWS::Region')]); const assetFileBaseName = path.basename(asset.fileName); const s3Filename = assetFileBaseName.split('.')[1] + '.zip'; diff --git a/src/stackset.ts b/src/stackset.ts index 7a72495..03ec23b 100644 --- a/src/stackset.ts +++ b/src/stackset.ts @@ -5,6 +5,7 @@ import { Lazy, Names, Resource, + Stack, } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { StackSetStack, fileAssetResourceNames } from './stackset-stack'; @@ -696,7 +697,13 @@ export class StackSet extends Resource implements IStackSet { effect: iam.Effect.ALLOW, actions: ['sts:AssumeRole'], resources: [ - `arn:aws:iam::*:role/${deploymentTypeConfig.executionRoleName ?? 'AWSCloudFormationStackSetExecutionRole'}`, + Stack.of(this).formatArn({ + service: 'iam', + region: '', + account: '*', + resource: 'role', + resourceName: deploymentTypeConfig.executionRoleName ?? 'AWSCloudFormationStackSetExecutionRole', + }), ], })); } @@ -768,7 +775,7 @@ export class StackSet extends Resource implements IStackSet { const disabledPrincipals: iam.IPrincipal[] = []; targetConfig.regions.forEach(region => { if (!ENABLED_REGIONS.includes(region)) { - disabledPrincipals.push(new iam.ServicePrincipal(`cloudformation.${region}.amazonaws.com`)); + disabledPrincipals.push(new iam.ServicePrincipal(`cloudformation.${region}.${Stack.of(this).urlSuffix}`)); } }); if (disabledPrincipals.length > 0) { diff --git a/test/integ.stack-set.ts b/test/integ.stack-set.ts index b6e9af9..35e5ad2 100644 --- a/test/integ.stack-set.ts +++ b/test/integ.stack-set.ts @@ -37,7 +37,7 @@ const app = new App({ postCliContext: { // I don't know why this is needed, but If I don't have it I get // an error about undefined not being a list - '@aws-cdk/core:target-partitions': ['aws'], + '@aws-cdk/core:target-partitions': ['aws', 'aws-us-gov'], }, }); diff --git a/test/integ.stack-set.ts.snapshot/asset.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690/index.js b/test/integ.stack-set.ts.snapshot/asset.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690/index.js deleted file mode 100644 index 0090ced..0000000 --- a/test/integ.stack-set.ts.snapshot/asset.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export async function handler(event) { - return event; -} diff --git a/test/integ.stack-set.ts.snapshot/integ-stackset-asset-test.assets.json b/test/integ.stack-set.ts.snapshot/integ-stackset-asset-test.assets.json index 16a5773..6745f94 100644 --- a/test/integ.stack-set.ts.snapshot/integ-stackset-asset-test.assets.json +++ b/test/integ.stack-set.ts.snapshot/integ-stackset-asset-test.assets.json @@ -1,41 +1,41 @@ { "version": "35.0.0", "files": { - "3fb6287214999ddeafa7cd0e3e58bc5144c8678bb720f3b5e45e8fd32f333eb3": { + "e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690": { "source": { - "path": "asset.3fb6287214999ddeafa7cd0e3e58bc5144c8678bb720f3b5e45e8fd32f333eb3.zip", - "packaging": "file" + "path": "asset.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690", + "packaging": "zip" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "3fb6287214999ddeafa7cd0e3e58bc5144c8678bb720f3b5e45e8fd32f333eb3.zip", + "objectKey": "e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } }, - "9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd": { + "3fb6287214999ddeafa7cd0e3e58bc5144c8678bb720f3b5e45e8fd32f333eb3": { "source": { - "path": "asset.9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd", - "packaging": "zip" + "path": "asset.3fb6287214999ddeafa7cd0e3e58bc5144c8678bb720f3b5e45e8fd32f333eb3.zip", + "packaging": "file" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd.zip", + "objectKey": "3fb6287214999ddeafa7cd0e3e58bc5144c8678bb720f3b5e45e8fd32f333eb3.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } }, - "e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690": { + "9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd": { "source": { - "path": "asset.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690", + "path": "asset.9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd", "packaging": "zip" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.zip", + "objectKey": "9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } @@ -53,7 +53,7 @@ } } }, - "a042f98f25fe2c784f1eb1f27c3963138d49d3e1913feb936f1a12f20a62349e": { + "304cac1154e09b2add5e34bdfc7918f471e54461f7bb556588868eded3c09498": { "source": { "path": "integ-stackset-asset-test.template.json", "packaging": "file" @@ -61,7 +61,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "a042f98f25fe2c784f1eb1f27c3963138d49d3e1913feb936f1a12f20a62349e.json", + "objectKey": "304cac1154e09b2add5e34bdfc7918f471e54461f7bb556588868eded3c09498.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/test/integ.stack-set.ts.snapshot/integ-stackset-asset-test.template.json b/test/integ.stack-set.ts.snapshot/integ-stackset-asset-test.template.json index 8412cc1..7b696cc 100644 --- a/test/integ.stack-set.ts.snapshot/integ-stackset-asset-test.template.json +++ b/test/integ.stack-set.ts.snapshot/integ-stackset-asset-test.template.json @@ -1,272 +1,272 @@ { - "Resources": { - "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0AwsCliLayerC5715512": { - "Type": "AWS::Lambda::LayerVersion", - "Properties": { - "Content": { - "S3Bucket": { - "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" - }, - "S3Key": "3fb6287214999ddeafa7cd0e3e58bc5144c8678bb720f3b5e45e8fd32f333eb3.zip" - }, - "Description": "/opt/awscli/aws" - } - }, - "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0CustomResource95864C0F": { - "Type": "Custom::CDKBucketDeployment", - "Properties": { - "ServiceToken": { - "Fn::GetAtt": [ - "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", - "Arn" - ] - }, - "SourceBucketNames": [ - { - "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" - } - ], - "SourceObjectKeys": [ - "e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.zip" - ], - "DestinationBucketName": "integ-assets", - "Extract": false, - "Prune": false - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" - }, - "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { - "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" - ] - ] - } - ] - } + "Resources": { + "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0AwsCliLayerC5715512": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "3fb6287214999ddeafa7cd0e3e58bc5144c8678bb720f3b5e45e8fd32f333eb3.zip" + }, + "Description": "/opt/awscli/aws" + } + }, + "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0CustomResource95864C0F": { + "Type": "Custom::CDKBucketDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", + "Arn" + ] + }, + "SourceBucketNames": [ + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ], + "SourceObjectKeys": [ + "e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.zip" + ], + "DestinationBucketName": "integ-assets", + "Extract": false, + "Prune": false + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { + "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" }, - "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "s3:GetBucket*", - "s3:GetObject*", - "s3:List*" - ], - "Effect": "Allow", - "Resource": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":s3:::", - { - "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" - }, - "/*" - ] - ] - }, - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":s3:::", - { - "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" - } - ] - ] - } - ] - }, - { - "Action": [ - "s3:Abort*", - "s3:DeleteObject*", - "s3:GetBucket*", - "s3:GetObject*", - "s3:List*", - "s3:PutObject", - "s3:PutObjectLegalHold", - "s3:PutObjectRetention", - "s3:PutObjectTagging", - "s3:PutObjectVersionTagging" - ], - "Effect": "Allow", - "Resource": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":s3:::integ-assets" - ] - ] - }, - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":s3:::integ-assets/*" - ] - ] - } - ] - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", - "Roles": [ - { - "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" - } - ] - } + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] }, - "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "S3Bucket": { - "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" - }, - "S3Key": "9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd.zip" - }, - "Environment": { - "Variables": { - "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" - } - }, - "Handler": "index.handler", - "Layers": [ - { - "Ref": "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0AwsCliLayerC5715512" - } - ], - "Role": { - "Fn::GetAtt": [ - "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", - "Arn" - ] - }, - "Runtime": "python3.9", - "Timeout": 900 - }, - "DependsOn": [ - "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", - "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" - ] + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + }, + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::integ-assets" + ] + ] }, - "StackSet6E6355CF": { - "Type": "AWS::CloudFormation::StackSet", - "Properties": { - "AutoDeployment": { - "Enabled": true, - "RetainStacksOnAccountRemoval": true - }, - "CallAs": "SELF", - "ManagedExecution": { - "Active": true - }, - "PermissionModel": "SERVICE_MANAGED", - "StackInstancesGroup": [ - { - "DeploymentTargets": { - "AccountFilterType": "INTERSECTION", - "Accounts": [ - "12345678" - ] - }, - "Regions": [ - "us-east-1" - ] - } - ], - "StackSetName": "integstacksetassettestStackSet091EC131", - "TemplateURL": { - "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/8a79245976356195e252a35c4adeb67d13403b4aa4797878ffeb1cbdbf6b1e10.json" - } - }, - "DependsOn": [ - "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0AwsCliLayerC5715512", - "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0CustomResource95864C0F" - ] + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::integ-assets/*" + ] + ] } + ] + } + ], + "Version": "2012-10-17" }, - "Parameters": { - "BootstrapVersion": { - "Type": "AWS::SSM::Parameter::Value", - "Default": "/cdk-bootstrap/hnb659fds/version", - "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" - } + "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "Roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "9eb41a5505d37607ac419321497a4f8c21cf0ee1f9b4a6b29aa04301aea5c7fd.zip" }, - "Rules": { - "CheckBootstrapVersion": { - "Assertions": [ - { - "Assert": { - "Fn::Not": [ - { - "Fn::Contains": [ - [ - "1", - "2", - "3", - "4", - "5" - ], - { - "Ref": "BootstrapVersion" - } - ] - } - ] - }, - "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." - } - ] - } + "Environment": { + "Variables": { + "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" + } + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0AwsCliLayerC5715512" + } + ], + "Role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn" + ] + }, + "Runtime": "python3.9", + "Timeout": 900 + }, + "DependsOn": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + ] + }, + "StackSet6E6355CF": { + "Type": "AWS::CloudFormation::StackSet", + "Properties": { + "AutoDeployment": { + "Enabled": true, + "RetainStacksOnAccountRemoval": true + }, + "CallAs": "SELF", + "ManagedExecution": { + "Active": true + }, + "PermissionModel": "SERVICE_MANAGED", + "StackInstancesGroup": [ + { + "DeploymentTargets": { + "AccountFilterType": "INTERSECTION", + "Accounts": [ + "12345678" + ] + }, + "Regions": [ + "us-east-1" + ] + } + ], + "StackSetName": "integstacksetassettestStackSet091EC131", + "TemplateURL": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/8a79245976356195e252a35c4adeb67d13403b4aa4797878ffeb1cbdbf6b1e10.json" + } + }, + "DependsOn": [ + "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0AwsCliLayerC5715512", + "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0CustomResource95864C0F" + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." } + ] + } + } } \ No newline at end of file diff --git a/test/integ.stack-set.ts.snapshot/integ-stackset-test.assets.json b/test/integ.stack-set.ts.snapshot/integ-stackset-test.assets.json index 06ed293..10e4e91 100644 --- a/test/integ.stack-set.ts.snapshot/integ-stackset-test.assets.json +++ b/test/integ.stack-set.ts.snapshot/integ-stackset-test.assets.json @@ -15,7 +15,7 @@ } } }, - "3a5af893feef3141770218b887957ce48f09d2c38f224496b6b5ac38d26a1d92": { + "086d2724faea854d118e9eed97164046f16b68bc3ba34294b040cc53360a9ead": { "source": { "path": "integ-stackset-test.template.json", "packaging": "file" @@ -23,7 +23,7 @@ "destinations": { "12345678-test-region": { "bucketName": "cdk-hnb659fds-assets-12345678-test-region", - "objectKey": "3a5af893feef3141770218b887957ce48f09d2c38f224496b6b5ac38d26a1d92.json", + "objectKey": "086d2724faea854d118e9eed97164046f16b68bc3ba34294b040cc53360a9ead.json", "region": "test-region", "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-file-publishing-role-12345678-test-region" } diff --git a/test/integ.stack-set.ts.snapshot/integ-stackset-test.template.json b/test/integ.stack-set.ts.snapshot/integ-stackset-test.template.json index 48ba4a8..e5cb9f1 100644 --- a/test/integ.stack-set.ts.snapshot/integ-stackset-test.template.json +++ b/test/integ.stack-set.ts.snapshot/integ-stackset-test.template.json @@ -41,7 +41,18 @@ { "Action": "sts:AssumeRole", "Effect": "Allow", - "Resource": "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole-integ-test" + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::*:role/AWSCloudFormationStackSetExecutionRole-integ-test" + ] + ] + } } ], "Version": "2012-10-17" diff --git a/test/integ.stack-set.ts.snapshot/integ.json b/test/integ.stack-set.ts.snapshot/integ.json index 80bcd03..7378190 100644 --- a/test/integ.stack-set.ts.snapshot/integ.json +++ b/test/integ.stack-set.ts.snapshot/integ.json @@ -1,6 +1,5 @@ { - "enableLookups": true, - "version": "35.0.0", + "version": "54.0.0", "testCases": { "integ-test/DefaultTest": { "stacks": [ @@ -10,5 +9,7 @@ "assertionStack": "integ-test/DefaultTest/DeployAssert", "assertionStackName": "integtestDefaultTestDeployAssert24D5C536" } - } + }, + "enableLookups": true, + "minimumCliVersion": "2.1132.0" } \ No newline at end of file diff --git a/test/integ.stack-set.ts.snapshot/integstacksetassettestassetstacksetB1BE16AD.stackset.template.json b/test/integ.stack-set.ts.snapshot/integstacksetassettestassetstacksetB1BE16AD.stackset.template.json index f9c0460..1c8bad5 100644 --- a/test/integ.stack-set.ts.snapshot/integstacksetassettestassetstacksetB1BE16AD.stackset.template.json +++ b/test/integ.stack-set.ts.snapshot/integstacksetassettestassetstacksetB1BE16AD.stackset.template.json @@ -35,7 +35,17 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "S3Bucket": "integ-assets", + "S3Bucket": { + "Fn::Join": [ + "-", + [ + "asset-bucket", + { + "Ref": "AWS::Region" + } + ] + ] + }, "S3Key": "e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.zip" }, "Handler": "index.handler", diff --git a/test/integ.stack-set.ts.snapshot/manifest.json b/test/integ.stack-set.ts.snapshot/manifest.json index 9993de9..e3fff0b 100644 --- a/test/integ.stack-set.ts.snapshot/manifest.json +++ b/test/integ.stack-set.ts.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "35.0.0", + "version": "54.0.0", "artifacts": { "integ-stack-set-support.assets": { "type": "cdk:asset-manifest", @@ -72,7 +72,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-12345678-test-region/3a5af893feef3141770218b887957ce48f09d2c38f224496b6b5ac38d26a1d92.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-12345678-test-region/086d2724faea854d118e9eed97164046f16b68bc3ba34294b040cc53360a9ead.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -139,7 +139,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/a042f98f25fe2c784f1eb1f27c3963138d49d3e1913feb936f1a12f20a62349e.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/304cac1154e09b2add5e34bdfc7918f471e54461f7bb556588868eded3c09498.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -161,13 +161,16 @@ "data": "[WARNING] Bucket Policy Permissions cannot be added to referenced Bucket. Please make sure your bucket has the correct permissions" } ], - "/integ-stackset-asset-test/StackSetAssetsBucketDeployment/AwsCliLayer/Resource": [ + "/integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/AwsCliLayer/Resource": [ { "type": "aws:cdk:logicalId", - "data": "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0AwsCliLayerC5715512" + "data": "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0AwsCliLayerC5715512", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_REPLACE" + ] } ], - "/integ-stackset-asset-test/StackSetAssetsBucketDeployment/CustomResource/Default": [ + "/integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/CustomResource/Default": [ { "type": "aws:cdk:logicalId", "data": "integstacksetassettestassetstacksetB1BE16ADAssetBucketDeployment0CustomResource95864C0F" @@ -266,5 +269,6 @@ "file": "tree.json" } } - } + }, + "minimumCliVersion": "2.1132.0" } \ No newline at end of file diff --git a/test/integ.stack-set.ts.snapshot/tree.json b/test/integ.stack-set.ts.snapshot/tree.json index ec67d19..a4e298d 100644 --- a/test/integ.stack-set.ts.snapshot/tree.json +++ b/test/integ.stack-set.ts.snapshot/tree.json @@ -160,7 +160,18 @@ { "Action": "sts:AssumeRole", "Effect": "Allow", - "Resource": "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole-integ-test" + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::*:role/AWSCloudFormationStackSetExecutionRole-integ-test" + ] + ] + } } ], "Version": "2012-10-17" @@ -404,7 +415,17 @@ "aws:cdk:cloudformation:type": "AWS::Lambda::Function", "aws:cdk:cloudformation:props": { "code": { - "s3Bucket": "integ-assets", + "s3Bucket": { + "Fn::Join": [ + "-", + [ + "asset-bucket", + { + "Ref": "AWS::Region" + } + ] + ] + }, "s3Key": "e56263bd51a9cda3a5920a2b978d8827ae857776a6807cbe4ac9b2115dfed690.zip" }, "handler": "index.handler", @@ -427,6 +448,14 @@ "fqn": "aws-cdk-lib.aws_lambda.Function", "version": "2.108.0" } + }, + "ParentAssetBucket": { + "id": "ParentAssetBucket", + "path": "integ-stackset-asset-test/asset-stack-set/ParentAssetBucket", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketBase", + "version": "2.108.0" + } } }, "constructInfo": { @@ -434,21 +463,21 @@ "version": "2.108.0" } }, - "StackSetAssetsBucketDeployment0": { - "id": "StackSetAssetsBucketDeployment", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment", + "integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0": { + "id": "integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0", "children": { "AwsCliLayer": { "id": "AwsCliLayer", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/AwsCliLayer", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/AwsCliLayer", "children": { "Code": { "id": "Code", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/AwsCliLayer/Code", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/AwsCliLayer/Code", "children": { "Stage": { "id": "Stage", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/AwsCliLayer/Code/Stage", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/AwsCliLayer/Code/Stage", "constructInfo": { "fqn": "aws-cdk-lib.AssetStaging", "version": "2.108.0" @@ -456,7 +485,7 @@ }, "AssetBucket": { "id": "AssetBucket", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/AwsCliLayer/Code/AssetBucket", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/AwsCliLayer/Code/AssetBucket", "constructInfo": { "fqn": "aws-cdk-lib.aws_s3.BucketBase", "version": "2.108.0" @@ -470,7 +499,7 @@ }, "Resource": { "id": "Resource", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/AwsCliLayer/Resource", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/AwsCliLayer/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::Lambda::LayerVersion", "aws:cdk:cloudformation:props": { @@ -496,45 +525,19 @@ }, "CustomResourceHandler": { "id": "CustomResourceHandler", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/CustomResourceHandler", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/CustomResourceHandler", "constructInfo": { "fqn": "aws-cdk-lib.aws_lambda.SingletonFunction", "version": "2.108.0" } }, - "Asset1": { - "id": "Asset1", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/Asset1", - "children": { - "Stage": { - "id": "Stage", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/Asset1/Stage", - "constructInfo": { - "fqn": "aws-cdk-lib.AssetStaging", - "version": "2.108.0" - } - }, - "AssetBucket": { - "id": "AssetBucket", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/Asset1/AssetBucket", - "constructInfo": { - "fqn": "aws-cdk-lib.aws_s3.BucketBase", - "version": "2.108.0" - } - } - }, - "constructInfo": { - "fqn": "aws-cdk-lib.aws_s3_assets.Asset", - "version": "2.108.0" - } - }, "CustomResource": { "id": "CustomResource", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/CustomResource", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/CustomResource", "children": { "Default": { "id": "Default", - "path": "integ-stackset-asset-test/StackSetAssetsBucketDeployment/CustomResource/Default", + "path": "integ-stackset-asset-test/integstacksetassettestassetstacksetB1BE16AD-AssetBucketDeployment-0/CustomResource/Default", "constructInfo": { "fqn": "aws-cdk-lib.CfnResource", "version": "2.108.0" @@ -883,7 +886,7 @@ "path": "integ-test/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.0.5" + "version": "10.5.1" } }, "DeployAssert": { @@ -915,13 +918,13 @@ }, "constructInfo": { "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", - "version": "2.109.0-alpha.0" + "version": "2.108.0-alpha.0" } } }, "constructInfo": { "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", - "version": "2.109.0-alpha.0" + "version": "2.108.0-alpha.0" } }, "Tree": { @@ -929,7 +932,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.0.5" + "version": "10.5.1" } } }, diff --git a/test/stack-set.test.ts b/test/stack-set.test.ts index ae61966..8042b83 100644 --- a/test/stack-set.test.ts +++ b/test/stack-set.test.ts @@ -174,7 +174,9 @@ test('self managed stackset with disabled regions', () => { { Effect: 'Allow', Principal: { - Service: 'cloudformation.af-south-1.amazonaws.com', + Service: { + 'Fn::Join': ['', ['cloudformation.af-south-1.', { Ref: 'AWS::URLSuffix' }]], + }, }, Action: 'sts:AssumeRole', }, @@ -618,3 +620,147 @@ test('passes operation preferences', () => { }], }); }); + +test('self managed stackset uses partition-aware ARN for execution role', () => { + const app = new App(); + const stack = new Stack(app); + + new StackSet(stack, 'StackSet', { + target: StackSetTarget.fromAccounts({ + regions: ['us-east-1'], + accounts: ['11111111111'], + }), + template: StackSetTemplate.fromStackSetStack(new StackSetStack(stack, 'Stack')), + }); + + // For env-agnostic stacks, formatArn produces a Fn::Join with AWS::Partition + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Effect: 'Allow', + Action: 'sts:AssumeRole', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::*:role/AWSCloudFormationStackSetExecutionRole', + ], + ], + }, + }, + ], + }, + }); +}); + +test('self managed stackset with custom execution role name uses partition-aware ARN', () => { + const app = new App(); + const stack = new Stack(app); + + new StackSet(stack, 'StackSet', { + target: StackSetTarget.fromAccounts({ + regions: ['us-east-1'], + accounts: ['11111111111'], + }), + template: StackSetTemplate.fromStackSetStack(new StackSetStack(stack, 'Stack')), + deploymentType: DeploymentType.selfManaged({ + executionRoleName: 'CustomExecutionRole', + }), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Effect: 'Allow', + Action: 'sts:AssumeRole', + Resource: { + 'Fn::Join': [ + '', + ['arn:', { Ref: 'AWS::Partition' }, ':iam::*:role/CustomExecutionRole'], + ], + }, + }, + ], + }, + }); +}); + +test('self managed stackset with disabled regions uses partition-aware URL suffix', () => { + const app = new App(); + const stack = new Stack(app); + + new StackSet(stack, 'StackSet', { + target: StackSetTarget.fromAccounts({ + regions: ['us-east-1', 'af-south-1'], + accounts: ['11111111111'], + }), + template: StackSetTemplate.fromStackSetStack(new StackSetStack(stack, 'Stack')), + }); + + // For env-agnostic stacks, urlSuffix produces AWS::URLSuffix reference + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Effect: 'Allow', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Action: 'sts:AssumeRole', + }, + { + Effect: 'Allow', + Principal: { + Service: { + 'Fn::Join': ['', ['cloudformation.af-south-1.', { Ref: 'AWS::URLSuffix' }]], + }, + }, + Action: 'sts:AssumeRole', + }, + ], + }, + }); +}); + +test('GovCloud partition - self managed stackset with specific environment', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack', { + env: { + account: '111111111111', + region: 'us-gov-west-1', + }, + }); + + new StackSet(stack, 'StackSet', { + target: StackSetTarget.fromAccounts({ + regions: ['us-gov-west-1'], + accounts: ['11111111111'], + }), + template: StackSetTemplate.fromStackSetStack(new StackSetStack(stack, 'Stack')), + }); + + // Verify that the ARN uses AWS::Partition which will correctly resolve to aws-us-gov at deployment time + // CDK's formatArn produces Fn::Join with AWS::Partition reference even with specific env + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Effect: 'Allow', + Action: 'sts:AssumeRole', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::*:role/AWSCloudFormationStackSetExecutionRole', + ], + ], + }, + }, + ], + }, + }); +});