diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-aws.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-aws.ts index a9ebe5a2ba968..8f9ca8d51a612 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-aws.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-aws.ts @@ -10,11 +10,11 @@ export type AwsContext = { readonly aws: AwsClients }; * * Allocate the next region from the REGION pool and dispose it afterwards. */ -export function withAws( - block: (context: TestContext & AwsContext & DisableBootstrapContext) => Promise, +export function withAws( + block: (context: A & AwsContext & DisableBootstrapContext) => Promise, disableBootstrap: boolean = false, -): (context: TestContext) => Promise { - return (context: TestContext) => regionPool().using(async (region) => { +): (context: A) => Promise { + return (context: A) => regionPool().using(async (region) => { const aws = await AwsClients.forRegion(region, context.output); await sanityCheck(aws); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/amplify.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/amplify.integtest.ts index 50be902ef31d2..8c926e010ef76 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/amplify.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/amplify.integtest.ts @@ -1,10 +1,11 @@ import { promises as fs } from 'fs'; import * as path from 'path'; -import { integTest, withTemporaryDirectory, ShellHelper, withPackages, TemporaryDirectoryContext } from '../../lib'; +import { withToolContext } from './with-tool-context'; +import { integTest, ShellHelper, TemporaryDirectoryContext } from '../../lib'; const TIMEOUT = 1800_000; -integTest('amplify integration', withTemporaryDirectory(withPackages(async (context) => { +integTest('amplify integration', withToolContext(async (context) => { const shell = ShellHelper.fromContext(context); await shell.shell(['npm', 'create', '-y', 'amplify@latest']); @@ -14,9 +15,24 @@ integTest('amplify integration', withTemporaryDirectory(withPackages(async (cont await updateCdkDependency(context, context.packages.requestedCliVersion(), context.packages.requestedFrameworkVersion()); await shell.shell(['npm', 'install']); - await shell.shell(['npx', 'ampx', 'sandbox', '--once']); - await shell.shell(['npx', 'ampx', 'sandbox', 'delete', '--yes']); -})), TIMEOUT); + await shell.shell(['npx', 'ampx', 'sandbox', '--once'], { + modEnv: { + AWS_REGION: context.aws.region, + }, + }); + try { + + // Future code goes here, putting the try/finally here already so it doesn't + // get forgotten. + + } finally { + await shell.shell(['npx', 'ampx', 'sandbox', 'delete', '--yes'], { + modEnv: { + AWS_REGION: context.aws.region, + }, + }); + } +}), TIMEOUT); async function updateCdkDependency(context: TemporaryDirectoryContext, cliVersion: string, libVersion: string) { const filename = path.join(context.integTestDir, 'package.json'); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/with-tool-context.ts b/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/with-tool-context.ts new file mode 100644 index 0000000000000..91a9bf3c688a7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/with-tool-context.ts @@ -0,0 +1,14 @@ +import { TestContext } from '../../lib/integ-test'; +import { AwsContext, withAws } from '../../lib/with-aws'; +import { DisableBootstrapContext } from '../../lib/with-cdk-app'; +import { PackageContext, withPackages } from '../../lib/with-packages'; +import { TemporaryDirectoryContext, withTemporaryDirectory } from '../../lib/with-temporary-directory'; + +/** + * The default prerequisites for tests running tool integrations + */ +export function withToolContext( + block: (context: A & TemporaryDirectoryContext & PackageContext & AwsContext & DisableBootstrapContext + ) => Promise) { + return withAws(withTemporaryDirectory(withPackages(block))); +} diff --git a/packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts b/packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts index cc45b67659c99..458b4e8757340 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts @@ -230,6 +230,13 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 { */ public abstract readonly ownerAccountId: string; + /** + * IPv4 CIDR provisioned under pool + * Required to check for overlapping CIDRs after provisioning + * is complete under IPAM pool + */ + public abstract readonly ipv4IpamProvisionedCidrs?: string[]; + /** * If this is set to true, don't error out on trying to select subnets */ diff --git a/packages/@aws-cdk/aws-location-alpha/lib/place-index.ts b/packages/@aws-cdk/aws-location-alpha/lib/place-index.ts index 026d8c5268a68..c1554300c33aa 100644 --- a/packages/@aws-cdk/aws-location-alpha/lib/place-index.ts +++ b/packages/@aws-cdk/aws-location-alpha/lib/place-index.ts @@ -74,39 +74,12 @@ export enum IntendedUse { STORAGE = 'Storage', } -abstract class PlaceIndexBase extends Resource implements IPlaceIndex { - public abstract readonly placeIndexName: string; - public abstract readonly placeIndexArn: string; - - /** - * Grant the given principal identity permissions to perform the actions on this place index. - */ - public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { - return iam.Grant.addToPrincipal({ - grantee: grantee, - actions: actions, - resourceArns: [this.placeIndexArn], - }); - } - - /** - * Grant the given identity permissions to search using this index - */ - public grantSearch(grantee: iam.IGrantable): iam.Grant { - return this.grant(grantee, - 'geo:SearchPlaceIndexForPosition', - 'geo:SearchPlaceIndexForSuggestions', - 'geo:SearchPlaceIndexForText', - ); - } -} - /** * A Place Index * * @see https://docs.aws.amazon.com/location/latest/developerguide/places-concepts.html */ -export class PlaceIndex extends PlaceIndexBase { +export class PlaceIndex extends Resource implements IPlaceIndex { /** * Use an existing place index by name */ @@ -130,7 +103,7 @@ export class PlaceIndex extends PlaceIndexBase { throw new Error(`Place Index Arn ${placeIndexArn} does not have a resource name.`); } - class Import extends PlaceIndexBase { + class Import extends Resource implements IPlaceIndex { public readonly placeIndexName = parsedArn.resourceName!; public readonly placeIndexArn = placeIndexArn; } @@ -187,4 +160,25 @@ export class PlaceIndex extends PlaceIndexBase { this.placeIndexUpdateTime = placeIndex.attrUpdateTime; } + /** + * Grant the given principal identity permissions to perform the actions on this place index. + */ + public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee: grantee, + actions: actions, + resourceArns: [this.placeIndexArn], + }); + } + + /** + * Grant the given identity permissions to search using this index + */ + public grantSearch(grantee: iam.IGrantable): iam.Grant { + return this.grant(grantee, + 'geo:SearchPlaceIndexForPosition', + 'geo:SearchPlaceIndexForSuggestions', + 'geo:SearchPlaceIndexForText', + ); + } } diff --git a/packages/@aws-cdk/aws-scheduler-alpha/README.md b/packages/@aws-cdk/aws-scheduler-alpha/README.md index 4365f83706719..6dc21f3098c46 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/README.md +++ b/packages/@aws-cdk/aws-scheduler-alpha/README.md @@ -85,8 +85,6 @@ const cronBasedSchedule = new Schedule(this, 'Schedule', { A one-time schedule is a schedule that invokes a target only once. You configure a one-time schedule when by specifying the time of the day, date, and time zone in which EventBridge Scheduler evaluates the schedule. -[comment]: <> (TODO: Switch to `ts` once Schedule is implemented) - ```ts declare const target: targets.LambdaInvoke; @@ -208,11 +206,6 @@ const target = new targets.LambdaInvoke(fn, { }); ``` - -### Cross-account and cross-region targets - -Executing cross-account and cross-region targets are not supported yet. - ### Specifying Encryption key EventBridge Scheduler integrates with AWS Key Management Service (AWS KMS) to encrypt and decrypt your data using an AWS KMS key. diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/input.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/input.ts index b7c87e1a98f86..030d1c97338b8 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/input.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/input.ts @@ -2,17 +2,12 @@ import { DefaultTokenResolver, IResolveContext, Stack, StringConcat, Token, Toke import { ISchedule } from './schedule'; /** - * The text, or well-formed JSON, passed to the target of the schedule. + * The text or well-formed JSON input passed to the target of the schedule. + * Tokens and ContextAttribute may be used in the input. */ export abstract class ScheduleTargetInput { /** - * Pass text to the target, it is possible to embed `ContextAttributes` - * that will be resolved to actual values while the CloudFormation is - * deployed or cdk Tokens that will be resolved when the CloudFormation - * templates are generated by CDK. - * - * The target input value will be a single string that you pass. - * For passing complex values like JSON object to a target use method + * Pass simple text to the target. For passing complex values like JSON object to a target use method * `ScheduleTargetInput.fromObject()` instead. * * @param text Text to use as the input for the target @@ -22,8 +17,7 @@ export abstract class ScheduleTargetInput { } /** - * Pass a JSON object to the target, it is possible to embed `ContextAttributes` and other - * cdk references. + * Pass a JSON object to the target. The object will be transformed into a well-formed JSON string in the final template. * * @param obj object to use to convert to JSON to use as input for the target */ @@ -66,7 +60,8 @@ class FieldAwareEventInput extends ScheduleTargetInput { } /** - * Represents a field in the event pattern + * A set of convenient static methods representing the Scheduler Context Attributes. + * These Context Attributes keywords can be used inside a ScheduleTargetInput. * * @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-context-attributes.html */ @@ -103,7 +98,7 @@ export class ContextAttribute { } /** - * Escape hatch for other ContextAttribute that might be resolved in future. + * Escape hatch for other Context Attributes that may be added in the future * * @param name - name will replace xxx in */ diff --git a/packages/aws-cdk-lib/core/lib/app.ts b/packages/aws-cdk-lib/core/lib/app.ts index d24ec829f00df..019919c8b85d4 100644 --- a/packages/aws-cdk-lib/core/lib/app.ts +++ b/packages/aws-cdk-lib/core/lib/app.ts @@ -192,7 +192,7 @@ export class App extends Stage { if (autoSynth) { // synth() guarantees it will only execute once, so a default of 'true' // doesn't bite manual calling of the function. - process.once('beforeExit', () => this.synth()); + process.once('beforeExit', () => this.synth({ errorOnDuplicateSynth: false })); } this._treeMetadata = props.treeMetadata ?? true; diff --git a/packages/aws-cdk-lib/core/lib/stage.ts b/packages/aws-cdk-lib/core/lib/stage.ts index f48ff158b9b54..c3f9acf4a0563 100644 --- a/packages/aws-cdk-lib/core/lib/stage.ts +++ b/packages/aws-cdk-lib/core/lib/stage.ts @@ -146,6 +146,11 @@ export class Stage extends Construct { */ private assembly?: cxapi.CloudAssembly; + /** + * The cached set of construct paths. Empty if assembly was not yet built. + */ + private constructPathsCache: Set; + /** * Validation plugins to run during synthesis. If any plugin reports any violation, * synthesis will be interrupted and the report displayed to the user. @@ -163,6 +168,7 @@ export class Stage extends Construct { Object.defineProperty(this, STAGE_SYMBOL, { value: true }); + this.constructPathsCache = new Set(); this.parentStage = Stage.of(this); this.region = props.env?.region ?? this.parentStage?.region; @@ -210,16 +216,62 @@ export class Stage extends Construct { * calls will return the same assembly. */ public synth(options: StageSynthesisOptions = { }): cxapi.CloudAssembly { - if (!this.assembly || options.force) { + + let newConstructPaths = this.listAllConstructPaths(this); + + // If the assembly cache is uninitiazed, run synthesize and reset construct paths cache + if (this.constructPathsCache.size == 0 || !this.assembly || options.force) { this.assembly = synthesize(this, { skipValidation: options.skipValidation, validateOnSynthesis: options.validateOnSynthesis, }); + newConstructPaths = this.listAllConstructPaths(this); + this.constructPathsCache = newConstructPaths; } + // If the construct paths set has changed + if (!this.constructPathSetsAreEqual(this.constructPathsCache, newConstructPaths)) { + const errorMessage = 'Synthesis has been called multiple times and the construct tree was modified after the first synthesis.'; + if (options.errorOnDuplicateSynth ?? true) { + throw new Error(errorMessage + ' This is not allowed. Remove multple synth() calls and do not modify the construct tree after the first synth().'); + } else { + // eslint-disable-next-line no-console + console.error(errorMessage + ' Only the results of the first synth() call are used, and modifications done after it are ignored. Avoid construct tree mutations after synth() has been called unless this is intentional.'); + } + } + + // Reset construct paths cache + this.constructPathsCache = newConstructPaths; + return this.assembly; } + // Function that lists all construct paths and returns them as a set + private listAllConstructPaths(construct: IConstruct): Set { + const paths = new Set(); + function recurse(root: IConstruct) { + paths.add(root.node.path); + for (const child of root.node.children) { + if (!Stage.isStage(child)) { + recurse(child); + } + } + } + recurse(construct); + return paths; + } + + // Checks if sets of construct paths are equal + private constructPathSetsAreEqual(set1: Set, set2: Set): boolean { + if (set1.size !== set2.size) return false; + for (const id of set1) { + if (!set2.has(id)) { + return false; + } + } + return true; + } + private createBuilder(outdir?: string) { // cannot specify "outdir" if we are a nested stage if (this.parentStage && outdir) { @@ -259,4 +311,11 @@ export interface StageSynthesisOptions { * @default false */ readonly force?: boolean; + + /** + * Whether or not to throw a warning instead of an error if the construct tree has + * been mutated since the last synth. + * @default true + */ + readonly errorOnDuplicateSynth?: boolean; } diff --git a/packages/aws-cdk-lib/core/test/synthesis.test.ts b/packages/aws-cdk-lib/core/test/synthesis.test.ts index 8b67b371e0be8..90760a19d05da 100644 --- a/packages/aws-cdk-lib/core/test/synthesis.test.ts +++ b/packages/aws-cdk-lib/core/test/synthesis.test.ts @@ -3,6 +3,7 @@ import * as os from 'os'; import * as path from 'path'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import { Construct } from 'constructs'; +import { Template } from '../../assertions'; import * as cxschema from '../../cloud-assembly-schema'; import * as cxapi from '../../cx-api'; import * as cdk from '../lib'; @@ -362,6 +363,30 @@ describe('synthesis', () => { }); + test('calling synth multiple times errors if construct tree is mutated', () => { + const app = new cdk.App(); + + const stages = [ + { + stage: 'PROD', + }, + { + stage: 'BETA', + }, + ]; + + // THEN - no error the first time synth is called + let stack = new cdk.Stack(app, `${stages[0].stage}-Stack`, {}); + expect(() => { + Template.fromStack(stack); + }).not.toThrow(); + + // THEN - error is thrown since synth was called with mutated stack name + stack = new cdk.Stack(app, `${stages[1].stage}-Stack`, {}); + expect(() => { + Template.fromStack(stack); + }).toThrow('Synthesis has been called multiple times and the construct tree was modified after the first synthesis'); + }); }); function list(outdir: string) { diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/synths.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/synths.test.ts index ce52f6a2df0a8..a0555f1a28800 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/synths.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/synths.test.ts @@ -186,6 +186,27 @@ test('CodeBuild: environment variables specified in multiple places are correctl }), }); + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk-2', { + synth: new cdkp.CodeBuildStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: '.', + env: { + SOME_ENV_VAR: 'SomeValue', + }, + installCommands: [ + 'install1', + 'install2', + ], + commands: ['synth'], + buildEnvironment: { + environmentVariables: { + INNER_VAR: { value: 'InnerValue' }, + }, + privileged: true, + }, + }), + }); + // THEN Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: Match.objectLike({ @@ -217,27 +238,6 @@ test('CodeBuild: environment variables specified in multiple places are correctl }, }); - new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk-2', { - synth: new cdkp.CodeBuildStep('Synth', { - input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), - primaryOutputDirectory: '.', - env: { - SOME_ENV_VAR: 'SomeValue', - }, - installCommands: [ - 'install1', - 'install2', - ], - commands: ['synth'], - buildEnvironment: { - environmentVariables: { - INNER_VAR: { value: 'InnerValue' }, - }, - privileged: true, - }, - }), - }); - // THEN Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: Match.objectLike({ diff --git a/packages/aws-cdk/lib/init-templates/app/typescript/tsconfig.json b/packages/aws-cdk/lib/init-templates/app/typescript/tsconfig.json index aaa7dc510f1d2..9fc64e91b956a 100644 --- a/packages/aws-cdk/lib/init-templates/app/typescript/tsconfig.json +++ b/packages/aws-cdk/lib/init-templates/app/typescript/tsconfig.json @@ -1,17 +1,13 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2022", "module": "commonjs", "lib": [ - "es2020", + "ES2023", "dom" ], "declaration": true, "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, diff --git a/packages/aws-cdk/lib/init-templates/sample-app/typescript/tsconfig.json b/packages/aws-cdk/lib/init-templates/sample-app/typescript/tsconfig.json index aaa7dc510f1d2..9fc64e91b956a 100644 --- a/packages/aws-cdk/lib/init-templates/sample-app/typescript/tsconfig.json +++ b/packages/aws-cdk/lib/init-templates/sample-app/typescript/tsconfig.json @@ -1,17 +1,13 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2022", "module": "commonjs", "lib": [ - "es2020", + "ES2023", "dom" ], "declaration": true, "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, diff --git a/tools/@aws-cdk/spec2cdk/README.md b/tools/@aws-cdk/spec2cdk/README.md index b2aeac3be97cd..da31ef031a65d 100644 --- a/tools/@aws-cdk/spec2cdk/README.md +++ b/tools/@aws-cdk/spec2cdk/README.md @@ -29,7 +29,9 @@ At a code level, import `@aws-cdk/spec2cdk/lib/cfn2ts` for a drop-in replacement ## Temporary Schemas You can import additional, temporary CloudFormation Registry Schemas to test new functionality that is not yet published in `@aws-cdk/aws-service-spec`. -To do this, drop the schema file into `temporary-schemas/us-east-1` and it will be imported on top of the default model. +To do this, drop the schema file into `temporary-schemas/us-east-1` ([e.g](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-type-schemas.html)). After you add the schema file, run spec2cdk with the specified module and short name. +As an example, if you were updating [AWS::KMS::Key](https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-kms), you must run: +`bin/spec2cdk --service AWS::KMS`. ## CLI