diff --git a/.github/workflows/github-merit-badger.yml b/.github/workflows/github-merit-badger.yml index a095694eebfcc..d818d2f2da061 100644 --- a/.github/workflows/github-merit-badger.yml +++ b/.github/workflows/github-merit-badger.yml @@ -17,4 +17,4 @@ jobs: badges: '[beginning-contributor,repeat-contributor,valued-contributor,admired-contributor,star-contributor,distinguished-contributor]' thresholds: '[0,3,6,13,25,50]' badge-type: 'achievement' - ignore-usernames: '[RomainMuller,rix0rrr,MrArnoldPalmer,iliapolo,otaviomacedo,madeline-k,kaizencc,comcalvi,corymhall,peterwoodworth,ryparker,TheRealAmazonKendra,vinayak-kukreja,Naumel,mrgrain,pahud,cgarvis,kellertk,HBobertz,sumupitchayan,pattasai,SankyRed,udaypant,colifran,khushail,aws-cdk-automation,dependabot[bot],mergify[bot]]' + ignore-usernames: '[RomainMuller,rix0rrr,MrArnoldPalmer,iliapolo,otaviomacedo,madeline-k,kaizencc,comcalvi,corymhall,peterwoodworth,ryparker,TheRealAmazonKendra,vinayak-kukreja,Naumel,mrgrain,pahud,cgarvis,kellertk,HBobertz,sumupitchayan,pattasai,SankyRed,udaypant,colifran,khushail,scanlonp,aws-cdk-automation,dependabot[bot],mergify[bot]]' diff --git a/.mergify.yml b/.mergify.yml index fafb56bf5a1cc..fc39ba7395066 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -11,7 +11,7 @@ pull_request_rules: label: add: [ contribution/core ] conditions: - - author~=^(RomainMuller|rix0rrr|MrArnoldPalmer|iliapolo|uttarasridhar|otaviomacedo|madeline-k|kaizencc|comcalvi|corymhall|peterwoodworth|ryparker|TheRealAmazonKendra|vinayak-kukreja|Naumel|mrgrain|pahud|cgarvis|kellertk|HBobertz|sumupitchayan|pattasai|SankyRed|udaypant|colifran)$ + - author~=^(RomainMuller|rix0rrr|MrArnoldPalmer|iliapolo|uttarasridhar|otaviomacedo|madeline-k|kaizencc|comcalvi|corymhall|peterwoodworth|ryparker|TheRealAmazonKendra|vinayak-kukreja|Naumel|mrgrain|pahud|cgarvis|kellertk|HBobertz|sumupitchayan|pattasai|SankyRed|udaypant|colifran|scanlonp)$ - -label~="contribution/core" - name: automatic merge actions: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1356abe3eb3e6..2a7567e8d64bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -108,6 +108,8 @@ Note: `lerna` uses a local cache by default. If your build fails, you can fix the issue and run the command again and it will not rerun any previously successful steps. +Note: If you encounter `ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory`, please try running the command with increased memory by using `export NODE_OPTIONS="--max-old-space-size=8192"`. + At this point, you can run build and test the `aws-cdk-lib` module by running ```console diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts index c76ea0f9bec21..18889bde7dd7e 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts @@ -255,6 +255,17 @@ integTest('can use the custom permissions boundary to bootstrap', withoutBootstr expect(template).toContain('permission-boundary-name'); })); +integTest('can use the custom permissions boundary (with slashes) to bootstrap', withoutBootstrap(async (fixture) => { + let template = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: fixture.bootstrapStackName, + showTemplate: true, + customPermissionsBoundary: 'permission-boundary-name/with/path', + }); + + expect(template).toContain('permission-boundary-name/with/path'); +})); + integTest('can remove customPermissionsBoundary', withoutBootstrap(async (fixture) => { const bootstrapStackName = fixture.bootstrapStackName; const policyName = `${bootstrapStackName}-pb`; diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts index be3a44eb15fd3..6875de6a123d2 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts @@ -111,6 +111,16 @@ export interface DefaultStagingStackOptions { * @default true */ readonly autoDeleteStagingAssets?: boolean; + + /** + * Specify a custom prefix to be used as the staging stack name and + * construct ID. The prefix will be appended before the appId, which + * is required to be part of the stack name and construct ID to + * ensure uniqueness. + * + * @default 'StagingStack' + */ + readonly stagingStackNamePrefix?: string; } /** @@ -159,12 +169,13 @@ export class DefaultStagingStack extends Stack implements IStagingResources { new UsingAppStagingSynthesizer(stack, `UsingAppStagingSynthesizer/${stack.stackName}`); } - const stackId = `StagingStack-${appId}-${context.environmentString}`; + const stackPrefix = options.stagingStackNamePrefix ?? 'StagingStack'; + // Stack name does not need to contain environment because appId is unique inside an env + const stackName = `${stackPrefix}-${appId}`; + const stackId = `${stackName}-${context.environmentString}`; return new DefaultStagingStack(app, stackId, { ...options, - - // Does not need to contain environment because stack names are unique inside an env anyway - stackName: `StagingStack-${appId}`, + stackName, env: { account: stack.account, region: stack.region, diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts index f1dec13b34a3a..3e6504b7befe9 100644 --- a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts @@ -492,6 +492,29 @@ describe(AppStagingSynthesizer, () => { Template.fromJSON(getStagingResourceStack(asm).template).resourceCountIs('Custom::S3AutoDeleteObjects', 0); }); + test('stack prefix can be customized', () => { + // GIVEN + const prefix = 'Prefix'; + app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + stagingStackNamePrefix: prefix, + }), + }); + stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + + // WHEN + const asm = app.synth(); + + // THEN + expect(getStagingResourceStack(asm, prefix).template).toBeDefined(); + }); + describe('environment specifics', () => { test('throws if App includes env-agnostic and specific env stacks', () => { // GIVEN - App with Stack with specific environment @@ -536,7 +559,7 @@ describe(AppStagingSynthesizer, () => { /** * Return the staging resource stack that is generated as part of the assembly */ - function getStagingResourceStack(asm: CloudAssembly) { - return asm.getStackArtifact(`StagingStack-${APP_ID}-000000000000-us-east-1`); + function getStagingResourceStack(asm: CloudAssembly, prefix?: string) { + return asm.getStackArtifact(`${prefix ?? 'StagingStack'}-${APP_ID}-000000000000-us-east-1`); } }); diff --git a/packages/aws-cdk-lib/aws-apigateway/test/lambda-api.test.ts b/packages/aws-cdk-lib/aws-apigateway/test/lambda-api.test.ts index 43db110625a26..a72bed650dbab 100644 --- a/packages/aws-cdk-lib/aws-apigateway/test/lambda-api.test.ts +++ b/packages/aws-cdk-lib/aws-apigateway/test/lambda-api.test.ts @@ -189,7 +189,7 @@ describe('lambda api', () => { // THEN const template = Template.fromStack(stack); // Ensure that all methods have "AWS_PROXY" integrations. - const methods = template.findResources('AWS::ApiGateway::Mathod'); + const methods = template.findResources('AWS::ApiGateway::Method'); const hasProxyIntegration = Match.objectLike({ Integration: Match.objectLike({ Type: 'AWS_PROXY' }) }); for (const method of Object.values(methods)) { expect(hasProxyIntegration.test(method)).toBeTruthy(); diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts index 05a0054491554..7eaaccbb86b01 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts @@ -1367,7 +1367,7 @@ export abstract class BaseService extends Resource } private deploymentAlarmsAvailableInRegion(): boolean { - const unsupportedPartitions = ['aws-cn', 'aws-us-gov', 'aws-us-iso', 'aws-us-iso-b']; + const unsupportedPartitions = ['aws-cn', 'aws-us-gov', 'aws-iso', 'aws-iso-b']; const currentRegion = RegionInfo.get(this.stack.resolve(this.stack.region)); if (currentRegion.partition) { return !unsupportedPartitions.includes(currentRegion.partition); diff --git a/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts index 8da11d24bae4e..a07841af5d75f 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/ec2/ec2-service.test.ts @@ -2365,6 +2365,32 @@ describe('ec2 service', () => { }); }); + test('no deployment alarms in isolated partitions', () => { + const app = new cdk.App(); + const govCloudStack = new cdk.Stack(app, 'IsoStack', { + env: { region: 'us-isob-east-1' }, + }); + const vpc = new ec2.Vpc(govCloudStack, 'MyVpc', {}); + const gcCluster = new ecs.Cluster(govCloudStack, 'EcsCluster', { vpc }); + addDefaultCapacityProvider(gcCluster, govCloudStack, vpc); + const gcTaskDefinition = new ecs.Ec2TaskDefinition(govCloudStack, 'Ec2TaskDef'); + + gcTaskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + new ecs.Ec2Service(govCloudStack, 'Ec2Service', { + cluster: gcCluster, + taskDefinition: gcTaskDefinition, + }); + + Template.fromStack(govCloudStack).hasResourceProperties('AWS::ECS::Service', { + DeploymentConfiguration: { + Alarms: Match.absent(), + }, + }); + }); + /** * This section of tests test all combinations of the following possible * alarm names and metrics. Most combinations work just fine, some diff --git a/packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts b/packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts index 7e595265ab569..df955a119c470 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts @@ -430,6 +430,8 @@ export class AuroraMysqlEngineVersion { public static readonly VER_3_02_1 = AuroraMysqlEngineVersion.builtIn_8_0('3.02.1'); /** Version "8.0.mysql_aurora.3.02.2". */ public static readonly VER_3_02_2 = AuroraMysqlEngineVersion.builtIn_8_0('3.02.2'); + /** Version "8.0.mysql_aurora.3.02.3". */ + public static readonly VER_3_02_3 = AuroraMysqlEngineVersion.builtIn_8_0('3.02.3'); /** Version "8.0.mysql_aurora.3.03.0". */ public static readonly VER_3_03_0 = AuroraMysqlEngineVersion.builtIn_8_0('3.03.0'); diff --git a/packages/aws-cdk-lib/core/lib/private/tree-metadata.ts b/packages/aws-cdk-lib/core/lib/private/tree-metadata.ts index f0ceb3c0301c5..24e6bb3f526b1 100644 --- a/packages/aws-cdk-lib/core/lib/private/tree-metadata.ts +++ b/packages/aws-cdk-lib/core/lib/private/tree-metadata.ts @@ -10,7 +10,9 @@ import { ISynthesisSession } from '../stack-synthesizers'; import { IInspectable, TreeInspector } from '../tree'; const FILE_PATH = 'tree.json'; - +type Mutable = { + -readonly [P in keyof T]: Mutable; +}; /** * Construct that is automatically attached to the top-level `App`. * This generates, as part of synthesis, a file containing the construct tree and the metadata for each node in the tree. @@ -109,21 +111,34 @@ export class TreeMetadata extends Construct { * tree that leads to a specific construct so drop any nodes not in that path * * @param node Node the current tree node - * @param child Node the previous tree node and the current node's child node - * @returns Node the new tree + * @returns Node the root node of the new tree */ - private renderTreeWithChildren(node: Node, child?: Node): Node { - if (node.parent) { - return this.renderTreeWithChildren(node.parent, node); - } else if (child) { - return { - ...node, - children: { - [child.id]: child, - }, + private renderTreeWithChildren(node: Node): Node { + /** + * @param currentNode - The current node being evaluated + * @param currentNodeChild - The previous node which should be the only child of the current node + * @returns The node with all children removed except for the path to the current node + */ + function renderTreeWithSingleChild(currentNode: Mutable, currentNodeChild: Mutable) { + currentNode.children = { + [currentNodeChild.id]: currentNodeChild, }; + if (currentNode.parent) { + currentNode.parent = renderTreeWithSingleChild(currentNode.parent, currentNode); + } + return currentNode; } - return node; + + const currentNode = node.parent ? renderTreeWithSingleChild(node.parent, node) : node; + // now that we have the new tree we need to return the root node + let root = currentNode; + do { + if (root.parent) { + root = root.parent; + } + } while (root.parent); + + return root; } /** diff --git a/packages/aws-cdk-lib/core/lib/validation/private/construct-tree.ts b/packages/aws-cdk-lib/core/lib/validation/private/construct-tree.ts index f6bd078ad89f2..a48553a8d5cc7 100644 --- a/packages/aws-cdk-lib/core/lib/validation/private/construct-tree.ts +++ b/packages/aws-cdk-lib/core/lib/validation/private/construct-tree.ts @@ -147,6 +147,17 @@ export class ConstructTree { return node; } + /** + * @param node - the root node of the tree + * @returns the terminal node in the tree + */ + private lastChild(node: Node): Node { + if (node.children) { + return this.lastChild(this.getChild(node.children)); + } + return node; + } + /** * Get a ConstructTrace from the cache for a given construct * @@ -154,7 +165,8 @@ export class ConstructTree { * root of the tree and go down to the construct that has the violation */ public getTrace(node: Node, locations?: string[]): ConstructTrace | undefined { - const trace = this._traceCache.get(node.path); + const lastChild = this.lastChild(node); + const trace = this._traceCache.get(lastChild.path); if (trace) { return trace; } @@ -177,7 +189,9 @@ export class ConstructTree { libraryVersion: node.constructInfo?.version, location: thisLocation ?? "Run with '--debug' to include location info", }; - this._traceCache.set(constructTrace.path, constructTrace); + // set the cache for the last child path. If the last child path is different then + // we have a different tree and need to retrieve the trace again + this._traceCache.set(lastChild.path, constructTrace); return constructTrace; } diff --git a/packages/aws-cdk-lib/core/test/validation/trace.test.ts b/packages/aws-cdk-lib/core/test/validation/trace.test.ts index 5f17537b88e1b..5ec4652c7a507 100644 --- a/packages/aws-cdk-lib/core/test/validation/trace.test.ts +++ b/packages/aws-cdk-lib/core/test/validation/trace.test.ts @@ -113,6 +113,12 @@ class MyL2Resource extends Resource implements IMyL2Resource { public readonly constructPath: string; constructor(scope: Construct, id: string) { super(scope, id); + new core.CfnResource(this, 'Resource1', { + type: 'AWS::CDK::TestResource', + properties: { + testProp1: 'testValue', + }, + }); const resource = new core.CfnResource(this, 'Resource', { type: 'AWS::CDK::TestResource', properties: { @@ -127,6 +133,7 @@ class MyConstruct extends Construct { public readonly constructPath: string; constructor(scope: Construct, id: string) { super(scope, id); + new MyL2Resource(this, 'MyL2Resource1'); const myResource = new MyL2Resource(this, 'MyL2Resource'); this.constructPath = myResource.constructPath; } @@ -136,6 +143,8 @@ class MyStack extends core.Stack { public readonly constructPath: string; constructor(scope: Construct, id: string, props?: core.StackProps) { super(scope, id, props); + new MyConstruct(this, 'MyConstruct2'); + new MyConstruct(this, 'MyConstruct3'); const myConstruct = new MyConstruct(this, 'MyConstruct'); this.constructPath = myConstruct.constructPath; } diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index f4a1b22729831..f3041fd3864ec 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -279,7 +279,9 @@ export class Bootstrapper { private validatePolicyName(permissionsBoundary: string) { // https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicy.html - const regexp: RegExp = /[\w+=,.@-]+/; + // Added support for policy names with a path + // See https://github.com/aws/aws-cdk/issues/26320 + const regexp: RegExp = /[\w+\/=,.@-]+/; const matches = regexp.exec(permissionsBoundary); if (!(matches && matches.length === 1 && matches[0] === permissionsBoundary)) { throw new Error(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`); diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 70d7650f58f7b..e5ff7cc642af3 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -143,6 +143,8 @@ export class CdkToolkit { } } + stream.write(format('\n✨ Number of stacks with differences: %s\n', diffs)); + return diffs && options.fail ? 1 : 0; } diff --git a/packages/aws-cdk/test/api/bootstrap2.test.ts b/packages/aws-cdk/test/api/bootstrap2.test.ts index b7e48f8493069..427ea1daf0906 100644 --- a/packages/aws-cdk/test/api/bootstrap2.test.ts +++ b/packages/aws-cdk/test/api/bootstrap2.test.ts @@ -207,6 +207,28 @@ describe('Bootstrapping v2', () => { ])); }); + test('adding permission boundary with path in policy name', async () => { + mockTheToolkitInfo({ + Parameters: [ + { + ParameterKey: 'InputPermissionsBoundary', + ParameterValue: '', + }, + ], + }); + await bootstrapper.bootstrapEnvironment(env, sdk, { + parameters: { + customPermissionsBoundary: 'permissions-boundary-name/with/path', + }, + }); + + expect(stderrMock.mock.calls).toEqual(expect.arrayContaining([ + expect.arrayContaining([ + expect.stringMatching(/Adding new permissions boundary permissions-boundary-name\/with\/path/), + ]), + ])); + }); + test('passing trusted accounts without CFN managed policies results in an error', async () => { await expect(bootstrapper.bootstrapEnvironment(env, sdk, { parameters: { diff --git a/packages/aws-cdk/test/diff.test.ts b/packages/aws-cdk/test/diff.test.ts index 128c933dad245..ad8202a64db63 100644 --- a/packages/aws-cdk/test/diff.test.ts +++ b/packages/aws-cdk/test/diff.test.ts @@ -81,6 +81,7 @@ describe('non-nested stacks', () => { expect(plainTextOutput).toContain('Stack A'); expect(plainTextOutput).toContain('Stack B'); + expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 2'); expect(exitCode).toBe(0); }); @@ -96,6 +97,7 @@ describe('non-nested stacks', () => { }); // THEN + expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 1'); expect(exitCode).toBe(1); }); @@ -121,6 +123,7 @@ describe('non-nested stacks', () => { }); // THEN + expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 1'); expect(exitCode).toBe(1); }); @@ -255,7 +258,10 @@ Resources └─ [~] .Properties: └─ [~] .Prop: ├─ [-] old-value - └─ [+] new-value`); + └─ [+] new-value + + +✨ Number of stacks with differences: 3`); expect(exitCode).toBe(0); });