diff --git a/.mergify.yml b/.mergify.yml index 25c67f41b33ea..96cc7bbb7c21f 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -31,7 +31,7 @@ pull_request_rules: - "#changes-requested-reviews-by=0" - status-success~=AWS CodeBuild us-east-1 #- status-success=Semantic Pull Request - - status-success=mandatory-changes + - status-success=validate-pr - name: automatic merge (2+ approvers) actions: comment: @@ -56,7 +56,7 @@ pull_request_rules: - "#changes-requested-reviews-by=0" - status-success~=AWS CodeBuild us-east-1 #- status-success=Semantic Pull Request - - status-success=mandatory-changes + - status-success=validate-pr - name: automatic merge (no-squash) actions: comment: @@ -82,7 +82,7 @@ pull_request_rules: - "#changes-requested-reviews-by=0" - status-success~=AWS CodeBuild us-east-1 #- status-success=Semantic Pull Request - - status-success=mandatory-changes + - status-success=validate-pr - name: remove stale reviews actions: dismiss_reviews: @@ -126,4 +126,4 @@ pull_request_rules: - "#changes-requested-reviews-by=0" - status-success~=AWS CodeBuild us-east-1 #- status-success=Semantic Pull Request - - status-success=mandatory-changes + - status-success=validate-pr diff --git a/CHANGELOG.md b/CHANGELOG.md index 88c47a4abd210..2e18651f1b19c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,41 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.61.0](https://github.com/aws/aws-cdk/compare/v1.60.0...v1.61.0) (2020-08-27) + + +### Features + +* **appsync:** implement resolvable fields for code-first schema ([#9660](https://github.com/aws/aws-cdk/issues/9660)) ([9e3b798](https://github.com/aws/aws-cdk/commit/9e3b7981dc269e66f45e2ee4ca54d281a7945723)) +* **appsync:** separating schema from graphql api ([#9903](https://github.com/aws/aws-cdk/issues/9903)) ([8d71fa1](https://github.com/aws/aws-cdk/commit/8d71fa1a1a9ca7557fcd33bd93df1b357627baed)) +* **cli:** automatically determine region on EC2 instances ([#9313](https://github.com/aws/aws-cdk/issues/9313)) ([1cf986d](https://github.com/aws/aws-cdk/commit/1cf986d56f2cc8b72f94f4a7b52a309790ce4722)) +* **core:** facility to warn when deprecated APIs are used ([#9585](https://github.com/aws/aws-cdk/issues/9585)) ([b1d0ac0](https://github.com/aws/aws-cdk/commit/b1d0ac0564a86ab325e06b18670657ee9c953e3e)) +* **custom-resources:** function name for AwsCustomResource ([#9774](https://github.com/aws/aws-cdk/issues/9774)) ([6da6581](https://github.com/aws/aws-cdk/commit/6da6581c91e3f6fae83e45f7d374a42407e57a2f)), closes [#9771](https://github.com/aws/aws-cdk/issues/9771) +* **eks:** envelope encryption for secrets ([#9438](https://github.com/aws/aws-cdk/issues/9438)) ([65fd3e6](https://github.com/aws/aws-cdk/commit/65fd3e66ab3817f7e5051c5a8ae3c13b65415f63)), closes [#9140](https://github.com/aws/aws-cdk/issues/9140) +* **rds:** deletion protection for RDS cluster ([#9871](https://github.com/aws/aws-cdk/issues/9871)) ([ef98b9f](https://github.com/aws/aws-cdk/commit/ef98b9f3b82129540177a94dc1cca7340856ae38)), closes [#6944](https://github.com/aws/aws-cdk/issues/6944) +* **rds:** grantConnect for database instances ([#9887](https://github.com/aws/aws-cdk/issues/9887)) ([e893828](https://github.com/aws/aws-cdk/commit/e8938282b2649fa7c4aa126cc9bb7e8d28600d77)), closes [#1558](https://github.com/aws/aws-cdk/issues/1558) +* **region-info:** add information for af-south-1 and eu-south-1 regions ([#9569](https://github.com/aws/aws-cdk/issues/9569)) ([9d76c26](https://github.com/aws/aws-cdk/commit/9d76c267b4777852fcab797ee6f54880663f6569)) +* **s3:** imported buckets can have an explicit region ([#9936](https://github.com/aws/aws-cdk/issues/9936)) ([f0c76ac](https://github.com/aws/aws-cdk/commit/f0c76ac1f930fcbe7a2610e7aeeb4a46721516e1)), closes [#8280](https://github.com/aws/aws-cdk/issues/8280) [#9556](https://github.com/aws/aws-cdk/issues/9556) +* **stepfunctions-tasks:** add support for CodeBuild StartBuild API ([#9757](https://github.com/aws/aws-cdk/issues/9757)) ([dae54ec](https://github.com/aws/aws-cdk/commit/dae54eccf995c868ddfc839f9ab078169a34464f)), closes [#8043](https://github.com/aws/aws-cdk/issues/8043) + + +### Bug Fixes + +* **appsync:** add dependency between apikey and schema ([#9737](https://github.com/aws/aws-cdk/issues/9737)) ([4448794](https://github.com/aws/aws-cdk/commit/44487946489298902fc9d15ded31e24d19171a6f)), closes [#8168](https://github.com/aws/aws-cdk/issues/8168) [#9736](https://github.com/aws/aws-cdk/issues/9736) [#8168](https://github.com/aws/aws-cdk/issues/8168) +* **bootstrap:** add alias for the asset key ([#9872](https://github.com/aws/aws-cdk/issues/9872)) ([952e686](https://github.com/aws/aws-cdk/commit/952e686989875e53a819c68513bba77c7fdd5e91)), closes [#6719](https://github.com/aws/aws-cdk/issues/6719) +* **cfn-include:** allow numbers to be passed to string properties ([#9849](https://github.com/aws/aws-cdk/issues/9849)) ([4c8c6f1](https://github.com/aws/aws-cdk/commit/4c8c6f1b4f564c5f0ef6ae95f635da431b619257)), closes [#9784](https://github.com/aws/aws-cdk/issues/9784) +* **cfn-include:** short form for Condition ([#9865](https://github.com/aws/aws-cdk/issues/9865)) ([371e8da](https://github.com/aws/aws-cdk/commit/371e8da890061e61e71fef10eb262cd0bb1a25e0)), closes [#9785](https://github.com/aws/aws-cdk/issues/9785) +* **core:** Access Denied using legacy synthesizer with new bootstrap ([#9831](https://github.com/aws/aws-cdk/issues/9831)) ([960ef12](https://github.com/aws/aws-cdk/commit/960ef1237e8379090e78dca554401213c81d2be7)) +* **core:** Duration incorrectly renders Days ([#9935](https://github.com/aws/aws-cdk/issues/9935)) ([0ca09a7](https://github.com/aws/aws-cdk/commit/0ca09a75f3104a7e0d0e66a4b89496158fcbeee8)), closes [#9906](https://github.com/aws/aws-cdk/issues/9906) +* **elasticloadbalancingv2:** imported listener ignores conditions attribute ([#9939](https://github.com/aws/aws-cdk/issues/9939)) ([1c9b733](https://github.com/aws/aws-cdk/commit/1c9b73361983346caa62921867519e3cfcc6288e)), closes [#8385](https://github.com/aws/aws-cdk/issues/8385) [#9262](https://github.com/aws/aws-cdk/issues/9262) [#9320](https://github.com/aws/aws-cdk/issues/9320) [#9643](https://github.com/aws/aws-cdk/issues/9643) +* **lambda:** cannot use latest version in multiple cloudfront distributions ([#9966](https://github.com/aws/aws-cdk/issues/9966)) ([71c60f2](https://github.com/aws/aws-cdk/commit/71c60f20300790bd8f4fa898c8855d93d37d7cd9)), closes [#4459](https://github.com/aws/aws-cdk/issues/4459) +* **lambda:** grantInvoke fails on second invocation ([#9960](https://github.com/aws/aws-cdk/issues/9960)) ([0fc5899](https://github.com/aws/aws-cdk/commit/0fc5899364b8fb3b2fac8cb774f103054607bb2e)), closes [#8553](https://github.com/aws/aws-cdk/issues/8553) +* **lambda-nodejs:** incorrect working directory for local bundling ([#9870](https://github.com/aws/aws-cdk/issues/9870)) ([a4185a0](https://github.com/aws/aws-cdk/commit/a4185a0a2a5f95dfdfed656f5f16d49c95f7cf83)), closes [#9632](https://github.com/aws/aws-cdk/issues/9632) + ## [1.60.0](https://github.com/aws/aws-cdk/compare/v1.59.0...v1.60.0) (2020-08-19) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cloudfront:** Distribution: `.domains` must be specified if `certificate` is provided. * **appsync:** **appsync.addXxxDataSource** `name` and `description` props are now optional and in an `DataSourceOptions` interface. @@ -60,7 +91,7 @@ All notable changes to this project will be documented in this file. See [standa ## [1.59.0](https://github.com/aws/aws-cdk/compare/v1.58.0...v1.59.0) (2020-08-14) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **eks:** `cluster.addResource` was renamed to `cluster.addManifest` and `KubernetesResource` was renamed to `KubernetesManifest` * **cloudfront:** (cloudfront) Changed IDs for Distributions (will cause resource replacement). @@ -120,7 +151,7 @@ All notable changes to this project will be documented in this file. See [standa ## [1.57.0](https://github.com/aws/aws-cdk/compare/v1.56.0...v1.57.0) (2020-08-07) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **apigatewayv2:** The parameter for the method `bind()` on `IHttpRouteIntegration` has changed to accept one of type @@ -173,7 +204,7 @@ Related: https://github.com/aws/aws-cdk-rfcs/issues/192 ## [1.56.0](https://github.com/aws/aws-cdk/compare/v1.55.0...v1.56.0) (2020-07-31) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **appsync:** **appsync** prop `schemaDefinition` no longer takes string, instead it is required to configure schema definition mode. - **appsync**: schemaDefinition takes param `SchemaDefinition.XXX` to declare how schema will be configured @@ -203,7 +234,7 @@ Related: https://github.com/aws/aws-cdk-rfcs/issues/192 ## [1.55.0](https://github.com/aws/aws-cdk/compare/v1.54.0...v1.55.0) (2020-07-28) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **lambda:** the `bundlingDockerImage` prop of a `Runtime` now points to the AWS SAM build image (`amazon/aws-sam-cli-build-image-`) instead of the LambCI build image (`lambci/lambda:build-`) * **appsync:** `pipelineConfig` is now an array of `string` instead of `CfnResolver.PipelineConfigProperty` for usability. @@ -269,7 +300,7 @@ Related: https://github.com/aws/aws-cdk-rfcs/issues/192 ## [1.52.0](https://github.com/aws/aws-cdk/compare/v1.51.0...v1.52.0) (2020-07-18) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **rds:** the property 'version' has been changed from string to an engine-specific version class; use VersionClass.of() if you need to create a specific version of an engine from a string @@ -350,7 +381,7 @@ These can be specifed directly in the OpenAPI spec or via `addMethod()` ## [1.50.0](https://github.com/aws/aws-cdk/compare/v1.49.1...v1.50.0) (2020-07-07) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **eks:** `version` is now a mandatory property @@ -406,7 +437,7 @@ These can be specifed directly in the OpenAPI spec or via `addMethod()` ## [1.48.0](https://github.com/aws/aws-cdk/compare/v1.47.1...v1.48.0) (2020-07-01) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **stepfunctions-tasks:** `containerName` is not supported as an override anymore and has been replaced by `containerDefinition` * **stepfunctions-tasks:** `EvaluateExpression` is now a construct representing a task state rather than an embedded property called `task` @@ -458,7 +489,7 @@ vault with recovery points cannot be deleted. ## [1.47.0](https://github.com/aws/aws-cdk/compare/v1.46.0...v1.47.0) (2020-06-24) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **stepfunctions-tasks:** `Dynamo*` tasks no longer implement`IStepFunctionsTask` and have been replaced by constructs that can be instantiated directly. See README for examples @@ -482,7 +513,7 @@ vault with recovery points cannot be deleted. ## [1.46.0](https://github.com/aws/aws-cdk/compare/v1.45.0...v1.46.0) (2020-06-19) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **stepfunctions-tasks:** constructs for `EMR*` have been introduced to replace previous implementation which implemented `IStepFUnctionsTask`. @@ -555,7 +586,7 @@ use cluster.connections.securityGroups instead ## [1.45.0](https://github.com/aws/aws-cdk/compare/v1.44.0...v1.45.0) (2020-06-09) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **stepfunctions-tasks:** constructs for `SageMakerCreateTrainingJob` and `SageMakerCreateTransformJob` replace previous implementation that @@ -606,7 +637,7 @@ is now type `core.Size` ## [1.43.0](https://github.com/aws/aws-cdk/compare/v1.42.1...v1.43.0) (2020-06-03) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **rds:** the default retention policy for RDS Cluster and DbInstance is now 'Snapshot' * **cognito:** OAuth flows `authorizationCodeGrant` and @@ -655,7 +686,7 @@ by default. ## [1.42.0](https://github.com/aws/aws-cdk/compare/v1.41.0...v1.42.0) (2020-05-27) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cloudtrail:** API signatures of `addS3EventSelectors` and `addLambdaEventSelectors` have changed. Their parameters are now @@ -731,7 +762,7 @@ events. Two new APIs `logAllS3DataEvents()` and ## [1.39.0](https://github.com/aws/aws-cdk/compare/v1.38.0...v1.39.0) (2020-05-15) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cognito:** An invalid template placeholder has been removed from the default verification email body in a user pool. @@ -795,7 +826,7 @@ from the default verification email body in a user pool. ## [1.37.0](https://github.com/aws/aws-cdk/compare/v1.36.0...v1.37.0) (2020-05-05) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **amplify:** `mapSubDomain()` called with an empty string for `prefix` now maps to the domain root. @@ -844,7 +875,7 @@ maps to the domain root. ## [1.36.0](https://github.com/aws/aws-cdk/compare/v1.35.0...v1.36.0) (2020-04-28) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **stepfunctions-tasks:** `payload` in RunLambdaTask is now of type `TaskInput` and has a default of the state input instead of the empty object. You can migrate your current assignment to payload by supplying it to the `TaskInput.fromObject()` API @@ -868,7 +899,7 @@ You can migrate your current assignment to payload by supplying it to the `TaskI ## [1.35.0](https://github.com/aws/aws-cdk/compare/v1.34.1...v1.35.0) (2020-04-23) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **assets:** `cdk deploy` now needs `s3:ListBucket` instead of `s3:HeadObject`. * **efs:** Exported types no longer have the `Efs` prefix. @@ -901,7 +932,7 @@ You can migrate your current assignment to payload by supplying it to the `TaskI ## [1.34.0](https://github.com/aws/aws-cdk/compare/v1.33.1...v1.34.0) (2020-04-21) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **glue:** `DateFormat` constant names are now **UPPERCASE** (`JSON, AVRO, LOGSTASH, ...`) @@ -928,7 +959,7 @@ You can migrate your current assignment to payload by supplying it to the `TaskI ## [1.33.0](https://github.com/aws/aws-cdk/compare/v1.32.2...v1.33.0) (2020-04-17) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **kinesis:** `grantRead()` API no longer provides permissions to `kinesis:DescribeStream` as it provides permissions to `kinesis:DescribeStreamSummary` and `kinesis:SubscribeToShard` in it's place. If it's still desired, it can be added through the `grant()` API on the stream. * **kinesis:** `grantWrite()` API no longer has `DescribeStream` permissions as it has been replaced by `ListShards` for shard discovery @@ -980,7 +1011,7 @@ You can migrate your current assignment to payload by supplying it to the `TaskI ## [1.32.0](https://github.com/aws/aws-cdk/compare/v1.31.0...v1.32.0) (2020-04-07) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cognito:** `UserPoolClient` construct no longer has the property `userPoolClientClientSecret`. The functionality to retrieve the client @@ -1059,7 +1090,7 @@ was already configured for that user pool operation. ## [1.31.0](https://github.com/aws/aws-cdk/compare/v1.30.0...v1.31.0) (2020-03-24) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * .NET Core v3.1 is required with JSII v1.1 @@ -1100,7 +1131,7 @@ was already configured for that user pool operation. :rocket: To enable new CDK projects such as [CDK for Kubernetes](https://github.com/awslabs/cdk8s), we have released the **constructs programming model** as an independent library called [constructs](https://github.com/aws/constructs). The `@aws-cdk/core.Construct` class is now a subclass of the base `constructs.Construct`. -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cognito:** `UserPoolAttribute` has been removed. It is no longer required to defined a `UserPool`. @@ -1127,7 +1158,7 @@ required to defined a `UserPool`. ## [1.28.0](https://github.com/aws/aws-cdk/compare/v1.27.0...v1.28.0) (2020-03-16) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **batch:** `computeEnvironments` is now required * **batch:** the `allocationStrategy` property was moved from `ComputeEnvironmentProps` to the `ComputeResources` interface, which is where it semantically belongs. @@ -1176,7 +1207,7 @@ required to defined a `UserPool`. ## [1.27.0](https://github.com/aws/aws-cdk/compare/v1.26.0...v1.27.0) (2020-03-03) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cognito:** `UserPool.fromUserPoolAttributes()` has been replaced by `fromUserPoolId()` and `fromUserPoolArn()`. @@ -1227,7 +1258,7 @@ required to defined a `UserPool`. ## [1.26.0](https://github.com/aws/aws-cdk/compare/v1.25.0...v1.26.0) (2020-02-25) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **apigateway:** the interface now accepts endpointconfiguration property instead of endpoint type as defined by cfn * **lambda-nodejs:** `parcel-bundler` v1.x is now a peer dependency of `@aws-cdk/aws-lambda-nodejs`. Please add it to your `package.json`. @@ -1262,7 +1293,7 @@ required to defined a `UserPool`. ## [1.25.0](https://github.com/aws/aws-cdk/compare/v1.24.0...v1.25.0) (2020-02-18) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **appsync:** Changes `MappingTemplate.dynamoDbPutItem()` to accept `PrimaryKey` and `AttributeValues`, which allow configuring the primary @@ -1357,7 +1388,7 @@ key and to project an object to a set of attribute values. ## [1.22.0](https://github.com/aws/aws-cdk/compare/v1.21.1...v1.22.0) (2020-01-23) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **eks:** (experimental module) the `Mapping` struct was renamed to `AwsAuthMapping`. * **core:** Arn.parseArn now returns empty string for nullable Arn components. Users who were depending on an undefined value will now receive the falsy empty string. @@ -1431,7 +1462,7 @@ key and to project an object to a set of attribute values. ## [1.20.0](https://github.com/aws/aws-cdk/compare/v1.19.0...v1.20.0) (2020-01-07) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **autoscaling:** AutoScalingGroups without `desiredCapacity` are now initially scaled to their minimum capacity (instead of their maximum @@ -1496,7 +1527,7 @@ capaciety). ## [1.19.0](https://github.com/aws/aws-cdk/compare/v1.18.0...v1.19.0) (2019-12-17) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **route53:** the value of `hostedZoneId` will no longer include `/hostedzone/` prefix and only includes the hostedZoneId when using `HostedZone.fromLookup` or `fromHostedZoneAttributes` * **cloudfront:** (experimental module) `S3OriginConfig.originAccessIdentityId` or type `string` has been removed in favor of `S3OriginConfig.originAccessIdentity` of type `IOriginAccessIdentity`. @@ -1667,7 +1698,7 @@ GitHub issues for more information and workarounds where applicable. ## [1.16.0](https://github.com/aws/aws-cdk/compare/v1.15.0...v1.16.0) (2019-11-11) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **core:** template file names in `cdk.out` for new projects created by `cdk init` will use `stack.artifactId` instead of the physical stack name to enable multiple stacks to use the same name. In most cases the artifact ID is the same as the stack name. To enable this fix for old projects, add the context key `@aws-cdk/core:enableStackNameDuplicates: true` in your `cdk.json` file. @@ -1724,7 +1755,7 @@ In addition to the above, several bugs in the Python, .NET and Java release of t ## [1.15.0](https://github.com/aws/aws-cdk/compare/v1.14.0...v1.15.0) (2019-10-28) -### ⚠ BREAKING CHANGES +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **rds:** `securityGroup: ec2.ISecurityGroup` is now `securityGroups: ec2.ISecurityGroup[]` in `DatabaseInstanceAttributes` * **rds:** removed `securityGroupId` from `IDatabaseInstance` @@ -1847,7 +1878,7 @@ In addition to the above, several bugs in the Python, .NET and Java release of t * **stepfunctions:** add support for Map state ([#4145](https://github.com/aws/aws-cdk/issues/4145)) ([c8f0bcf](https://github.com/aws/aws-cdk/commit/c8f0bcf)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cloudmap:** `cloudmap.Service.fromServiceAttributes` takes a newly required argument `namespace`. @@ -1923,7 +1954,7 @@ required argument `namespace`. * publish construct tree into the cloud assembly ([#4194](https://github.com/aws/aws-cdk/issues/4194)) ([3cca03d](https://github.com/aws/aws-cdk/commit/3cca03d)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **ses-actions:** adding an action to a receipt rule now requires an integration object from the `@aws-cdk/aws-ses-actions` package. @@ -1965,7 +1996,7 @@ object from the `@aws-cdk/aws-ses-actions` package. * **toolkit:** conditionally emit AWS::CDK::Metadata resource ([#3692](https://github.com/aws/aws-cdk/issues/3692)) ([5901d6e](https://github.com/aws/aws-cdk/commit/5901d6e)), closes [#3648](https://github.com/aws/aws-cdk/issues/3648) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **s3-deployment:** Property `source` is now `sources` and is a `Source` array @@ -2000,7 +2031,7 @@ object from the `@aws-cdk/aws-ses-actions` package. * **events:** allow passing a role to the CodePipeline target ([#4006](https://github.com/aws/aws-cdk/issues/4006)) ([c4054ce](https://github.com/aws/aws-cdk/commit/c4054ce)), closes [#3999](https://github.com/aws/aws-cdk/issues/3999) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **assets:** assets no longer expose a property `contentHash`. Use `sourceHash` as a good approximation. if you have a strong use case for content hashes, please @@ -2030,7 +2061,7 @@ raise a github issue and we will figure out a solution. * upgrade to CloudFormation specification 6.0.0 ([#3942](https://github.com/aws/aws-cdk/issues/3942)) ([27de0a0](https://github.com/aws/aws-cdk/commit/27de0a0)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **ecs,lambda,rds:** `securityGroupId: string` replaced by `securityGroup: ISecurityGroup` when importing a cluster/instance in `@aws-cdk/aws-rds` @@ -2086,7 +2117,7 @@ importing a cluster/instance in `@aws-cdk/aws-rds` * updated CloudFormation Resource specification 5.3.0 ([#3789](https://github.com/aws/aws-cdk/issues/3789)) ([39ee810](https://github.com/aws/aws-cdk/commit/39ee810)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **ec2:** By default, egress rules are not created anymore on imported security groups. This can be configured by setting `allowAllOutbound: false` upon importing. @@ -2115,7 +2146,7 @@ importing a cluster/instance in `@aws-cdk/aws-rds` * **events-targets:** allow specifying event for codebuild project target ([#3637](https://github.com/aws/aws-cdk/issues/3637)) ([c240e1e](https://github.com/aws/aws-cdk/commit/c240e1e)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **aws-cdk:** Java builders no longer use the "with" prefix. * **eks:** cluster name output will not be synthesized by default. instead we synthesize an output that includes the full `aws eks update-kubeconfig` command. You can enable synthesis of the cluster name output using the `outputClusterName: true` options. @@ -2182,7 +2213,7 @@ importing a cluster/instance in `@aws-cdk/aws-rds` * **s3-deployment:** CloudFront invalidation ([#3213](https://github.com/aws/aws-cdk/issues/3213)) ([e84bdd6](https://github.com/aws/aws-cdk/commit/e84bdd6)), closes [#3106](https://github.com/aws/aws-cdk/issues/3106) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **eks:** clusters will be created with a default capacity of x2 m5.large instances. You can specify `defaultCapacity: 0` if you wish to disable. @@ -2304,7 +2335,7 @@ and to continue working closely with the open-source community. * **assets:** packages `assets`, `aws-ecr-assets` and `aws-s3-assets` are now experimental instead of stable -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **codepipeline:** Pipeline.crossRegionReplicationBuckets is now experimental * **codepipeline:** Pipeline.crossRegionSupport is now experimental @@ -2334,7 +2365,7 @@ and to continue working closely with the open-source community. * **codebuild:** allow specifying principals and credentials for pulling build images. ([#3049](https://github.com/aws/aws-cdk/issues/3049)) ([3319fe5](https://github.com/aws/aws-cdk/commit/3319fe5)), closes [#2175](https://github.com/aws/aws-cdk/issues/2175) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **codebuild:** `LinuxBuildImage.fromDockerHub()` has been renamed to `fromDockerRegistry()` and `WindowsBuildImage.fromDockerHub()` has been renamed to `fromDockerRegistry()` * **iam:** `aws-iam.User` and `Group`: `managedPolicyArns` => @@ -2355,7 +2386,7 @@ and to continue working closely with the open-source community. * **stepfunctions:** Downscope SageMaker permissions ([#2991](https://github.com/aws/aws-cdk/issues/2991)) ([69c82c8](https://github.com/aws/aws-cdk/commit/69c82c8)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **core:** `construct.findChild()` now only looks up direct children * **ec2:** `Port.toRuleJSON` was renamed to `toRuleJson` @@ -2455,7 +2486,7 @@ and to continue working closely with the open-source community. * **issues:** new format for issue templates ([#2917](https://github.com/aws/aws-cdk/issues/2917)) ([67f6de0](https://github.com/aws/aws-cdk/commit/67f6de0)) * **sns:** add support for subscription filter policy ([#2778](https://github.com/aws/aws-cdk/issues/2778)) ([ae789ed](https://github.com/aws/aws-cdk/commit/ae789ed)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * *IMPORTANT*: previous versions of the CDK CLI will not be fully compatible with this version of the framework and vice versa. * **core:** the `@aws-cdk/cdk` module was renamed to `@aws-cdk/core`, **python:** `aws_cdk.core`, **java:** the artifact `cdk` in groupId `software.amazon.awscdk` was renamed to `core` @@ -2560,7 +2591,7 @@ and to continue working closely with the open-source community. * formalize the concept of physical names, and use them for cross-environment CodePipelines. ([#1924](https://github.com/aws/aws-cdk/issues/1924)) ([6daaca8](https://github.com/aws/aws-cdk/commit/6daaca8)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **assets:** `AssetProps.packaging` has been removed and is now automatically discovered based on the file type. * **assets:** `ZipDirectoryAsset` has been removed, use `aws-s3-assets.Asset`. @@ -2704,7 +2735,7 @@ package. * **tokens:** enable type coercion ([#2680](https://github.com/aws/aws-cdk/issues/2680)) ([0f54698](https://github.com/aws/aws-cdk/commit/0f54698)), closes [#2679](https://github.com/aws/aws-cdk/issues/2679) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **route53:** `recordValue: string` prop in `route53.TxtRecord` changed to `values: string[]` * `recordValue` prop in `route53.CnameRecord` renamed to `domainName` @@ -2776,7 +2807,7 @@ package. * **cloudwatch:** support all Y-Axis properties ([#2406](https://github.com/aws/aws-cdk/issues/2406)) ([8904c3e](https://github.com/aws/aws-cdk/commit/8904c3e)), closes [#2385](https://github.com/aws/aws-cdk/issues/2385) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **logs:** using a Lambda or Kinesis Stream as CloudWatch log subscription destination now requires an integration object from the `@aws-cdk/aws-logs-destinations` package. * **codepipeline-actions:** removed the `addPutJobResultPolicy` property when creating LambdaInvokeAction. @@ -2848,7 +2879,7 @@ package. * **toolkit:** show when new version is available ([#2484](https://github.com/aws/aws-cdk/issues/2484)) ([6cf4bd3](https://github.com/aws/aws-cdk/commit/6cf4bd3)), closes [#297](https://github.com/aws/aws-cdk/issues/297) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **route53-targets:** using a CloudFront Distribution or an ELBv2 Load Balancer as an Alias Record Target now requires an integration @@ -2935,7 +2966,7 @@ corresponding `fromXxx` methods to import them as needed. * **elbv2:** add fixed response support for application load balancers ([#2328](https://github.com/aws/aws-cdk/issues/2328)) ([750bc8b](https://github.com/aws/aws-cdk/commit/750bc8b)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * all `Foo.import` static methods are now `Foo.fromFooAttributes` * all `FooImportProps` structs are now called `FooAttributes` @@ -2971,7 +3002,7 @@ corresponding `fromXxx` methods to import them as needed. -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * `s3.Bucket.domainName` renamed to `s3.Bucket.bucketDomainName`. * `codedeploy.IXxxDeploymentConfig.deploymentConfigArn` is now a property and not a method. @@ -3042,7 +3073,7 @@ corresponding `fromXxx` methods to import them as needed. * **toolkit:** stage assets under .cdk.assets ([#2182](https://github.com/aws/aws-cdk/issues/2182)) ([2f74eb4](https://github.com/aws/aws-cdk/commit/2f74eb4)), closes [#1716](https://github.com/aws/aws-cdk/issues/1716) [#2096](https://github.com/aws/aws-cdk/issues/2096) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cloudwatch:** Renamed `MetricCustomization` to `MetricOptions`. * **codepipeline:** CodePipeline Actions no longer have the `outputArtifact` and `outputArtifacts` properties. @@ -3093,7 +3124,7 @@ corresponding `fromXxx` methods to import them as needed. * update CloudFormation resource spec to v2.29.0 ([#2170](https://github.com/aws/aws-cdk/issues/2170)) ([ebc490d](https://github.com/aws/aws-cdk/commit/ebc490d)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * The `secretsmanager.SecretString` class has been removed in favor of `cdk.SecretValue.secretsManager(id[, options])` * The following prop types have been changed from `string` to `cdk.SecretValue`: `codepipeline-actions.AlexaSkillDeployAction.clientSecret`, `codepipeline-actions.AlexaSkillDeployAction.refreshToken`, `codepipeline-actions.GitHubSourceAction.oauthToken`, `iam.User.password` @@ -3145,7 +3176,7 @@ corresponding `fromXxx` methods to import them as needed. * **toolkit:** introduce the concept of auto-deployed Stacks. ([#2046](https://github.com/aws/aws-cdk/issues/2046)) ([abacc66](https://github.com/aws/aws-cdk/commit/abacc66)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **lambda:** `cloudWatchLogsRetentionTimeDays` in `@aws-cdk/aws-cloudtrail` now uses a `logs.RetentionDays` instead of a `LogRetention`. @@ -3198,7 +3229,7 @@ on all objects, `subnetsToUse` has been renamed to `subnetType`. * add more directories excluded and treated as source in the JetBrains script. ([#1961](https://github.com/aws/aws-cdk/issues/1961)) ([a1df717](https://github.com/aws/aws-cdk/commit/a1df717)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * “toCloudFormation” is now internal and should not be called directly. Instead use “app.synthesizeStack” * **ecs:** `ContainerImage.fromDockerHub` has been renamed to `ContainerImage.fromRegistry`. @@ -3312,7 +3343,7 @@ on all objects, `subnetsToUse` has been renamed to `subnetType`. * **decdk:** Prototype for declarative CDK (decdk) ([#1618](https://github.com/aws/aws-cdk/pull/1618)) ([8713ac6](https://github.com/aws/aws-cdk/commit/8713ac6)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cloudtrail:** The `CloudTrail.addS3EventSelector` accepts an options object instead of only a `ReadWriteType` value. @@ -3385,7 +3416,7 @@ For ECS's `addDefaultAutoScalingGroupCapacity()`, `instanceCount` => * **ssm:** Add L2 resource for SSM Parameters ([#1515](https://github.com/aws/aws-cdk/issues/1515)) ([9858a64](https://github.com/aws/aws-cdk/commit/9858a64)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cdk:** if you are using TagManager the API for this object has completely changed. You should no longer use TagManager directly, but instead replace this with Tag Aspects. `cdk.Tag` has been renamed to `cdk.CfnTag` to enable `cdk.Tag` to be the Tag Aspect. @@ -3434,7 +3465,7 @@ For ECS's `addDefaultAutoScalingGroupCapacity()`, `instanceCount` => * **toolkit:** disable colors if a terminal is not attached to stdout ([#1641](https://github.com/aws/aws-cdk/issues/1641)) ([58b4685](https://github.com/aws/aws-cdk/commit/58b4685)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **aws-codepipeline:** the `role` property in the CloudFormation Actions has been renamed to `deploymentRole`. * **aws-codepipeline:** the `role` property in the `app-delivery` package has been renamed to `deploymentRole`. @@ -3484,7 +3515,7 @@ communicate when this foundational work is complete. * **cloudformation:** stop generating legacy cloudformation resources ([#1493](https://github.com/aws/aws-cdk/issues/1493)) ([81b4174](https://github.com/aws/aws-cdk/commit/81b4174)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **Cross-stack references:** if you are using `export()` and `import()` to share constructs between stacks, you can stop doing that, instead of `FooImportProps` accept an `IFoo` directly on the consuming stack, and use that object as usual. * `ArnUtils.fromComponents()` and `ArnUtils.parse()` have been moved onto `Stack`. @@ -3529,7 +3560,7 @@ communicate when this foundational work is complete. * **iam:** CompositePrincipal and allow multiple principal types ([#1377](https://github.com/aws/aws-cdk/issues/1377)) ([b942ae5](https://github.com/aws/aws-cdk/commit/b942ae5)), closes [#1201](https://github.com/aws/aws-cdk/issues/1201) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **aws-cloudformation:** this changes the type of the `role` property in CFN CodePipeline Actions from `Role` to `IRole`. This is needed to use imported Roles when creating Actions. @@ -3569,7 +3600,7 @@ lower-case trailing `d`). * **toolkit:** include toolkit version in AWS::CDK::Metadata ([#1287](https://github.com/aws/aws-cdk/issues/1287)) ([5004f50](https://github.com/aws/aws-cdk/commit/5004f50)), closes [#1286](https://github.com/aws/aws-cdk/issues/1286) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **assert:** the behavior change of `haveResource` can cause tests to fail. If allowing extension of the expected values is the intended behavior, you can @@ -3601,7 +3632,7 @@ behavior. * Update to CloudFormation spec v2.16.0 ([#1280](https://github.com/aws/aws-cdk/issues/1280)) ([9df5c54](https://github.com/aws/aws-cdk/commit/9df5c54)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES * **aws-codebuild:** `ecr.RepositoryRef` has been replaced by `ecr.IRepository`, which means that `RepositoryRef.import` is now `Repository.import`. Futhermore, the CDK @@ -3659,7 +3690,7 @@ and is required instead of optional. - **toolkit:** improve diff user interface ([#1187](https://github.com/aws/aws-cdk/issues/1187)) ([9c3c5c7](https://github.com/aws/aws-cdk/commit/9c3c5c7)), closes [#1121](https://github.com/aws/aws-cdk/issues/1121) [#1120](https://github.com/aws/aws-cdk/issues/1120) - **aws-codepipeline**: switch to webhooks instead of polling by default for the GitHub ([#1074](https://github.com/aws/aws-cdk/issues/1074)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - **aws-codebuild:** this changes the way CodeBuild Sources are constructed (we moved away from multiple parameters in the constructor, in favor of the more idiomatic property interface). - **aws-elasticloadbalancingv2:** `targetGroup.listenerDependency()` has been renamed to `targetGroup.loadBalancerDependency()`. @@ -3678,7 +3709,7 @@ and is required instead of optional. - **aws-codepipeline, aws-cloudformation:** support cross-region CloudFormation pipeline action ([#1152](https://github.com/aws/aws-cdk/issues/1152)) ([8e701ad](https://github.com/aws/aws-cdk/commit/8e701ad)) - **toolkit:** print available templates when --language is omitted ([#1159](https://github.com/aws/aws-cdk/issues/1159)) ([5726c45](https://github.com/aws/aws-cdk/commit/5726c45)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - **aws-ec2:** Method signature of VpcPublicSubnet.addDefaultIGWRouteEntry changed in order to add a dependency on gateway attachment completing before creating the public route to the gateway. Instead of passing a gateway ID string, pass in a cloudformation.InternetGatewayResource object and a cloudformation.VPCGatewayAttachmentResource object. - If you were using `DockerHub.image()` to reference docker hub images, use `ContainerImage.fromDockerHub()` instead. @@ -3698,7 +3729,7 @@ and is required instead of optional. - **aws-route53:** route53 Alias record support ([#1131](https://github.com/aws/aws-cdk/issues/1131)) ([72f0124](https://github.com/aws/aws-cdk/commit/72f0124)) - **cdk:** allow Tokens to be encoded as lists ([#1144](https://github.com/aws/aws-cdk/issues/1144)) ([cd7947c](https://github.com/aws/aws-cdk/commit/cd7947c)), closes [#744](https://github.com/aws/aws-cdk/issues/744) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - **aws-codedeploy:** this changes the API of the CodeDeploy Pipeline Action to take the DeploymentGroup AWS Construct as an argument instead of the names of the Application and Deployment Group. @@ -3752,7 +3783,7 @@ and is required instead of optional. - **toolkit:** deployment ui improvements ([#1067](https://github.com/aws/aws-cdk/issues/1067)) ([c832eaf](https://github.com/aws/aws-cdk/commit/c832eaf)) - Update to CloudFormation resource specification v2.11.0 -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - The ec2.Connections object has been changed to be able to manage multiple security groups. The relevant property has been changed from `securityGroup` to `securityGroups` (an array of security group objects). - **aws-codecommit:** this modifies the default behavior of the CodeCommit Action. It also changes the internal API contract between the aws-codepipeline-api module and the CodePipeline Actions in the service packages. @@ -3815,7 +3846,7 @@ $ cdk --version - **aws-sqs:** Add grantXxx() methods ([#1004](https://github.com/aws/aws-cdk/issues/1004)) ([8c90350](https://github.com/aws/aws-cdk/commit/8c90350)) - **core:** Pre-concatenate Fn::Join ([#967](https://github.com/aws/aws-cdk/issues/967)) ([33c32a8](https://github.com/aws/aws-cdk/commit/33c32a8)), closes [#916](https://github.com/aws/aws-cdk/issues/916) [#958](https://github.com/aws/aws-cdk/issues/958) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - DynamoDB AutoScaling: Instead of `addReadAutoScaling()`, call `autoScaleReadCapacity()`, and similar for write scaling. - CloudFormation resource usage: If you use L1s, you may need to change some `XxxName` properties back into `Name`. These will match the CloudFormation property names. @@ -3857,7 +3888,7 @@ $ cdk --version - **aws-s3-deployment:** bucket deployments ([#971](https://github.com/aws/aws-cdk/issues/971)) ([84d6876](https://github.com/aws/aws-cdk/commit/84d6876)), closes [#952](https://github.com/aws/aws-cdk/issues/952) [#953](https://github.com/aws/aws-cdk/issues/953) [#954](https://github.com/aws/aws-cdk/issues/954) - **docs:** added link to CloudFormation concepts ([#934](https://github.com/aws/aws-cdk/issues/934)) ([666bbba](https://github.com/aws/aws-cdk/commit/666bbba)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - **aws-apigateway:** specifying a path no longer works. If you used to provide a '/', remove it. Otherwise, you will have to supply `proxy: false` and construct more complex resource paths yourself. - **aws-lambda:** The construct `lambda.InlineJavaScriptLambda` is no longer supported. Use `lambda.Code.inline` instead; `lambda.Runtime.NodeJS43Edge` runtime is removed. CloudFront docs [stipulate](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-lambda-function-configuration) that you should use node6.10 or node8.10\. It is always possible to use any value by instantiating a `lambda.Runtime` object. @@ -3892,7 +3923,7 @@ Java (maven) | [`mvn versions:use-latest-versions`](https://www.m - **aws-cloudformation:** add permission management to CreateUpdate and Delete Stack CodePipeline Actions. ([#880](https://github.com/aws/aws-cdk/issues/880)) ([8b3ae43](https://github.com/aws/aws-cdk/commit/8b3ae43)) - **aws-codepipeline:** make input and output artifact names optional when creating Actions. ([#845](https://github.com/aws/aws-cdk/issues/845)) ([3d91c93](https://github.com/aws/aws-cdk/commit/3d91c93)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - **aws-codepipeline:** this commit contains the following breaking changes: @@ -3980,7 +4011,7 @@ Java (maven) | [`mvn versions:use-latest-versions`](https://www.m - **toolkit:** Stop creating 'empty' stacks ([#779](https://github.com/aws/aws-cdk/issues/779)) ([1dddd8a](https://github.com/aws/aws-cdk/commit/1dddd8a)) - **aws-autoscaling, aws-ec2:** Tagging support for AutoScaling/SecurityGroup ([#766](https://github.com/aws/aws-cdk/issues/766)) ([3d48eb2](https://github.com/aws/aws-cdk/commit/3d48eb2)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - **framework:** The `cdk.App` constructor doesn't accept any arguments, and `app.run()` does not return a `string` anymore. All AWS CDK apps in all languages would need to be modified to adhere to the new API of the `cdk.App` construct. @@ -4050,7 +4081,7 @@ bucketResource.addPropertyOverride('BucketName', 'NewerBucketName'); - **core:** resource overrides (escape hatch) ([#784](https://github.com/aws/aws-cdk/issues/784)) ([5054eef](https://github.com/aws/aws-cdk/commit/5054eef)), closes [#606](https://github.com/aws/aws-cdk/issues/606) - **toolkit:** stop creating 'empty' stacks ([#779](https://github.com/aws/aws-cdk/issues/779)) ([1dddd8a](https://github.com/aws/aws-cdk/commit/1dddd8a)) -### BREAKING CHANGES +### BREAKING CHANGES TO EXPERIMENTAL FEATURES - **cdk**: the constructor signature of `TagManager` has changed. `initialTags` is now passed inside a props object. - **util:** `@aws-cdk/util` is no longer available diff --git a/lerna.json b/lerna.json index 480b7635043ca..6cd02888d14d3 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.60.0" + "version": "1.61.0" } diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index d1f78b163d71c..88f059fc30b95 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -47,8 +47,7 @@ import * as db from '@aws-cdk/aws-dynamodb'; const api = new appsync.GraphQLApi(stack, 'Api', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: join(__dirname, 'schema.graphql'), + schema: appsync.Schema.fromAsset(join(__dirname, 'schema.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM @@ -83,7 +82,53 @@ demoDS.createResolver({ }); ``` -## Imports +### Schema + +Every GraphQL Api needs a schema to define the Api. CDK offers `appsync.Schema` +for static convenience methods for various types of schema declaration: code-first +or schema-first. + +#### Code-First + +When declaring your GraphQL Api, CDK defaults to a code-first approach if the +`schema` property is not configured. + +```ts +const api = new appsync.GraphQLApi(stack, 'api', { name: 'myApi' }); +``` + +CDK will declare a `Schema` class that will give your Api access functions to +define your schema code-first: `addType`, `addObjectType`, `addToSchema`, etc. + +You can also declare your `Schema` class outside of your CDK stack, to define +your schema externally. + +```ts +const schema = new appsync.Schema(); +schema.addObjectType('demo', { + definition: { id: appsync.GraphqlType.id() }, +}); +const api = new appsync.GraphQLApi(stack, 'api', { + name: 'myApi', + schema +}); +``` + +See the [code-first schema](#Code-First-Schema) section for more details. + +#### Schema-First + +You can define your GraphQL Schema from a file on disk. For convenience, use +the `appsync.Schema.fromAsset` to specify the file representing your schema. + +```ts +const api = appsync.GraphQLApi(stack, 'api', { + name: 'myApi', + schema: appsync.Schema.fromAsset(join(__dirname, 'schema.graphl')), +}); +``` + +### Imports Any GraphQL Api that has been created outside the stack can be imported from another stack into your CDK app. Utilizing the `fromXxx` function, you have @@ -101,7 +146,7 @@ If you don't specify `graphqlArn` in `fromXxxAttributes`, CDK will autogenerate the expected `arn` for the imported api, given the `apiId`. For creating data sources and resolvers, an `apiId` is sufficient. -## Permissions +### Permissions When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role with correct permissions must be used for access to API. @@ -153,7 +198,7 @@ const api = new appsync.GraphQLApi(stack, 'API', { api.grant(role, appsync.IamResource.custom('types/Mutation/fields/updateExample'), 'appsync:GraphQL') ``` -### IamResource +#### IamResource In order to use the `grant` functions, you need to use the class `IamResource`. @@ -163,7 +208,7 @@ In order to use the `grant` functions, you need to use the class `IamResource`. - `IamResource.all()` permits ALL resources. -### Generic Permissions +#### Generic Permissions Alternatively, you can use more generic `grant` functions to accomplish the same usage. @@ -280,7 +325,6 @@ import * as schema from './object-types'; const api = new appsync.GraphQLApi(stack, 'Api', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.CODE, }); this.objectTypes = [ schema.Node, schema.Film ]; @@ -294,13 +338,12 @@ api.addType('Query', { args: schema.args, requestMappingTemplate: dummyRequest, responseMappingTemplate: dummyResponse, - }, + }), } - }); -}) +}); -this.objectTypes.map((t) => api.appendToSchema(t)); -Object.keys(filmConnections).forEach((key) => api.appendToSchema(filmConnections[key])); +this.objectTypes.map((t) => api.addType(t)); +Object.keys(filmConnections).forEach((key) => api.addType(filmConnections[key])); ``` Notice how we can utilize the `generateEdgeAndConnection` function to generate @@ -457,7 +500,6 @@ You can create Object Types in three ways: ```ts const api = new appsync.GraphQLApi(stack, 'Api', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.CODE, }); const demo = new appsync.ObjectType('Demo', { defintion: { @@ -466,7 +508,7 @@ You can create Object Types in three ways: }, }); - api.appendToSchema(object.toString()); + api.addType(object); ``` > This method allows for reusability and modularity, ideal for larger projects. For example, imagine moving all Object Type definition outside the stack. @@ -490,7 +532,7 @@ You can create Object Types in three ways: `cdk-stack.ts` - a file containing our cdk stack ```ts import { demo } from './object-types'; - api.appendToSchema(demo.toString()); + api.addType(demo); ``` 2. Object Types can be created ***externally*** from an Interface Type. @@ -513,7 +555,6 @@ You can create Object Types in three ways: ```ts const api = new appsync.GraphQLApi(stack, 'Api', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.CODE, }); api.addType('Demo', { defintion: { diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 0384899f1ba18..554bc3be77464 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -1,10 +1,10 @@ -import { readFileSync } from 'fs'; import { IUserPool } from '@aws-cdk/aws-cognito'; import { ManagedPolicy, Role, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam'; import { CfnResource, Construct, Duration, IResolvable, Stack } from '@aws-cdk/core'; import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'; import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base'; -import { ObjectType, ObjectTypeProps } from './schema-intermediate'; +import { Schema } from './schema'; +import { IIntermediateType } from './schema-base'; /** * enum with all possible values for AppSync authorization type @@ -201,21 +201,6 @@ export interface LogConfig { readonly fieldLogLevel?: FieldLogLevel; } -/** - * Enum containing the different modes of schema definition - */ -export enum SchemaDefinition { - /** - * Define schema through functions like addType, addQuery, etc. - */ - CODE = 'CODE', - - /** - * Define schema in a file, i.e. schema.graphql - */ - FILE = 'FILE', -} - /** * Properties for an AppSync GraphQL API */ @@ -242,18 +227,13 @@ export interface GraphQLApiProps { /** * GraphQL schema definition. Specify how you want to define your schema. * - * SchemaDefinition.CODE allows schema definition through CDK - * SchemaDefinition.FILE allows schema definition through schema.graphql file + * Schema.fromFile(filePath: string) allows schema definition through schema.graphql file * - * @experimental - */ - readonly schemaDefinition: SchemaDefinition; - /** - * File containing the GraphQL schema definition. You have to specify a definition or a file containing one. + * @default - schema will be generated code-first (i.e. addType, addObjectType, etc.) * - * @default - Use schemaDefinition + * @experimental */ - readonly schemaDefinitionFile?: string; + readonly schema?: Schema; /** * A flag indicating whether or not X-Ray tracing is enabled for the GraphQL API. * @@ -390,9 +370,9 @@ export class GraphQLApi extends GraphqlApiBase { public readonly name: string; /** - * underlying CFN schema resource + * the schema attached to this api */ - public readonly schema: CfnGraphQLSchema; + public readonly schema: Schema; /** * the configured API key, if present @@ -401,9 +381,9 @@ export class GraphQLApi extends GraphqlApiBase { */ public readonly apiKey?: string; - private schemaMode: SchemaDefinition; + private schemaResource: CfnGraphQLSchema; private api: CfnGraphQLApi; - private _apiKey?: CfnApiKey; + private apiKeyResource?: CfnApiKey; constructor(scope: Construct, id: string, props: GraphQLApiProps) { super(scope, id); @@ -429,16 +409,16 @@ export class GraphQLApi extends GraphqlApiBase { this.arn = this.api.attrArn; this.graphQlUrl = this.api.attrGraphQlUrl; this.name = this.api.name; - this.schemaMode = props.schemaDefinition; - this.schema = this.defineSchema(props.schemaDefinitionFile); + this.schema = props.schema ?? new Schema(); + this.schemaResource = this.schema.bind(this); if (modes.some((mode) => mode.authorizationType === AuthorizationType.API_KEY)) { const config = modes.find((mode: AuthorizationMode) => { return mode.authorizationType === AuthorizationType.API_KEY && mode.apiKeyConfig; })?.apiKeyConfig; - this._apiKey = this.createAPIKey(config); - this._apiKey.addDependsOn(this.schema); - this.apiKey = this._apiKey.attrApiKey; + this.apiKeyResource = this.createAPIKey(config); + this.apiKeyResource.addDependsOn(this.schemaResource); + this.apiKey = this.apiKeyResource.attrApiKey; } } @@ -515,7 +495,7 @@ export class GraphQLApi extends GraphqlApiBase { * @param construct the dependee */ public addSchemaDependency(construct: CfnResource): boolean { - construct.addDependsOn(this.schema); + construct.addDependsOn(this.schemaResource); return true; } @@ -584,29 +564,6 @@ export class GraphQLApi extends GraphqlApiBase { }); } - /** - * Define schema based on props configuration - * @param file the file name/s3 location of Schema - */ - private defineSchema(file?: string): CfnGraphQLSchema { - let definition; - - if ( this.schemaMode === SchemaDefinition.FILE && !file) { - throw new Error('schemaDefinitionFile must be configured if using FILE definition mode.'); - } else if ( this.schemaMode === SchemaDefinition.FILE && file ) { - definition = readFileSync(file).toString('utf-8'); - } else if ( this.schemaMode === SchemaDefinition.CODE && !file ) { - definition = ''; - } else if ( this.schemaMode === SchemaDefinition.CODE && file) { - throw new Error('definition mode CODE is incompatible with file definition. Change mode to FILE/S3 or unconfigure schemaDefinitionFile'); - } - - return new CfnGraphQLSchema(this, 'Schema', { - apiId: this.apiId, - definition, - }); - } - /** * Escape hatch to append to Schema as desired. Will always result * in a newline. @@ -617,31 +574,18 @@ export class GraphQLApi extends GraphqlApiBase { * * @experimental */ - public appendToSchema(addition: string, delimiter?: string): void { - if ( this.schemaMode !== SchemaDefinition.CODE ) { - throw new Error('API cannot append to schema because schema definition mode is not configured as CODE.'); - } - const sep = delimiter ?? ''; - this.schema.definition = `${this.schema.definition}${sep}${addition}\n`; + public addToSchema(addition: string, delimiter?: string): void { + this.schema.addToSchema(addition, delimiter); } /** - * Add an object type to the schema + * Add type to the schema * - * @param name the name of the object type - * @param props the definition + * @param type the intermediate type to add to the schema * * @experimental */ - public addType(name: string, props: ObjectTypeProps): ObjectType { - if ( this.schemaMode !== SchemaDefinition.CODE ) { - throw new Error('API cannot add type because schema definition mode is not configured as CODE.'); - }; - const type = new ObjectType(name, { - definition: props.definition, - directives: props.directives, - }); - this.appendToSchema(type.toString()); - return type; + public addType(type: IIntermediateType): IIntermediateType { + return this.schema.addType(type); } } diff --git a/packages/@aws-cdk/aws-appsync/lib/index.ts b/packages/@aws-cdk/aws-appsync/lib/index.ts index 341312977c5d3..c4e05e9a5300e 100644 --- a/packages/@aws-cdk/aws-appsync/lib/index.ts +++ b/packages/@aws-cdk/aws-appsync/lib/index.ts @@ -4,6 +4,7 @@ export * from './key'; export * from './data-source'; export * from './mapping-template'; export * from './resolver'; +export * from './schema'; export * from './schema-intermediate'; export * from './schema-field'; export * from './schema-base'; diff --git a/packages/@aws-cdk/aws-appsync/lib/private.ts b/packages/@aws-cdk/aws-appsync/lib/private.ts index 9118b503349c3..25e99fa93b753 100644 --- a/packages/@aws-cdk/aws-appsync/lib/private.ts +++ b/packages/@aws-cdk/aws-appsync/lib/private.ts @@ -1,7 +1,75 @@ -function concatAndDedup(left: T[], right: T[]): T[] { - return left.concat(right).filter((elem, index, self) => { - return index === self.indexOf(elem); - }); +import { Directive } from './schema-base'; +import { InterfaceType } from './schema-intermediate'; + +/** + * Utility enum for Schema class + */ +export enum SchemaMode { + FILE = 'FILE', + CODE = 'CODE', +}; + +/** + * Generates an addition to the schema + * + * ``` + * prefix name interfaces directives { + * field + * field + * ... + * } + * ``` + */ +export interface SchemaAdditionOptions { + /** + * the prefix for this additon (type, interface, enum, input, schema) + */ + readonly prefix: string; + /** + * the name for this addition (some additions dont need this [i.e. schema]) + * + * @default - no name + */ + readonly name?: string; + /** + * the interface types if this is creating an object type + * + * @default - no interfaces + */ + readonly interfaceTypes?: InterfaceType[]; + /** + * the directives for this type + * + * @default - no directives + */ + readonly directives?: Directive[]; + /** + * the fields to reduce onto the addition + */ + readonly fields: string[]; +} + +/** + * Generates an addition to the schema + * + * @param options the options to produced a stringfied addition + * + * @returns the following shape: + * + * ``` + * prefix name interfaces directives { + * field + * field + * ... + * } + * ``` + */ +export function shapeAddition(options: SchemaAdditionOptions): string { + const typeName = (): string => { return options.name ? ` ${options.name}` : ''; }; + const interfaces = generateInterfaces(options.interfaceTypes); + const directives = generateDirectives(options.directives); + return options.fields.reduce((acc, field) => + `${acc} ${field}\n`, `${options.prefix}${typeName()}${interfaces}${directives} {\n`) + '}'; } /** @@ -110,4 +178,33 @@ export class Between extends BaseKeyCondition { public args(): string[] { return [this.arg1, this.arg2]; } +} + +function concatAndDedup(left: T[], right: T[]): T[] { + return left.concat(right).filter((elem, index, self) => { + return index === self.indexOf(elem); + }); +} + +/** + * Utility function to generate interfaces for object types + * + * @param interfaceTypes the interfaces this object type implements + */ +function generateInterfaces(interfaceTypes?: InterfaceType[]): string { + if (!interfaceTypes || interfaceTypes.length === 0) return ''; + return interfaceTypes.reduce((acc, interfaceType) => + `${acc} ${interfaceType.name},`, ' implements').slice(0, -1); +} + +/** + * Utility function to generate directives + * + * @param directives the directives of a given type + * @param delimiter the separator betweeen directives (by default we will add a space) + */ +function generateDirectives(directives?: Directive[], delimiter?: string): string { + if (!directives || directives.length === 0) return ''; + return directives.reduce((acc, directive) => + `${acc}${directive.statement}${delimiter ?? ' '}`, ' ').slice(0, -1); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-base.ts b/packages/@aws-cdk/aws-appsync/lib/schema-base.ts index c3f67fe46a595..07659a9101dc1 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema-base.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema-base.ts @@ -1,5 +1,5 @@ import { Resolver } from './resolver'; -import { ResolvableFieldOptions } from './schema-field'; +import { ResolvableFieldOptions, BaseTypeOptions, GraphqlType } from './schema-field'; import { InterfaceType } from './schema-intermediate'; /** @@ -105,6 +105,16 @@ export interface IIntermediateType { */ readonly intermediateType?: InterfaceType; + /** + * Create an GraphQL Type representing this Intermediate Type + * + * @param options the options to configure this attribute + * - isList + * - isRequired + * - isRequiredList + */ + attribute(options?: BaseTypeOptions): GraphqlType; + /** * Generate the string of this object type */ diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts b/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts index 4102e506d013f..7cbddac56dddc 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts @@ -1,3 +1,4 @@ +import { shapeAddition } from './private'; import { Resolver } from './resolver'; import { Directive, IField, IIntermediateType } from './schema-base'; import { BaseTypeOptions, GraphqlType, ResolvableFieldOptions } from './schema-field'; @@ -59,13 +60,12 @@ export class InterfaceType implements IIntermediateType { * Generate the string of this object type */ public toString(): string { - let schemaAddition = `interface ${this.name} {\n`; - Object.keys(this.definition).forEach( (key) => { - const attribute = this.definition[key]; - const args = attribute.argsToString(); - schemaAddition = `${schemaAddition} ${key}${args}: ${attribute.toString()}\n`; + return shapeAddition({ + prefix: 'interface', + name: this.name, + fields: Object.keys(this.definition).map((key) => + `${key}${this.definition[key].argsToString()}: ${this.definition[key].toString()}`), }); - return `${schemaAddition}}`; } /** @@ -159,38 +159,14 @@ export class ObjectType extends InterfaceType implements IIntermediateType { * Generate the string of this object type */ public toString(): string { - let title = this.name; - if (this.interfaceTypes && this.interfaceTypes.length) { - title = `${title} implements`; - this.interfaceTypes.map((interfaceType) => { - title = `${title} ${interfaceType.name},`; - }); - title = title.slice(0, -1); - } - const directives = this.generateDirectives(this.directives); - let schemaAddition = `type ${title} ${directives}{\n`; - Object.keys(this.definition).forEach( (key) => { - const attribute = this.definition[key]; - const args = attribute.argsToString(); - schemaAddition = `${schemaAddition} ${key}${args}: ${attribute.toString()}\n`; - }); - return `${schemaAddition}}`; - } - - /** - * Utility function to generate directives - * - * @param directives the directives of a given type - * @param delimiter the separator betweeen directives - * @default - ' ' - */ - private generateDirectives(directives?: Directive[], delimiter?: string): string { - let schemaAddition = ''; - if (!directives) { return schemaAddition; } - directives.map((directive) => { - schemaAddition = `${schemaAddition}${directive.statement}${delimiter ?? ' '}`; + return shapeAddition({ + prefix: 'type', + name: this.name, + interfaceTypes: this.interfaceTypes, + directives: this.directives, + fields: Object.keys(this.definition).map((key) => + `${key}${this.definition[key].argsToString()}: ${this.definition[key].toString()}`), }); - return schemaAddition; } /** diff --git a/packages/@aws-cdk/aws-appsync/lib/schema.ts b/packages/@aws-cdk/aws-appsync/lib/schema.ts new file mode 100644 index 0000000000000..7d3df6b569ae1 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/lib/schema.ts @@ -0,0 +1,109 @@ +import { readFileSync } from 'fs'; +import { Lazy } from '@aws-cdk/core'; +import { CfnGraphQLSchema } from './appsync.generated'; +import { GraphQLApi } from './graphqlapi'; +import { SchemaMode } from './private'; +import { IIntermediateType } from './schema-base'; + +/** + * The options for configuring a schema + * + * If no options are specified, then the schema will + * be generated code-first. + */ +export interface SchemaOptions { + /** + * The file path for the schema. When this option is + * configured, then the schema will be generated from an + * existing file from disk. + * + * @default - schema not configured through disk asset + */ + readonly filePath?: string, +}; + +/** + * The Schema for a GraphQL Api + * + * If no options are configured, schema will be generated + * code-first. + */ +export class Schema { + /** + * Generate a Schema from file + * + * @returns `SchemaAsset` with immutable schema defintion + * @param filePath the file path of the schema file + */ + public static fromAsset(filePath: string): Schema { + return new Schema({ filePath }); + } + + /** + * The definition for this schema + */ + public definition: string; + + protected schema?: CfnGraphQLSchema; + + private mode: SchemaMode; + + public constructor(options?: SchemaOptions) { + if (options?.filePath) { + this.mode = SchemaMode.FILE; + this.definition = readFileSync(options.filePath).toString('utf-8'); + } else { + this.mode = SchemaMode.CODE; + this.definition = ''; + } + } + + /** + * Called when the GraphQL Api is initialized to allow this object to bind + * to the stack. + * + * @param api The binding GraphQL Api + */ + public bind(api: GraphQLApi): CfnGraphQLSchema { + if (!this.schema) { + this.schema = new CfnGraphQLSchema(api, 'Schema', { + apiId: api.apiId, + definition: Lazy.stringValue({ produce: () => this.definition }), + }); + } + return this.schema; + } + + /** + * Escape hatch to add to Schema as desired. Will always result + * in a newline. + * + * @param addition the addition to add to schema + * @param delimiter the delimiter between schema and addition + * @default - '' + * + * @experimental + */ + public addToSchema(addition: string, delimiter?: string): void { + if (this.mode !== SchemaMode.CODE) { + throw new Error('API cannot append to schema because schema definition mode is not configured as CODE.'); + } + const sep = delimiter ?? ''; + this.definition = `${this.definition}${sep}${addition}\n`; + } + + /** + * Add type to the schema + * + * @param type the intermediate type to add to the schema + * + * @experimental + */ + public addType(type: IIntermediateType): IIntermediateType { + if (this.mode !== SchemaMode.CODE) { + throw new Error('API cannot add type because schema definition mode is not configured as CODE.'); + } + this.addToSchema(Lazy.stringValue({ produce: () => type.toString() })); + return type; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts index 84cfa79aac5fd..e9ad68e4e66e0 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts @@ -15,8 +15,7 @@ describe('AppSync API Key Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), }); // THEN @@ -27,8 +26,7 @@ describe('AppSync API Key Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM }, additionalAuthorizationModes: [ @@ -45,8 +43,7 @@ describe('AppSync API Key Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM }, }, @@ -60,8 +57,7 @@ describe('AppSync API Key Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM }, additionalAuthorizationModes: [], @@ -76,8 +72,7 @@ describe('AppSync API Key Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM }, additionalAuthorizationModes: [{ @@ -97,8 +92,7 @@ describe('AppSync API Key Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM }, additionalAuthorizationModes: [ @@ -125,8 +119,7 @@ describe('AppSync API Key Authorization', () => { expect(() => { new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.API_KEY, @@ -141,8 +134,7 @@ describe('AppSync API Key Authorization', () => { expect(() => { new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.API_KEY }, additionalAuthorizationModes: [{ @@ -158,8 +150,7 @@ describe('AppSync API Key Authorization', () => { expect(() => { new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM }, additionalAuthorizationModes: [ @@ -177,8 +168,7 @@ describe('AppSync IAM Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM }, }, @@ -194,8 +184,7 @@ describe('AppSync IAM Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.IAM }], }, @@ -212,8 +201,7 @@ describe('AppSync IAM Authorization', () => { expect(() => { new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM }, additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.IAM }], @@ -227,8 +215,7 @@ describe('AppSync IAM Authorization', () => { expect(() => { new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { additionalAuthorizationModes: [ { authorizationType: appsync.AuthorizationType.IAM }, @@ -249,8 +236,7 @@ describe('AppSync User Pool Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.USER_POOL, @@ -273,8 +259,7 @@ describe('AppSync User Pool Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.USER_POOL, @@ -303,8 +288,7 @@ describe('AppSync User Pool Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.USER_POOL, @@ -329,8 +313,7 @@ describe('AppSync User Pool Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.USER_POOL, @@ -360,8 +343,7 @@ describe('AppSync User Pool Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.USER_POOL, @@ -417,8 +399,7 @@ describe('AppSync OIDC Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.OIDC, @@ -440,8 +421,7 @@ describe('AppSync OIDC Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.OIDC, @@ -471,8 +451,7 @@ describe('AppSync OIDC Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.OIDC, @@ -496,8 +475,7 @@ describe('AppSync OIDC Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { additionalAuthorizationModes: [{ authorizationType: appsync.AuthorizationType.OIDC, @@ -529,8 +507,7 @@ describe('AppSync OIDC Authorization', () => { // WHEN new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.OIDC, diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-code-first.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-code-first.test.ts index 8258bbf38010e..0b39cab4c4c24 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-code-first.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-code-first.test.ts @@ -4,20 +4,207 @@ import * as appsync from '../lib'; import * as t from './scalar-type-defintions'; let stack: cdk.Stack; -let api: appsync.GraphQLApi; beforeEach(() => { // GIVEN stack = new cdk.Stack(); - api = new appsync.GraphQLApi(stack, 'api', { - name: 'api', - schemaDefinition: appsync.SchemaDefinition.CODE, +}); + +describe('code-first implementation through GraphQL Api functions`', () => { + let api: appsync.GraphQLApi; + beforeEach(() => { + // GIVEN + api = new appsync.GraphQLApi(stack, 'api', { + name: 'api', + }); + }); + + test('testing addType w/ Interface Type for schema definition mode `code`', () => { + // WHEN + const test = new appsync.InterfaceType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + }, + }); + api.addType(test); + test.addField('dupid', t.dup_id); + const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('testing addType w/ Object Type for schema definition mode `code`', () => { + // WHEN + const test = new appsync.ObjectType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + }, + }); + api.addType(test); + test.addField('dupid', t.dup_id); + const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('testing addObjectType for schema definition mode `code`', () => { + // WHEN + api.addType(new appsync.ObjectType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + dupid: t.dup_id, + }, + })); + const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('addField dynamically adds field to schema', () => { + // WHEN + const test = api.addType(new appsync.ObjectType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + }, + })); + + test.addField('dupid', t.dup_id); + const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('testing addInterfaceType for schema definition mode `code`', () => { + // WHEN + api.addType(new appsync.InterfaceType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + dupid: t.dup_id, + }, + })); + const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('addField dynamically adds field to schema', () => { + // WHEN + const test = api.addType(new appsync.InterfaceType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + }, + })); + + test.addField('dupid', t.dup_id); + const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); }); }); -describe('testing addType for schema definition mode `code`', () => { - test('check scalar type id with all options', () => { +describe('code-first implementation through Schema functions`', () => { + let schema: appsync.Schema; + beforeEach(() => { + // GIVEN + schema = new appsync.Schema(); + }); + + test('testing addType w/ Interface Type for schema definition mode `code`', () => { // WHEN - api.addType('Test', { + const test = new appsync.InterfaceType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + }, + }); + schema.addType(test); + test.addField('dupid', t.dup_id); + + new appsync.GraphQLApi(stack, 'api', { + name: 'api', + schema, + }); + const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('testing addType w/ Object Type for schema definition mode `code`', () => { + // WHEN + const test = new appsync.ObjectType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + }, + }); + schema.addType(test); + test.addField('dupid', t.dup_id); + + new appsync.GraphQLApi(stack, 'api', { + name: 'api', + schema, + }); + const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('testing addObjectType for schema definition mode `code`', () => { + // WHEN + schema.addType(new appsync.ObjectType('Test', { definition: { id: t.id, lid: t.list_id, @@ -26,7 +213,13 @@ describe('testing addType for schema definition mode `code`', () => { rlrid: t.required_list_required_id, dupid: t.dup_id, }, + })); + + new appsync.GraphQLApi(stack, 'api', { + name: 'api', + schema, }); + const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; // THEN @@ -35,4 +228,77 @@ describe('testing addType for schema definition mode `code`', () => { }); }); -}); \ No newline at end of file + test('addField dynamically adds field to schema', () => { + // WHEN + const test = schema.addType(new appsync.ObjectType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + }, + })); + + test.addField('dupid', t.dup_id); + new appsync.GraphQLApi(stack, 'api', { + name: 'api', + schema, + }); + const out = 'type Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('testing addInterfaceType for schema definition mode `code`', () => { + // WHEN + schema.addType(new appsync.InterfaceType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + dupid: t.dup_id, + }, + })); + new appsync.GraphQLApi(stack, 'api', { + name: 'api', + schema, + }); + const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('addField dynamically adds field to schema', () => { + // WHEN + const test = schema.addType(new appsync.InterfaceType('Test', { + definition: { + id: t.id, + lid: t.list_id, + rid: t.required_id, + rlid: t.required_list_id, + rlrid: t.required_list_required_id, + }, + })); + + test.addField('dupid', t.dup_id); + new appsync.GraphQLApi(stack, 'api', { + name: 'api', + schema, + }); + const out = 'interface Test {\n id: ID\n lid: [ID]\n rid: ID!\n rlid: [ID]!\n rlrid: [ID!]!\n dupid: [ID!]!\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts index 12d8d543c8556..e54a9576396d1 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts @@ -15,8 +15,7 @@ beforeEach(() => { stack = new cdk.Stack(); api = new appsync.GraphQLApi(stack, 'baseApi', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), }); }); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-grant.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-grant.test.ts index 25ef663be23cc..44251cd7fabee 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-grant.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-grant.test.ts @@ -15,8 +15,7 @@ beforeEach(() => { }); api = new appsync.GraphQLApi(stack, 'API', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(join(__dirname, 'appsync.test.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM, diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-http.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-http.test.ts index eac1415be5a67..899a76ebd19f4 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-http.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-http.test.ts @@ -11,8 +11,7 @@ beforeEach(() => { stack = new cdk.Stack(); api = new appsync.GraphQLApi(stack, 'baseApi', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), }); endpoint = 'aws.amazon.com'; }); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-interface-type.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-interface-type.test.ts index f6b2c35d31c2c..981ec8b60a039 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-interface-type.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-interface-type.test.ts @@ -10,7 +10,6 @@ beforeEach(() => { stack = new cdk.Stack(); api = new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.CODE, }); }); @@ -25,7 +24,7 @@ describe('testing InterfaceType properties', () => { }); test('basic InterfaceType produces correct schema', () => { // WHEN - api.appendToSchema(baseTest.toString()); + api.addToSchema(baseTest.toString()); const out = 'interface baseTest {\n id: ID\n}\n'; // THEN @@ -40,7 +39,7 @@ describe('testing InterfaceType properties', () => { returnType: t.string, args: { success: t.int }, })); - api.appendToSchema(baseTest.toString()); + api.addToSchema(baseTest.toString()); const out = 'interface baseTest {\n id: ID\n test(success: Int): String\n}\n'; // THEN @@ -56,7 +55,7 @@ describe('testing InterfaceType properties', () => { args: { success: t.int }, dataSource: api.addNoneDataSource('none'), })); - api.appendToSchema(baseTest.toString()); + api.addToSchema(baseTest.toString()); const out = 'interface baseTest {\n id: ID\n test(success: Int): String\n}\n'; // THEN @@ -75,7 +74,7 @@ describe('testing InterfaceType properties', () => { test: graphqlType, }, }); - api.appendToSchema(test.toString()); + api.addToSchema(test.toString()); const out = 'type Test {\n test: baseTest\n}\n'; // THEN diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts index b4e35c0058e55..a67fd4b1691b7 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts @@ -11,8 +11,7 @@ beforeEach(() => { stack = new cdk.Stack(); api = new appsync.GraphQLApi(stack, 'baseApi', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), }); }); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-none.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-none.test.ts index e391d16bfb6b2..f2b52c7dfba03 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-none.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-none.test.ts @@ -10,8 +10,7 @@ beforeEach(() => { stack = new cdk.Stack(); api = new appsync.GraphQLApi(stack, 'baseApi', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), }); }); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts index 1d4359b735697..a60a5242f6fe7 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts @@ -10,7 +10,6 @@ beforeEach(() => { stack = new cdk.Stack(); api = new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.CODE, }); }); @@ -30,8 +29,8 @@ describe('testing Object Type properties', () => { directives: [appsync.Directive.custom('@test')], }); - api.appendToSchema(baseTest.toString()); - api.appendToSchema(objectTest.toString()); + api.addToSchema(baseTest.toString()); + api.addToSchema(objectTest.toString()); const gql_interface = 'interface baseTest {\n id: ID\n}\n'; const gql_object = 'type objectTest implements baseTest @test {\n id2: ID\n id: ID\n}\n'; const out = `${gql_interface}${gql_object}`; @@ -57,9 +56,9 @@ describe('testing Object Type properties', () => { }, }); - api.appendToSchema(baseTest.toString()); - api.appendToSchema(anotherTest.toString()); - api.appendToSchema(objectTest.toString()); + api.addToSchema(baseTest.toString()); + api.addToSchema(anotherTest.toString()); + api.addToSchema(objectTest.toString()); const gql_interface = 'interface baseTest {\n id: ID\n}\ninterface anotherTest {\n id2: ID\n}\n'; const gql_object = 'type objectTest implements anotherTest, baseTest {\n id3: ID\n id2: ID\n id: ID\n}\n'; @@ -84,7 +83,7 @@ describe('testing Object Type properties', () => { test: graphqlType, }, }); - api.appendToSchema(test.toString()); + api.addToSchema(test.toString()); const out = 'type Test {\n test: baseTest\n}\n'; // THEN @@ -108,7 +107,7 @@ describe('testing Object Type properties', () => { resolve: field, }, }); - api.appendToSchema(test.toString()); + api.addToSchema(test.toString()); const out = 'type Test {\n test: String\n resolve(arg: Int): String\n}\n'; // THEN @@ -132,7 +131,7 @@ describe('testing Object Type properties', () => { resolve: field, }, }); - api.appendToSchema(test.toString()); + api.addToSchema(test.toString()); const out = 'type Test {\n test: String\n resolve(arg: Int): String\n}\n'; // THEN @@ -155,7 +154,7 @@ describe('testing Object Type properties', () => { }), }, }); - api.appendToSchema(test.toString()); + api.addToSchema(test.toString()); // THEN expect(stack).toHaveResourceLike('AWS::AppSync::Resolver', { @@ -182,7 +181,7 @@ describe('testing Object Type properties', () => { // test.addField('resolve', field); test.addField('dynamic', t.string); - api.appendToSchema(test.toString()); + api.addToSchema(test.toString()); const out = 'type Test {\n test: String\n resolve(arg: Int): String\n dynamic: String\n}\n'; // THEN @@ -215,7 +214,7 @@ describe('testing Object Type properties', () => { // test.addField('resolve', field); test.addField('dynamic', garbage.attribute()); - api.appendToSchema(test.toString()); + api.addToSchema(test.toString()); const out = 'type Test {\n test: String\n resolve(arg: Garbage): Garbage\n dynamic: Garbage\n}\n'; // THEN diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-scalar-type.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-scalar-type.test.ts index fb8165384d8b6..64fddd55bc3d3 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-scalar-type.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-scalar-type.test.ts @@ -10,18 +10,17 @@ beforeEach(() => { stack = new cdk.Stack(); api = new appsync.GraphQLApi(stack, 'api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.CODE, }); }); describe('testing all GraphQL Types', () => { test('scalar type id', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.id, }, - }); + })); const out = 'type Test {\n id: ID\n}\n'; // THEN @@ -32,11 +31,11 @@ describe('testing all GraphQL Types', () => { test('scalar type string', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.string, }, - }); + })); const out = 'type Test {\n id: String\n}\n'; // THEN @@ -47,11 +46,11 @@ describe('testing all GraphQL Types', () => { test('scalar type int', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.int, }, - }); + })); const out = 'type Test {\n id: Int\n}\n'; // THEN @@ -62,11 +61,11 @@ describe('testing all GraphQL Types', () => { test('scalar type float', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.float, }, - }); + })); const out = 'type Test {\n id: Float\n}\n'; // THEN @@ -77,11 +76,11 @@ describe('testing all GraphQL Types', () => { test('scalar type boolean', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.boolean, }, - }); + })); const out = 'type Test {\n id: Boolean\n}\n'; // THEN @@ -92,11 +91,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSDate', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.awsDate, }, - }); + })); const out = 'type Test {\n id: AWSDate\n}\n'; // THEN @@ -107,11 +106,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSTime', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.awsTime, }, - }); + })); const out = 'type Test {\n id: AWSTime\n}\n'; // THEN @@ -122,11 +121,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSDateTime', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.awsDateTime, }, - }); + })); const out = 'type Test {\n id: AWSDateTime\n}\n'; // THEN @@ -137,11 +136,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSTimestamp', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.awsTimestamp, }, - }); + })); const out = 'type Test {\n id: AWSTimestamp\n}\n'; // THEN @@ -152,11 +151,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSEmail', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.awsEmail, }, - }); + })); const out = 'type Test {\n id: AWSEmail\n}\n'; // THEN @@ -167,11 +166,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSJSON', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.awsJson, }, - }); + })); const out = 'type Test {\n id: AWSJSON\n}\n'; // THEN @@ -183,11 +182,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSUrl', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.awsUrl, }, - }); + })); const out = 'type Test {\n id: AWSURL\n}\n'; // THEN @@ -198,11 +197,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSPhone', () => { // WHEN - api.addType('Test', { + api.addType(new appsync.ObjectType('Test', { definition: { id: t.awsPhone, }, - }); + })); const out = 'type Test {\n id: AWSPhone\n}\n'; // THEN @@ -213,11 +212,11 @@ describe('testing all GraphQL Types', () => { test('scalar type AWSIPAddress', () => { // WHEN - api.addType('Test', { + api.addType( new appsync.ObjectType('Test', { definition: { id: t.awsIpAddress, }, - }); + })); const out = 'type Test {\n id: AWSIPAddress\n}\n'; // THEN diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts index ee358b658fab3..b229061366117 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts @@ -21,7 +21,6 @@ describe('basic testing schema definition mode `code`', () => { // WHEN new appsync.GraphQLApi(stack, 'API', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.CODE, }); // THEN @@ -30,33 +29,20 @@ describe('basic testing schema definition mode `code`', () => { }); }); - test('definition mode `code` generates correct schema with appendToSchema', () => { + test('definition mode `code` generates correct schema with addToSchema', () => { // WHEN const api = new appsync.GraphQLApi(stack, 'API', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.CODE, }); - api.appendToSchema(type); - api.appendToSchema(query); - api.appendToSchema(mutation); + api.addToSchema(type); + api.addToSchema(query); + api.addToSchema(mutation); // THEN expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { Definition: `${type}\n${query}\n${mutation}\n`, }); }); - - test('definition mode `code` errors when schemaDefinitionFile is configured', () => { - // THEN - expect(() => { - new appsync.GraphQLApi(stack, 'API', { - name: 'demo', - schemaDefinition: appsync.SchemaDefinition.CODE, - schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), - }); - }).toThrowError('definition mode CODE is incompatible with file definition. Change mode to FILE/S3 or unconfigure schemaDefinitionFile'); - }); - }); describe('testing schema definition mode `file`', () => { @@ -65,8 +51,7 @@ describe('testing schema definition mode `file`', () => { // WHEN new appsync.GraphQLApi(stack, 'API', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(join(__dirname, 'appsync.test.graphql')), }); // THEN @@ -75,43 +60,46 @@ describe('testing schema definition mode `file`', () => { }); }); - test('definition mode `file` errors when schemaDefinitionFile is not configured', () => { + test('definition mode `file` errors when addObjectType is called', () => { + // WHEN + const api = new appsync.GraphQLApi(stack, 'API', { + name: 'demo', + schema: appsync.Schema.fromAsset(join(__dirname, 'appsync.test.graphql')), + }); + // THEN expect(() => { - new appsync.GraphQLApi(stack, 'API', { - name: 'demo', - schemaDefinition: appsync.SchemaDefinition.FILE, - }); - }).toThrowError('schemaDefinitionFile must be configured if using FILE definition mode.'); + api.addType(new appsync.ObjectType('blah', { + definition: { fail: t.id }, + })); + }).toThrowError('API cannot add type because schema definition mode is not configured as CODE.'); }); - test('definition mode `file` errors when addType is called', () => { + test('definition mode `file` errors when addInterfaceType is called', () => { // WHEN const api = new appsync.GraphQLApi(stack, 'API', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(join(__dirname, 'appsync.test.graphql')), }); // THEN expect(() => { - api.addType('blah', { + api.addType(new appsync.InterfaceType('blah', { definition: { fail: t.id }, - }); + })); }).toThrowError('API cannot add type because schema definition mode is not configured as CODE.'); }); - test('definition mode `file` errors when appendToSchema is called', () => { + test('definition mode `file` errors when addToSchema is called', () => { // WHEN const api = new appsync.GraphQLApi(stack, 'API', { name: 'demo', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(join(__dirname, 'appsync.test.graphql')), }); // THEN expect(() => { - api.appendToSchema('blah'); + api.addToSchema('blah'); }).toThrowError('API cannot append to schema because schema definition mode is not configured as CODE.'); }); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts index 9008f9b117d47..9dc6808a672e1 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts @@ -3,18 +3,19 @@ import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; import * as appsync from '../lib'; -test('appsync should configure pipeline when pipelineConfig has contents', () => { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const api = new appsync.GraphQLApi(stack, 'api', { +let stack: cdk.Stack; +let api: appsync.GraphQLApi; +beforeEach(() => { + stack = new cdk.Stack(); + api = new appsync.GraphQLApi(stack, 'api', { authorizationConfig: {}, name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), }); +}); +test('appsync should configure pipeline when pipelineConfig has contents', () => { + // WHEN new appsync.Resolver(stack, 'resolver', { api: api, typeName: 'test', @@ -30,17 +31,7 @@ test('appsync should configure pipeline when pipelineConfig has contents', () => }); test('appsync should configure resolver as unit when pipelineConfig is empty', () => { - // GIVEN - const stack = new cdk.Stack(); - // WHEN - const api = new appsync.GraphQLApi(stack, 'api', { - authorizationConfig: {}, - name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), - }); - new appsync.Resolver(stack, 'resolver', { api: api, typeName: 'test', @@ -54,17 +45,7 @@ test('appsync should configure resolver as unit when pipelineConfig is empty', ( }); test('appsync should configure resolver as unit when pipelineConfig is empty array', () => { - // GIVEN - const stack = new cdk.Stack(); - // WHEN - const api = new appsync.GraphQLApi(stack, 'api', { - authorizationConfig: {}, - name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), - }); - new appsync.Resolver(stack, 'resolver', { api: api, typeName: 'test', @@ -79,15 +60,11 @@ test('appsync should configure resolver as unit when pipelineConfig is empty arr }); test('when xray is enabled should not throw an Error', () => { - // GIVEN - const stack = new cdk.Stack(); - // WHEN - new appsync.GraphQLApi(stack, 'api', { + new appsync.GraphQLApi(stack, 'api-x-ray', { authorizationConfig: {}, name: 'api', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), xrayEnabled: true, }); diff --git a/packages/@aws-cdk/aws-appsync/test/integ.api-import.ts b/packages/@aws-cdk/aws-appsync/test/integ.api-import.ts index 74a8942246e51..b36c6a1bef7b1 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.api-import.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.api-import.ts @@ -24,8 +24,7 @@ const baseStack = new cdk.Stack(app, 'baseStack'); const baseApi = new appsync.GraphQLApi(baseStack, 'baseApi', { name: 'baseApi', - schemaDefinition: appsync.SchemaDefinition.FILE, - schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'), + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), }); const stack = new cdk.Stack(app, 'stack'); diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts index 5c6dced5c21e1..0d4c95a10ea3f 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts @@ -12,7 +12,7 @@ import { UserPoolDefaultAction, Values, IamResource, - SchemaDefinition, + Schema, } from '../lib'; /* @@ -38,8 +38,7 @@ const userPool = new UserPool(stack, 'Pool', { const api = new GraphQLApi(stack, 'Api', { name: 'Integ_Test_IAM', - schemaDefinition: SchemaDefinition.FILE, - schemaDefinitionFile: join(__dirname, 'integ.graphql-iam.graphql'), + schema: Schema.fromAsset(join(__dirname, 'integ.graphql-iam.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: AuthorizationType.USER_POOL, diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json index 4527fed9237fd..0a6fc134d5511 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.expected.json @@ -16,7 +16,7 @@ "ApiId" ] }, - "Definition": "type Planet {\n name: String\n diameter: Int\n rotationPeriod: Int\n orbitalPeriod: Int\n gravity: String\n population: [String]\n climates: [String]\n terrains: [String]\n surfaceWater: Float\n created: String\n edited: String\n id: ID!\n}\ntype Species {\n name: String\n classification: String\n designation: String\n averageHeight: Float\n averageLifespan: Int\n eyeColors: [String]\n hairColors: [String]\n skinColors: [String]\n language: String\n homeworld: Planet\n created: String\n edited: String\n id: ID!\n}\n\ntype Query {\n getPlanets: [Planet]\n}\n" + "Definition": "interface Node {\n created: String\n edited: String\n id: ID!\n}\ntype Planet {\n name: String\n diameter: Int\n rotationPeriod: Int\n orbitalPeriod: Int\n gravity: String\n population: [String]\n climates: [String]\n terrains: [String]\n surfaceWater: Float\n created: String\n edited: String\n id: ID!\n}\ntype Species implements Node {\n name: String\n classification: String\n designation: String\n averageHeight: Float\n averageLifespan: Int\n eyeColors: [String]\n hairColors: [String]\n skinColors: [String]\n language: String\n homeworld: Planet\n created: String\n edited: String\n id: ID!\n}\n\ntype Query {\n getPlanets: [Planet]\n}\n" } }, "codefirstapiDefaultApiKey89863A80": { diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts index ad840e93384e8..ae2057c68eb64 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts @@ -19,15 +19,26 @@ import * as ScalarType from './scalar-type-defintions'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'code-first-schema'); +const schema = new appsync.Schema(); + +const node = schema.addType(new appsync.InterfaceType('Node', { + definition: { + created: ScalarType.string, + edited: ScalarType.string, + id: ScalarType.required_id, + }, +})); + const api = new appsync.GraphQLApi(stack, 'code-first-api', { name: 'api', - schemaDefinition: appsync.SchemaDefinition.CODE, + schema: schema, }); const planet = ObjectType.planet; -api.appendToSchema(planet.toString()); +schema.addToSchema(planet.toString()); -api.addType('Species', { +api.addType(new appsync.ObjectType('Species', { + interfaceTypes: [node], definition: { name: ScalarType.string, classification: ScalarType.string, @@ -39,13 +50,10 @@ api.addType('Species', { skinColors: ScalarType.list_string, language: ScalarType.string, homeworld: planet.attribute(), - created: ScalarType.string, - edited: ScalarType.string, - id: ScalarType.required_id, }, -}); +})); -api.appendToSchema('type Query {\n getPlanets: [Planet]\n}', '\n'); +api.addToSchema('type Query {\n getPlanets: [Planet]\n}', '\n'); const table = new db.Table(stack, 'table', { partitionKey: { diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts index 607a495d9b735..715864b209bc1 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts @@ -8,9 +8,9 @@ import { KeyCondition, MappingTemplate, PrimaryKey, + Schema, UserPoolDefaultAction, Values, - SchemaDefinition, } from '../lib'; /* @@ -36,8 +36,7 @@ const userPool = new UserPool(stack, 'Pool', { const api = new GraphQLApi(stack, 'Api', { name: 'demoapi', - schemaDefinition: SchemaDefinition.FILE, - schemaDefinitionFile: join(__dirname, 'integ.graphql.graphql'), + schema: Schema.fromAsset(join(__dirname, 'integ.graphql.graphql')), authorizationConfig: { defaultAuthorization: { authorizationType: AuthorizationType.USER_POOL, diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 0d79aa18bf10b..0a8b5933e7f59 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -592,6 +592,42 @@ describe('with Lambda@Edge functions', () => { expect(() => app.synth()).toThrow(/KEY/); }); + + test('with singleton function', () => { + const singleton = new lambda.SingletonFunction(stack, 'Singleton', { + uuid: 'singleton-for-cloudfront', + runtime: lambda.Runtime.NODEJS_12_X, + code: lambda.Code.fromInline('code'), + handler: 'index.handler', + }); + + new Distribution(stack, 'MyDist', { + defaultBehavior: { + origin, + edgeLambdas: [ + { + functionVersion: singleton.currentVersion, + eventType: LambdaEdgeEventType.ORIGIN_REQUEST, + }, + ], + }, + }); + + expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + DistributionConfig: { + DefaultCacheBehavior: { + LambdaFunctionAssociations: [ + { + EventType: 'origin-request', + LambdaFunctionARN: { + Ref: 'SingletonLambdasingletonforcloudfrontCurrentVersion0078406348a0962a52448a200cd0dbc0e22edb2a', + }, + }, + ], + }, + }, + }); + }); }); test('price class is included if provided', () => { diff --git a/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts index e91c4e2f53192..c6cd639a7262a 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts @@ -443,7 +443,7 @@ nodeunitShim({ isDefaultBehavior: true, lambdaFunctionAssociations: [{ eventType: LambdaEdgeEventType.ORIGIN_REQUEST, - lambdaFunction: lambdaFunction.addVersion('1'), + lambdaFunction: lambdaFunction.currentVersion, }], }, ], @@ -458,7 +458,7 @@ nodeunitShim({ { 'EventType': 'origin-request', 'LambdaFunctionARN': { - 'Ref': 'LambdaVersion1BB7548E1', + 'Ref': 'LambdaCurrentVersionDF706F6A97fb843e9bd06fcd2bb15eeace80e13e', }, }, ], @@ -492,7 +492,7 @@ nodeunitShim({ isDefaultBehavior: true, lambdaFunctionAssociations: [{ eventType: LambdaEdgeEventType.ORIGIN_REQUEST, - lambdaFunction: lambdaFunction.addVersion('1'), + lambdaFunction: lambdaFunction.currentVersion, }], }, ], @@ -532,7 +532,7 @@ nodeunitShim({ isDefaultBehavior: true, lambdaFunctionAssociations: [{ eventType: LambdaEdgeEventType.ORIGIN_REQUEST, - lambdaFunction: lambdaFunction.addVersion('1'), + lambdaFunction: lambdaFunction.currentVersion, }], }, ], diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 61cf04f591f4e..8efbc0447324d 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -344,7 +344,7 @@ Triggers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user Any user pool that has been created outside of this stack, can be imported into the CDK app. Importing a user pool allows for it to be used in other parts of the CDK app that reference an `IUserPool`. However, imported user pools have -limited configurability. As a rule of thumb, none of the properties that is are part of the +limited configurability. As a rule of thumb, none of the properties that are part of the [`AWS::Cognito::UserPool`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html) CloudFormation resource can be configured. diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/.gitignore b/packages/@aws-cdk/aws-elasticloadbalancing/.gitignore index dcc1dc41e477f..f5370b5b8fc68 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/.gitignore +++ b/packages/@aws-cdk/aws-elasticloadbalancing/.gitignore @@ -14,5 +14,6 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js -junit.xml \ No newline at end of file +junit.xml diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/.npmignore b/packages/@aws-cdk/aws-elasticloadbalancing/.npmignore index 95a6e5fe5bb87..1a36d0617073e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/.npmignore +++ b/packages/@aws-cdk/aws-elasticloadbalancing/.npmignore @@ -22,4 +22,5 @@ tsconfig.json # exclude cdk artifacts **/cdk.out -junit.xml \ No newline at end of file +junit.xml +jest.config.js diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/jest.config.js b/packages/@aws-cdk/aws-elasticloadbalancing/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancing/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/package.json b/packages/@aws-cdk/aws-elasticloadbalancing/package.json index b48480d426c29..961544e9e33ab 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::ElasticLoadBalancing" + "cloudformation": "AWS::ElasticLoadBalancing", + "jest": true }, "keywords": [ "aws", @@ -64,11 +65,9 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/test.loadbalancer.ts b/packages/@aws-cdk/aws-elasticloadbalancing/test/loadbalancer.test.ts similarity index 88% rename from packages/@aws-cdk/aws-elasticloadbalancing/test/test.loadbalancer.ts rename to packages/@aws-cdk/aws-elasticloadbalancing/test/loadbalancer.test.ts index 9451e66fba146..873ee201c16b7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/test.loadbalancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/loadbalancer.test.ts @@ -1,11 +1,10 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Connections, Peer, SubnetType, Vpc } from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { ILoadBalancerTarget, LoadBalancer, LoadBalancingProtocol } from '../lib'; -export = { - 'test specifying nonstandard port works'(test: Test) { +describe('tests', () => { + test('test specifying nonstandard port works', () => { const stack = new Stack(undefined, undefined, { env: { account: '1234', region: 'test' } }); stack.node.setContext('availability-zones:1234:test', ['test-1a', 'test-1b']); const vpc = new Vpc(stack, 'VCP'); @@ -27,11 +26,9 @@ export = { Protocol: 'http', }], })); + }); - test.done(); - }, - - 'add a health check'(test: Test) { + test('add a health check', () => { // GIVEN const stack = new Stack(); const vpc = new Vpc(stack, 'VCP'); @@ -57,11 +54,9 @@ export = { UnhealthyThreshold: '5', }, })); + }); - test.done(); - }, - - 'add a listener and load balancing target'(test: Test) { + test('add a listener and load balancing target', () => { // GIVEN const stack = new Stack(); const vpc = new Vpc(stack, 'VCP'); @@ -91,11 +86,9 @@ export = { }, ], })); + }); - test.done(); - }, - - 'enable cross zone load balancing'(test: Test) { + test('enable cross zone load balancing', () => { // GIVEN const stack = new Stack(); const vpc = new Vpc(stack, 'VCP'); @@ -110,11 +103,9 @@ export = { expect(stack).to(haveResource('AWS::ElasticLoadBalancing::LoadBalancer', { CrossZone: true, })); + }); - test.done(); - }, - - 'disable cross zone load balancing'(test: Test) { + test('disable cross zone load balancing', () => { // GIVEN const stack = new Stack(); const vpc = new Vpc(stack, 'VCP'); @@ -129,11 +120,9 @@ export = { expect(stack).to(haveResource('AWS::ElasticLoadBalancing::LoadBalancer', { CrossZone: false, })); + }); - test.done(); - }, - - 'cross zone load balancing enabled by default'(test: Test) { + test('cross zone load balancing enabled by default', () => { // GIVEN const stack = new Stack(); const vpc = new Vpc(stack, 'VCP'); @@ -147,11 +136,9 @@ export = { expect(stack).to(haveResource('AWS::ElasticLoadBalancing::LoadBalancer', { CrossZone: true, })); + }); - test.done(); - }, - - 'use specified subnet'(test: Test) { + test('use specified subnet', () => { // GIVEN const stack = new Stack(); const vpc = new Vpc(stack, 'VCP', { @@ -188,11 +175,8 @@ export = { subnetName: 'private1', }).subnetIds.map((subnetId: string) => stack.resolve(subnetId)), })); - - test.done(); - }, - -}; + }); +}); class FakeTarget implements ILoadBalancerTarget { public readonly connections = new Connections({ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/.gitignore b/packages/@aws-cdk/aws-elasticloadbalancingv2/.gitignore index 018c65919d67c..266c0684c6844 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/.gitignore +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/.gitignore @@ -14,5 +14,6 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js -junit.xml \ No newline at end of file +junit.xml diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/.npmignore b/packages/@aws-cdk/aws-elasticloadbalancingv2/.npmignore index 95a6e5fe5bb87..1a36d0617073e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/.npmignore +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/.npmignore @@ -22,4 +22,5 @@ tsconfig.json # exclude cdk artifacts **/cdk.out -junit.xml \ No newline at end of file +junit.xml +jest.config.js diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/jest.config.js b/packages/@aws-cdk/aws-elasticloadbalancingv2/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json index 275c85044f388..1bf66849dfa9f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::ElasticLoadBalancingV2" + "cloudformation": "AWS::ElasticLoadBalancingV2", + "jest": true }, "keywords": [ "aws", @@ -64,11 +65,9 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/actions.test.ts similarity index 74% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/actions.test.ts index 62a1c378bf067..719abb84dc44e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/actions.test.ts @@ -1,7 +1,6 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; let stack: cdk.Stack; @@ -9,18 +8,16 @@ let group1: elbv2.ApplicationTargetGroup; let group2: elbv2.ApplicationTargetGroup; let lb: elbv2.ApplicationLoadBalancer; -export = { - 'setUp'(cb: () => void) { - stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'Stack'); - group1 = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup1', { vpc, port: 80 }); - group2 = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup2', { vpc, port: 80 }); - lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); +beforeEach(() => { + stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + group1 = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup1', { vpc, port: 80 }); + group2 = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup2', { vpc, port: 80 }); + lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); +}); - cb(); - }, - - 'Forward action legacy rendering'(test: Test) { +describe('tests', () => { + test('Forward action legacy rendering', () => { // WHEN lb.addListener('Listener', { port: 80, @@ -28,19 +25,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'TargetGroup1E5480F51' }, Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Forward to multiple targetgroups with an Action and stickiness'(test: Test) { + test('Forward to multiple targetgroups with an Action and stickiness', () => { // WHEN lb.addListener('Listener', { port: 80, @@ -50,7 +45,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { ForwardConfig: { @@ -70,12 +65,10 @@ export = { Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Weighted forward to multiple targetgroups with an Action'(test: Test) { + test('Weighted forward to multiple targetgroups with an Action', () => { // WHEN lb.addListener('Listener', { port: 80, @@ -88,7 +81,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { ForwardConfig: { @@ -110,11 +103,10 @@ export = { Type: 'forward', }, ], - })); - test.done(); - }, + }); + }); - 'Chaining OIDC authentication action'(test: Test) { + test('Chaining OIDC authentication action', () => { // WHEN lb.addListener('Listener', { port: 80, @@ -130,7 +122,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { AuthenticateOidcConfig: { @@ -150,12 +142,10 @@ export = { Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Add default Action and add Action with conditions'(test: Test) { + test('Add default Action and add Action with conditions', () => { // GIVEN const listener = lb.addListener('Listener', { port: 80 }); @@ -171,19 +161,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Add Action with multiple Conditions'(test: Test) { + test('Add Action with multiple Conditions', () => { // GIVEN const listener = lb.addListener('Listener', { port: 80 }); @@ -202,7 +190,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, @@ -227,8 +215,6 @@ export = { }, }, ], - })); - - test.done(); - }, -}; \ No newline at end of file + }); + }); +}); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts similarity index 77% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index aa6345eac07cc..3e0ecb3ea740a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -1,13 +1,13 @@ -import { expect, haveResource, MatchStyle } from '@aws-cdk/assert'; +import { MatchStyle } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; import { FakeSelfRegisteringTarget } from '../helpers'; -export = { - 'Listener guesses protocol from port'(test: Test) { +describe('tests', () => { + test('Listener guesses protocol from port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -21,14 +21,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'HTTPS', - })); - - test.done(); - }, + }); + }); - 'Listener guesses port from protocol'(test: Test) { + test('Listener guesses port from protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -41,14 +39,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Port: 80, - })); - - test.done(); - }, + }); + }); - 'Listener default to open - IPv4'(test: Test) { + test('Listener default to open - IPv4', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -61,7 +57,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ { Description: 'Allow from anyone on port 80', @@ -71,12 +67,10 @@ export = { ToPort: 80, }, ], - })); - - test.done(); - }, + }); + }); - 'Listener default to open - IPv4 and IPv6 (dualstack)'(test: Test) { + test('Listener default to open - IPv4 and IPv6 (dualstack)', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -89,7 +83,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ { Description: 'Allow from anyone on port 80', @@ -106,12 +100,10 @@ export = { ToPort: 80, }, ], - })); - - test.done(); - }, + }); + }); - 'HTTPS listener requires certificate'(test: Test) { + test('HTTPS listener requires certificate', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -125,12 +117,10 @@ export = { // THEN const errors = cdk.ConstructNode.validate(stack.node); - test.deepEqual(errors.map(e => e.message), ['HTTPS Listener needs at least one certificate (call addCertificates)']); - - test.done(); - }, + expect(errors.map(e => e.message)).toEqual(['HTTPS Listener needs at least one certificate (call addCertificates)']); + }); - 'HTTPS listener can add certificate after construction'(test: Test) { + test('HTTPS listener can add certificate after construction', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -145,16 +135,14 @@ export = { listener.addCertificateArns('Arns', ['cert']); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Certificates: [ { CertificateArn: 'cert' }, ], - })); - - test.done(); - }, + }); + }); - 'Can configure targetType on TargetGroups'(test: Test) { + test('Can configure targetType on TargetGroups', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -167,14 +155,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetType: 'ip', - })); - - test.done(); - }, + }); + }); - 'Can configure name on TargetGroups'(test: Test) { + test('Can configure name on TargetGroups', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -187,14 +173,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Name: 'foo', - })); - - test.done(); - }, + }); + }); - 'Can add target groups with and without conditions'(test: Test) { + test('Can add target groups with and without conditions', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -213,15 +197,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'TargetGroup3D7CD9B8' }, Type: 'forward', }, ], - })); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + }); + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -235,12 +219,10 @@ export = { Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Can implicitly create target groups with and without conditions'(test: Test) { + test('Can implicitly create target groups with and without conditions', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -260,43 +242,41 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'LBListenerTargetsGroup76EF81E8' }, Type: 'forward', }, ], - })); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + }); + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 80, Protocol: 'HTTP', Targets: [ { Id: 'i-12345' }, ], - })); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + }); + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { TargetGroupArn: { Ref: 'LBListenerWithPathGroupE889F9E5' }, Type: 'forward', }, ], - })); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + }); + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 80, Protocol: 'HTTP', Targets: [ { Id: 'i-5678' }, ], - })); - - test.done(); - }, + }); + }); - 'Add certificate to constructed listener'(test: Test) { + test('Add certificate to constructed listener', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -308,16 +288,14 @@ export = { listener.addTargets('Targets', { port: 8080, targets: [new elbv2.IpTarget('1.2.3.4')] }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Certificates: [ { CertificateArn: 'cert' }, ], - })); - - test.done(); - }, + }); + }); - 'Add certificate to imported listener'(test: Test) { + test('Add certificate to imported listener', () => { // GIVEN const stack2 = new cdk.Stack(); const listener2 = elbv2.ApplicationListener.fromApplicationListenerAttributes(stack2, 'Listener', { @@ -330,16 +308,14 @@ export = { listener2.addCertificateArns('Arns', ['cert']); // THEN - expect(stack2).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + expect(stack2).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [ { CertificateArn: 'cert' }, ], - })); - - test.done(); - }, + }); + }); - 'Enable stickiness for targets'(test: Test) { + test('Enable stickiness for targets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -354,7 +330,7 @@ export = { group.enableCookieStickiness(cdk.Duration.hours(1)); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'stickiness.enabled', @@ -369,12 +345,10 @@ export = { Value: '3600', }, ], - })); - - test.done(); - }, + }); + }); - 'Enable health check for targets'(test: Test) { + test('Enable health check for targets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -394,17 +368,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { UnhealthyThresholdCount: 3, HealthCheckIntervalSeconds: 30, HealthCheckPath: '/test', HealthCheckTimeoutSeconds: 3600, - })); - - test.done(); - }, + }); + }); - 'validation error if invalid health check protocol'(test: Test) { + test('validation error if invalid health check protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -427,12 +399,10 @@ export = { // THEN const validationErrors: string[] = (group as any).validate(); - test.deepEqual(validationErrors, ["Health check protocol 'TCP' is not supported. Must be one of [HTTP, HTTPS]"]); + expect(validationErrors).toEqual(["Health check protocol 'TCP' is not supported. Must be one of [HTTP, HTTPS]"]); + }); - test.done(); - }, - - 'Can call addTargetGroups on imported listener'(test: Test) { + test('Can call addTargetGroups on imported listener', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -450,7 +420,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { ListenerArn: 'ieks', Priority: 30, Actions: [ @@ -459,12 +429,10 @@ export = { Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Can call addTargetGroups on imported listener with conditions prop'(test: Test) { + test('Can call addTargetGroups on imported listener with conditions prop', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -482,7 +450,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { ListenerArn: 'ieks', Priority: 30, Actions: [ @@ -491,12 +459,10 @@ export = { Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Can depend on eventual listener via TargetGroup'(test: Test) { + test('Can depend on eventual listener via TargetGroup', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -512,7 +478,7 @@ export = { }); // THEN - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { SomeResource: { Type: 'Test::Resource', @@ -520,11 +486,9 @@ export = { }, }, }, MatchStyle.SUPERSET); + }); - test.done(); - }, - - 'Exercise metrics'(test: Test) { + test('Exercise metrics', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -547,10 +511,10 @@ export = { metrics.push(group.metricTargetTLSNegotiationErrorCount()); for (const metric of metrics) { - test.equal('AWS/ApplicationELB', metric.namespace); + expect(metric.namespace).toEqual('AWS/ApplicationELB'); const loadBalancerArn = { Ref: 'LBSomeListenerCA01F1A0' }; - test.deepEqual(stack.resolve(metric.dimensions), { + expect(stack.resolve(metric.dimensions)).toEqual({ TargetGroup: { 'Fn::GetAtt': ['TargetGroup3D7CD9B8', 'TargetGroupFullName'] }, LoadBalancer: { 'Fn::Join': @@ -563,11 +527,9 @@ export = { }, }); } + }); - test.done(); - }, - - 'Can add dependency on ListenerRule via TargetGroup'(test: Test) { + test('Can add dependency on ListenerRule via TargetGroup', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -589,7 +551,7 @@ export = { }); // THEN - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { SomeResource: { Type: 'Test::Resource', @@ -597,11 +559,9 @@ export = { }, }, }, MatchStyle.SUPERSET); + }); - test.done(); - }, - - 'Can add fixed responses'(test: Test) { + test('Can add fixed responses', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -625,7 +585,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { FixedResponseConfig: { @@ -636,9 +596,9 @@ export = { Type: 'fixed-response', }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { FixedResponseConfig: { @@ -647,12 +607,10 @@ export = { Type: 'fixed-response', }, ], - })); - - test.done(); - }, + }); + }); - 'Can add redirect responses'(test: Test) { + test('Can add redirect responses', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -677,7 +635,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { RedirectConfig: { @@ -688,9 +646,9 @@ export = { Type: 'redirect', }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Actions: [ { RedirectConfig: { @@ -700,12 +658,10 @@ export = { Type: 'redirect', }, ], - })); - - test.done(); - }, + }); + }); - 'Can configure deregistration_delay for targets'(test: Test) { + test('Can configure deregistration_delay for targets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -718,21 +674,19 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'deregistration_delay.timeout_seconds', Value: '30', }, ], - })); - - test.done(); - }, + }); + }); - 'Throws with bad fixed responses': { + describe('Throws with bad fixed responses', () => { - 'status code'(test: Test) { + test('status code', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -744,14 +698,12 @@ export = { }); // THEN - test.throws(() => listener.addFixedResponse('Default', { + expect(() => listener.addFixedResponse('Default', { statusCode: '301', - }), /`statusCode`/); - - test.done(); - }, + })).toThrow(/`statusCode`/); + }); - 'message body'(test: Test) { + test('message body', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -763,18 +715,16 @@ export = { }); // THEN - test.throws(() => listener.addFixedResponse('Default', { + expect(() => listener.addFixedResponse('Default', { messageBody: 'a'.repeat(1025), statusCode: '500', - }), /`messageBody`/); - - test.done(); - }, - }, + })).toThrow(/`messageBody`/); + }); + }); - 'Throws with bad redirect responses': { + describe('Throws with bad redirect responses', () => { - 'status code'(test: Test) { + test('status code', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -786,14 +736,12 @@ export = { }); // THEN - test.throws(() => listener.addRedirectResponse('Default', { + expect(() => listener.addRedirectResponse('Default', { statusCode: '301', - }), /`statusCode`/); - - test.done(); - }, + })).toThrow(/`statusCode`/); + }); - 'protocol'(test: Test) { + test('protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -805,16 +753,14 @@ export = { }); // THEN - test.throws(() => listener.addRedirectResponse('Default', { + expect(() => listener.addRedirectResponse('Default', { protocol: 'tcp', statusCode: 'HTTP_301', - }), /`protocol`/); - - test.done(); - }, - }, + })).toThrow(/`protocol`/); + }); + }); - 'Throws when specifying both target groups and fixed reponse'(test: Test) { + test('Throws when specifying both target groups and fixed reponse', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -826,7 +772,7 @@ export = { }); // THEN - test.throws(() => new elbv2.ApplicationListenerRule(stack, 'Rule', { + expect(() => new elbv2.ApplicationListenerRule(stack, 'Rule', { listener, priority: 10, pathPattern: '/hello', @@ -834,12 +780,10 @@ export = { fixedResponse: { statusCode: '500', }, - }), /'targetGroups,fixedResponse'.*/); + })).toThrow(/'targetGroups,fixedResponse'.*/); + }); - test.done(); - }, - - 'Throws when specifying priority 0'(test: Test) { + test('Throws when specifying priority 0', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -851,19 +795,17 @@ export = { }); // THEN - test.throws(() => new elbv2.ApplicationListenerRule(stack, 'Rule', { + expect(() => new elbv2.ApplicationListenerRule(stack, 'Rule', { listener, priority: 0, pathPattern: '/hello', fixedResponse: { statusCode: '500', }, - }), Error, 'Priority must have value greater than or equal to 1'); - - test.done(); - }, + })).toThrowError('Priority must have value greater than or equal to 1'); + }); - 'Throws when specifying both target groups and redirect reponse'(test: Test) { + test('Throws when specifying both target groups and redirect reponse', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -875,7 +817,7 @@ export = { }); // THEN - test.throws(() => new elbv2.ApplicationListenerRule(stack, 'Rule', { + expect(() => new elbv2.ApplicationListenerRule(stack, 'Rule', { listener, priority: 10, pathPattern: '/hello', @@ -883,9 +825,9 @@ export = { redirectResponse: { statusCode: 'HTTP_301', }, - }), /'targetGroups,redirectResponse'.*/); + })).toThrow(/'targetGroups,redirectResponse'.*/); - test.throws(() => new elbv2.ApplicationListenerRule(stack, 'Rule2', { + expect(() => new elbv2.ApplicationListenerRule(stack, 'Rule2', { listener, priority: 10, pathPattern: '/hello', @@ -896,12 +838,10 @@ export = { redirectResponse: { statusCode: 'HTTP_301', }, - }), /'targetGroups,fixedResponse,redirectResponse'.*/); - - test.done(); - }, + })).toThrow(/'targetGroups,fixedResponse,redirectResponse'.*/); + }); - 'Imported listener with imported security group and allowAllOutbound set to false'(test: Test) { + test('Imported listener with imported security group and allowAllOutbound set to false', () => { // GIVEN const stack = new cdk.Stack(); const listener = elbv2.ApplicationListener.fromApplicationListenerAttributes(stack, 'Listener', { @@ -916,14 +856,12 @@ export = { listener.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { GroupId: 'security-group-id', - })); - - test.done(); - }, + }); + }); - 'Can pass multiple certificate arns to application listener constructor'(test: Test) { + test('Can pass multiple certificate arns to application listener constructor', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -937,18 +875,16 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'HTTPS', - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert2' }], - })); - - test.done(); - }, + }); + }); - 'Can use certificate wrapper class'(test: Test) { + test('Can use certificate wrapper class', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -962,18 +898,16 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'HTTPS', - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert2' }], - })); - - test.done(); - }, + }); + }); - 'Can add additional certificates via addCertficateArns to application listener'(test: Test) { + test('Can add additional certificates via addCertficateArns to application listener', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -989,22 +923,20 @@ export = { listener.addCertificateArns('ListenerCertificateX', ['cert3']); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'HTTPS', - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert2' }], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { Certificates: [{ CertificateArn: 'cert3' }], - })); - - test.done(); - }, + }); + }); - 'Can add multiple path patterns to listener rule'(test: Test) { + test('Can add multiple path patterns to listener rule', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1023,7 +955,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -1031,12 +963,10 @@ export = { Values: ['/test/path/1', '/test/path/2'], }, ], - })); - - test.done(); - }, + }); + }); - 'Cannot add pathPattern and pathPatterns to listener rule'(test: Test) { + test('Cannot add pathPattern and pathPatterns to listener rule', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1050,16 +980,14 @@ export = { }); // THEN - test.throws(() => listener.addTargets('Target1', { + expect(() => listener.addTargets('Target1', { priority: 10, pathPatterns: ['/test/path/1', '/test/path/2'], pathPattern: '/test/path/3', - }), Error, 'At least one of \'hostHeader\', \'pathPattern\' or \'pathPatterns\' is required when defining a load balancing rule.'); + })).toThrowError('Both `pathPatterns` and `pathPattern` are specified, specify only one'); + }); - test.done(); - }, - - 'Add additonal condition to listener rule'(test: Test) { + test('Add additonal condition to listener rule', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1090,7 +1018,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -1107,9 +1035,9 @@ export = { }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 20, Conditions: [ { @@ -1119,12 +1047,10 @@ export = { }, }, ], - })); - - test.done(); - }, + }); + }); - 'Add multiple additonal condition to listener rule'(test: Test) { + test('Add multiple additonal condition to listener rule', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1173,7 +1099,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -1203,9 +1129,9 @@ export = { }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 20, Conditions: [ { @@ -1222,9 +1148,9 @@ export = { }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 30, Conditions: [ { @@ -1240,9 +1166,9 @@ export = { }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 40, Conditions: [ { @@ -1252,12 +1178,10 @@ export = { }, }, ], - })); - - test.done(); - }, + }); + }); - 'Can exist together legacy style conditions and modan style conditions'(test: Test) { + test('Can exist together legacy style conditions and modan style conditions', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1282,7 +1206,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 10, Conditions: [ { @@ -1300,12 +1224,10 @@ export = { }, }, ], - })); - - test.done(); - }, + }); + }); - 'Add condition to imported application listener'(test: Test) { + test('Add condition to imported application listener', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1324,7 +1246,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { Priority: 1, Conditions: [ { @@ -1332,12 +1254,10 @@ export = { Values: ['/path1', '/path2'], }, ], - })); - - test.done(); - }, + }); + }); - 'not allowed to combine action specifiers when instantiating a Rule directly'(test: Test) { + test('not allowed to combine action specifiers when instantiating a Rule directly', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1348,26 +1268,24 @@ export = { const baseProps = { listener, priority: 1, pathPatterns: ['/path1', '/path2'] }; // WHEN - test.throws(() => { + expect(() => { new elbv2.ApplicationListenerRule(stack, 'Rule1', { ...baseProps, fixedResponse: { statusCode: '200' }, action: elbv2.ListenerAction.fixedResponse(200), }); - }, /specify only one/); + }).toThrow(/specify only one/); - test.throws(() => { + expect(() => { new elbv2.ApplicationListenerRule(stack, 'Rule2', { ...baseProps, targetGroups: [group], action: elbv2.ListenerAction.fixedResponse(200), }); - }, /specify only one/); + }).toThrow(/specify only one/); + }); - test.done(); - }, - - 'not allowed to specify defaultTargetGroups and defaultAction together'(test: Test) { + test('not allowed to specify defaultTargetGroups and defaultAction together', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1375,17 +1293,15 @@ export = { const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); // WHEN - test.throws(() => { + expect(() => { lb.addListener('Listener1', { port: 80, defaultTargetGroups: [group], defaultAction: elbv2.ListenerAction.fixedResponse(200), }); - }, /Specify at most one/); - - test.done(); - }, -}; + }).toThrow(/Specify at most one/); + }); +}); class ResourceWithLBDependency extends cdk.CfnResource { constructor(scope: cdk.Construct, id: string, targetGroup: elbv2.ITargetGroup) { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts similarity index 78% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index 5d623fbc5dce4..8627f2deea272 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -1,13 +1,13 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; -export = { - 'Trivial construction: internet facing'(test: Test) { +describe('tests', () => { + test('Trivial construction: internet facing', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -19,19 +19,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internet-facing', Subnets: [ { Ref: 'StackPublicSubnet1Subnet0AD81D22' }, { Ref: 'StackPublicSubnet2Subnet3C7D2288' }, ], Type: 'application', - })); - - test.done(); - }, + }); + }); - 'internet facing load balancer has dependency on IGW'(test: Test) { + test('internet facing load balancer has dependency on IGW', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -43,17 +41,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { DependsOn: [ 'StackPublicSubnet1DefaultRoute16154E3D', 'StackPublicSubnet2DefaultRoute0319539B', ], - }, ResourcePart.CompleteDefinition)); - - test.done(); - }, + }, ResourcePart.CompleteDefinition); + }); - 'Trivial construction: internal'(test: Test) { + test('Trivial construction: internal', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -62,19 +58,17 @@ export = { new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'StackPrivateSubnet1Subnet47AC2BC7' }, { Ref: 'StackPrivateSubnet2SubnetA2F8EDD8' }, ], Type: 'application', - })); - - test.done(); - }, + }); + }); - 'Attributes'(test: Test) { + test('Attributes', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -88,7 +82,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { LoadBalancerAttributes: [ { Key: 'deletion_protection.enabled', @@ -103,12 +97,10 @@ export = { Value: '1000', }, ], - })); - - test.done(); - }, + }); + }); - 'Access logging'(test: Test) { + test('Access logging', () => { // GIVEN const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -121,7 +113,7 @@ export = { // THEN // verify that the LB attributes reference the bucket - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { LoadBalancerAttributes: [ { Key: 'access_logs.s3.enabled', @@ -132,10 +124,10 @@ export = { Value: { Ref: 'AccessLoggingBucketA6D88F29' }, }, ], - })); + }); // verify the bucket policy allows the ALB to put objects in the bucket - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -150,17 +142,15 @@ export = { }, ], }, - })); + }); // verify the ALB depends on the bucket *and* the bucket policy - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'], - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); + }); - test.done(); - }, - - 'access logging with prefix'(test: Test) { + test('access logging with prefix', () => { // GIVEN const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -172,7 +162,7 @@ export = { // THEN // verify that the LB attributes reference the bucket - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { LoadBalancerAttributes: [ { Key: 'access_logs.s3.enabled', @@ -187,10 +177,10 @@ export = { Value: 'prefix-of-access-logs', }, ], - })); + }); // verify the bucket policy allows the ALB to put objects in the bucket - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -205,12 +195,10 @@ export = { }, ], }, - })); - - test.done(); - }, + }); + }); - 'Exercise metrics'(test: Test) { + test('Exercise metrics', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -242,16 +230,14 @@ export = { metrics.push(lb.metricTargetTLSNegotiationErrorCount()); for (const metric of metrics) { - test.equal('AWS/ApplicationELB', metric.namespace); - test.deepEqual(stack.resolve(metric.dimensions), { + expect(metric.namespace).toEqual('AWS/ApplicationELB'); + expect(stack.resolve(metric.dimensions)).toEqual({ LoadBalancer: { 'Fn::GetAtt': ['LB8A12904C', 'LoadBalancerFullName'] }, }); } + }); - test.done(); - }, - - 'loadBalancerName'(test: Test) { + test('loadBalancerName', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -263,13 +249,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Name: 'myLoadBalancer', - })); - test.done(); - }, + }); + }); - 'imported load balancer with no vpc throws error when calling addTargets'(test: Test) { + test('imported load balancer with no vpc throws error when calling addTargets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -285,12 +270,10 @@ export = { // WHEN const listener = alb.addListener('Listener', { port: 80 }); - test.throws(() => listener.addTargets('Targets', { port: 8080 })); + expect(() => listener.addTargets('Targets', { port: 8080 })).toThrow(); + }); - test.done(); - }, - - 'imported load balancer with vpc does not throw error when calling addTargets'(test: Test) { + test('imported load balancer with vpc does not throw error when calling addTargets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -307,8 +290,6 @@ export = { // WHEN const listener = alb.addListener('Listener', { port: 80 }); - test.doesNotThrow(() => listener.addTargets('Targets', { port: 8080 })); - - test.done(); - }, -}; + expect(() => listener.addTargets('Targets', { port: 8080 })).not.toThrow(); + }); +}); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.security-groups.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts similarity index 86% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.security-groups.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts index f12377b5a5ab5..e4d45eb609335 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.security-groups.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; import { FakeSelfRegisteringTarget } from '../helpers'; -export = { - 'security groups are automatically opened bidi for default rule'(test: Test) { +describe('tests', () => { + test('security groups are automatically opened bidi for default rule', () => { // GIVEN const fixture = new TestFixture(); const target = new FakeSelfRegisteringTarget(fixture.stack, 'Target', fixture.vpc); @@ -19,11 +18,9 @@ export = { // THEN expectSameStackSGRules(fixture.stack); + }); - test.done(); - }, - - 'security groups are automatically opened bidi for additional rule'(test: Test) { + test('security groups are automatically opened bidi for additional rule', () => { // GIVEN const fixture = new TestFixture(); const target1 = new FakeSelfRegisteringTarget(fixture.stack, 'DefaultTarget', fixture.vpc); @@ -47,11 +44,9 @@ export = { // THEN expectSameStackSGRules(fixture.stack); + }); - test.done(); - }, - - 'adding the same targets twice also works'(test: Test) { + test('adding the same targets twice also works', () => { // GIVEN const fixture = new TestFixture(); const target = new FakeSelfRegisteringTarget(fixture.stack, 'Target', fixture.vpc); @@ -74,11 +69,9 @@ export = { // THEN expectSameStackSGRules(fixture.stack); + }); - test.done(); - }, - - 'same result if target is added to group after assigning to listener'(test: Test) { + test('same result if target is added to group after assigning to listener', () => { // GIVEN const fixture = new TestFixture(); const group = new elbv2.ApplicationTargetGroup(fixture.stack, 'TargetGroup', { @@ -95,11 +88,9 @@ export = { // THEN expectSameStackSGRules(fixture.stack); + }); - test.done(); - }, - - 'ingress is added to child stack SG instead of parent stack'(test: Test) { + test('ingress is added to child stack SG instead of parent stack', () => { // GIVEN const fixture = new TestFixture(true); @@ -132,11 +123,9 @@ export = { // THEN expectSameStackSGRules(fixture.stack); expectedImportedSGRules(childStack); + }); - test.done(); - }, - - 'SG peering works on exported/imported load balancer'(test: Test) { + test('SG peering works on exported/imported load balancer', () => { // GIVEN const fixture = new TestFixture(false); const stack2 = new cdk.Stack(fixture.app, 'stack2'); @@ -159,11 +148,9 @@ export = { // THEN expectedImportedSGRules(stack2); + }); - test.done(); - }, - - 'SG peering works on exported/imported listener'(test: Test) { + test('SG peering works on exported/imported listener', () => { // GIVEN const fixture = new TestFixture(); const stack2 = new cdk.Stack(fixture.app, 'stack2'); @@ -192,11 +179,9 @@ export = { // THEN expectedImportedSGRules(stack2); + }); - test.done(); - }, - - 'default port peering works on constructed listener'(test: Test) { + test('default port peering works on constructed listener', () => { // GIVEN const fixture = new TestFixture(); fixture.listener.addTargets('Default', { port: 8080, targets: [new elbv2.InstanceTarget('i-12345')] }); @@ -205,7 +190,7 @@ export = { fixture.listener.connections.allowDefaultPortFromAnyIpv4('Open to the world'); // THEN - expect(fixture.stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(fixture.stack).toHaveResource('AWS::EC2::SecurityGroup', { SecurityGroupIngress: [ { CidrIp: '0.0.0.0/0', @@ -215,12 +200,10 @@ export = { ToPort: 80, }, ], - })); - - test.done(); - }, + }); + }); - 'default port peering works on imported listener'(test: Test) { + test('default port peering works on imported listener', () => { // GIVEN const stack2 = new cdk.Stack(); @@ -233,18 +216,16 @@ export = { listener2.connections.allowDefaultPortFromAnyIpv4('Open to the world'); // THEN - expect(stack2).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack2).toHaveResource('AWS::EC2::SecurityGroupIngress', { CidrIp: '0.0.0.0/0', Description: 'Open to the world', IpProtocol: 'tcp', FromPort: 8080, ToPort: 8080, GroupId: 'imported-security-group-id', - })); - - test.done(); - }, -}; + }); + }); +}); const LB_SECURITY_GROUP = { 'Fn::GetAtt': ['LBSecurityGroup8A41EA2B', 'GroupId'] }; const IMPORTED_LB_SECURITY_GROUP = { 'Fn::ImportValue': 'Stack:ExportsOutputFnGetAttLBSecurityGroup8A41EA2BGroupId851EE1F6' }; @@ -258,22 +239,22 @@ function expectedImportedSGRules(stack: cdk.Stack) { } function expectSGRules(stack: cdk.Stack, lbGroup: any) { - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { GroupId: lbGroup, IpProtocol: 'tcp', Description: 'Load balancer to target', DestinationSecurityGroupId: { 'Fn::GetAtt': ['TargetSGDB98152D', 'GroupId'] }, FromPort: 8008, ToPort: 8008, - })); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + }); + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { IpProtocol: 'tcp', Description: 'Load balancer to target', FromPort: 8008, GroupId: { 'Fn::GetAtt': ['TargetSGDB98152D', 'GroupId'] }, SourceSecurityGroupId: lbGroup, ToPort: 8008, - })); + }); } class TestFixture { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts similarity index 73% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.target-group.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts index cdea234295273..1d8df0a706d9c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; import { FakeSelfRegisteringTarget } from '../helpers'; -export = { - 'Empty target Group without type still requires a VPC'(test: Test) { +describe('tests', () => { + test('Empty target Group without type still requires a VPC', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'Stack'); @@ -15,14 +14,12 @@ export = { new elbv2.ApplicationTargetGroup(stack, 'LB', {}); // THEN - test.throws(() => { + expect(() => { app.synth(); - }, /'vpc' is required for a non-Lambda TargetGroup/); + }).toThrow(/'vpc' is required for a non-Lambda TargetGroup/); + }); - test.done(); - }, - - 'Can add self-registering target to imported TargetGroup'(test: Test) { + test('Can add self-registering target to imported TargetGroup', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'Stack'); @@ -33,12 +30,9 @@ export = { targetGroupArn: 'arn', }); tg.addTarget(new FakeSelfRegisteringTarget(stack, 'Target', vpc)); + }); - // THEN - test.done(); - }, - - 'Cannot add direct target to imported TargetGroup'(test: Test) { + test('Cannot add direct target to imported TargetGroup', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'Stack'); @@ -47,14 +41,12 @@ export = { }); // WHEN - test.throws(() => { + expect(() => { tg.addTarget(new elbv2.InstanceTarget('i-1234')); - }, /Cannot add a non-self registering target to an imported TargetGroup/); + }).toThrow(/Cannot add a non-self registering target to an imported TargetGroup/); + }); - test.done(); - }, - - 'HealthCheck fields set if provided'(test: Test) { + test('HealthCheck fields set if provided', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'Stack'); @@ -83,7 +75,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { HealthCheckEnabled: true, HealthCheckIntervalSeconds: 255, HealthCheckPath: '/arbitrary', @@ -94,8 +86,6 @@ export = { }, Port: 80, UnhealthyThresholdCount: 27, - })); - - test.done(); - }, -}; + }); + }); +}); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.actions.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/actions.test.ts similarity index 68% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.actions.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/actions.test.ts index f093cf1df2548..cfb9b67e717ec 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.actions.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/actions.test.ts @@ -1,7 +1,6 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; let stack: cdk.Stack; @@ -9,18 +8,16 @@ let group1: elbv2.NetworkTargetGroup; let group2: elbv2.NetworkTargetGroup; let lb: elbv2.NetworkLoadBalancer; -export = { - 'setUp'(cb: () => void) { - stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'Stack'); - group1 = new elbv2.NetworkTargetGroup(stack, 'TargetGroup1', { vpc, port: 80 }); - group2 = new elbv2.NetworkTargetGroup(stack, 'TargetGroup2', { vpc, port: 80 }); - lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); +beforeEach(() => { + stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + group1 = new elbv2.NetworkTargetGroup(stack, 'TargetGroup1', { vpc, port: 80 }); + group2 = new elbv2.NetworkTargetGroup(stack, 'TargetGroup2', { vpc, port: 80 }); + lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); +}); - cb(); - }, - - 'Forward to multiple targetgroups with an Action and stickiness'(test: Test) { +describe('tests', () => { + test('Forward to multiple targetgroups with an Action and stickiness', () => { // WHEN lb.addListener('Listener', { port: 80, @@ -30,7 +27,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { ForwardConfig: { @@ -50,12 +47,10 @@ export = { Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Weighted forward to multiple targetgroups with an Action'(test: Test) { + test('Weighted forward to multiple targetgroups with an Action', () => { // WHEN lb.addListener('Listener', { port: 80, @@ -68,7 +63,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { ForwardConfig: { @@ -90,7 +85,6 @@ export = { Type: 'forward', }, ], - })); - test.done(); - }, -}; \ No newline at end of file + }); + }); +}); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts similarity index 76% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts index 9c16210a9ed68..6ffbdc0ebd6c3 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts @@ -1,13 +1,13 @@ -import { expect, haveResource, MatchStyle } from '@aws-cdk/assert'; +import { MatchStyle } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; import { FakeSelfRegisteringTarget } from '../helpers'; -export = { - 'Trivial add listener'(test: Test) { +describe('tests', () => { + test('Trivial add listener', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -20,15 +20,13 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'TCP', Port: 443, - })); - - test.done(); - }, + }); + }); - 'Can add target groups'(test: Test) { + test('Can add target groups', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -40,19 +38,17 @@ export = { listener.addTargetGroups('Default', group); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'TargetGroup3D7CD9B8' }, Type: 'forward', }, ], - })); - - test.done(); - }, + }); + }); - 'Can implicitly create target groups'(test: Test) { + test('Can implicitly create target groups', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -66,27 +62,25 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'LBListenerTargetsGroup76EF81E8' }, Type: 'forward', }, ], - })); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + }); + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 80, Protocol: 'TCP', Targets: [ { Id: 'i-12345' }, ], - })); - - test.done(); - }, + }); + }); - 'implicitly created target group inherits protocol'(test: Test) { + test('implicitly created target group inherits protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -100,27 +94,25 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { DefaultActions: [ { TargetGroupArn: { Ref: 'LBListenerTargetsGroup76EF81E8' }, Type: 'forward', }, ], - })); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + }); + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { VpcId: { Ref: 'Stack8A423254' }, Port: 9700, Protocol: 'TCP_UDP', Targets: [ { Id: 'i-12345' }, ], - })); - - test.done(); - }, + }); + }); - 'Enable health check for targets'(test: Test) { + test('Enable health check for targets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -137,14 +129,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { HealthCheckIntervalSeconds: 30, - })); - - test.done(); - }, + }); + }); - 'Enable taking a dependency on an NLB target group\'s load balancer'(test: Test) { + test('Enable taking a dependency on an NLB target group\'s load balancer', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -159,7 +149,7 @@ export = { new ResourceWithLBDependency(stack, 'MyResource', group); // THEN - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { MyResource: { Type: 'Test::Resource', @@ -172,11 +162,9 @@ export = { }, }, }, MatchStyle.SUPERSET); + }); - test.done(); - }, - - 'Trivial add TLS listener'(test: Test) { + test('Trivial add TLS listener', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -195,33 +183,29 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { Protocol: 'TLS', Port: 443, Certificates: [ { CertificateArn: { Ref: 'Certificate4E7ABB08' } }, ], SslPolicy: 'ELBSecurityPolicy-TLS-1-2-2017-01', - })); - - test.done(); - }, + }); + }); - 'Invalid Protocol listener'(test: Test) { + test('Invalid Protocol listener', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); - test.throws(() => lb.addListener('Listener', { + expect(() => lb.addListener('Listener', { port: 443, protocol: elbv2.Protocol.HTTP, defaultTargetGroups: [new elbv2.NetworkTargetGroup(stack, 'Group', { vpc, port: 80 })], - }), /The protocol must be one of TCP, TLS, UDP, TCP_UDP\. Found HTTP/); + })).toThrow(/The protocol must be one of TCP, TLS, UDP, TCP_UDP\. Found HTTP/); + }); - test.done(); - }, - - 'Invalid Listener Target Healthcheck Interval'(test: Test) { + test('Invalid Listener Target Healthcheck Interval', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -235,12 +219,10 @@ export = { const validationErrors: string[] = (targetGroup as any).validate(); const intervalError = validationErrors.find((err) => /Health check interval '60' not supported. Must be one of the following values/.test(err)); - test.notEqual(intervalError, undefined, 'Failed to return health check interval validation error'); - - test.done(); - }, + expect(intervalError).toBeDefined(); + }); - 'validation error if invalid health check protocol'(test: Test) { + test('validation error if invalid health check protocol', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -259,12 +241,10 @@ export = { // THEN const validationErrors: string[] = (targetGroup as any).validate(); - test.deepEqual(validationErrors, ["Health check protocol 'UDP' is not supported. Must be one of [HTTP, HTTPS, TCP]"]); - - test.done(); - }, + expect(validationErrors).toEqual(["Health check protocol 'UDP' is not supported. Must be one of [HTTP, HTTPS, TCP]"]); + }); - 'validation error if invalid path health check protocol'(test: Test) { + test('validation error if invalid path health check protocol', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -284,14 +264,12 @@ export = { // THEN const validationErrors: string[] = (targetGroup as any).validate(); - test.deepEqual(validationErrors, [ + expect(validationErrors).toEqual([ "'TCP' health checks do not support the path property. Must be one of [HTTP, HTTPS]", ]); + }); - test.done(); - }, - - 'validation error if invalid timeout health check'(test: Test) { + test('validation error if invalid timeout health check', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -311,28 +289,24 @@ export = { // THEN const validationErrors: string[] = (targetGroup as any).validate(); - test.deepEqual(validationErrors, [ + expect(validationErrors).toEqual([ 'Custom health check timeouts are not supported for Network Load Balancer health checks. Expected 6 seconds for HTTP, got 10', ]); + }); - test.done(); - }, - - 'Protocol & certs TLS listener'(test: Test) { + test('Protocol & certs TLS listener', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); - test.throws(() => lb.addListener('Listener', { + expect(() => lb.addListener('Listener', { port: 443, protocol: elbv2.Protocol.TLS, defaultTargetGroups: [new elbv2.NetworkTargetGroup(stack, 'Group', { vpc, port: 80 })], - }), /When the protocol is set to TLS, you must specify certificates/); - - test.done(); - }, + })).toThrow(/When the protocol is set to TLS, you must specify certificates/); + }); - 'TLS and certs specified listener'(test: Test) { + test('TLS and certs specified listener', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -340,17 +314,15 @@ export = { domainName: 'example.com', }); - test.throws(() => lb.addListener('Listener', { + expect(() => lb.addListener('Listener', { port: 443, protocol: elbv2.Protocol.TCP, certificates: [{ certificateArn: cert.certificateArn }], defaultTargetGroups: [new elbv2.NetworkTargetGroup(stack, 'Group', { vpc, port: 80 })], - }), /Protocol must be TLS when certificates have been specified/); + })).toThrow(/Protocol must be TLS when certificates have been specified/); + }); - test.done(); - }, - - 'not allowed to specify defaultTargetGroups and defaultAction together'(test: Test) { + test('not allowed to specify defaultTargetGroups and defaultAction together', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -358,17 +330,15 @@ export = { const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); // WHEN - test.throws(() => { + expect(() => { lb.addListener('Listener1', { port: 80, defaultTargetGroups: [group], defaultAction: elbv2.NetworkListenerAction.forward([group]), }); - }, /Specify at most one/); - - test.done(); - }, -}; + }).toThrow(/Specify at most one/); + }); +}); class ResourceWithLBDependency extends cdk.CfnResource { constructor(scope: cdk.Construct, id: string, targetGroup: elbv2.ITargetGroup) { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts similarity index 78% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts index f80755c536586..667b72875ef78 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts @@ -1,12 +1,12 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; -export = { - 'Trivial construction: internet facing'(test: Test) { +describe('tests', () => { + test('Trivial construction: internet facing', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -18,19 +18,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internet-facing', Subnets: [ { Ref: 'StackPublicSubnet1Subnet0AD81D22' }, { Ref: 'StackPublicSubnet2Subnet3C7D2288' }, ], Type: 'network', - })); - - test.done(); - }, + }); + }); - 'Trivial construction: internal'(test: Test) { + test('Trivial construction: internal', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -39,19 +37,17 @@ export = { new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'StackPrivateSubnet1Subnet47AC2BC7' }, { Ref: 'StackPrivateSubnet2SubnetA2F8EDD8' }, ], Type: 'network', - })); - - test.done(); - }, + }); + }); - 'Attributes'(test: Test) { + test('Attributes', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -63,19 +59,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { LoadBalancerAttributes: [ { Key: 'load_balancing.cross_zone.enabled', Value: 'true', }, ], - })); - - test.done(); - }, + }); + }); - 'Access logging'(test: Test) { + test('Access logging', () => { // GIVEN const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -88,7 +82,7 @@ export = { // THEN // verify that the LB attributes reference the bucket - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { LoadBalancerAttributes: [ { Key: 'access_logs.s3.enabled', @@ -99,10 +93,10 @@ export = { Value: { Ref: 'AccessLoggingBucketA6D88F29' }, }, ], - })); + }); // verify the bucket policy allows the ALB to put objects in the bucket - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -135,17 +129,15 @@ export = { }, ], }, - })); + }); // verify the ALB depends on the bucket *and* the bucket policy - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'], - }, ResourcePart.CompleteDefinition)); - - test.done(); - }, + }, ResourcePart.CompleteDefinition); + }); - 'access logging with prefix'(test: Test) { + test('access logging with prefix', () => { // GIVEN const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -157,7 +149,7 @@ export = { // THEN // verify that the LB attributes reference the bucket - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { LoadBalancerAttributes: [ { Key: 'access_logs.s3.enabled', @@ -172,10 +164,10 @@ export = { Value: 'prefix-of-access-logs', }, ], - })); + }); // verify the bucket policy allows the ALB to put objects in the bucket - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -208,12 +200,10 @@ export = { }, ], }, - })); - - test.done(); - }, + }); + }); - 'loadBalancerName'(test: Test) { + test('loadBalancerName', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -225,13 +215,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Name: 'myLoadBalancer', - })); - test.done(); - }, + }); + }); - 'imported network load balancer with no vpc specified throws error when calling addTargets'(test: Test) { + test('imported network load balancer with no vpc specified throws error when calling addTargets', () => { // GIVEN const stack = new cdk.Stack(); const nlbArn = 'arn:aws:elasticloadbalancing::000000000000::dummyloadbalancer'; @@ -240,12 +229,10 @@ export = { }); // WHEN const listener = nlb.addListener('Listener', { port: 80 }); - test.throws(() => listener.addTargets('targetgroup', { port: 8080 })); + expect(() => listener.addTargets('targetgroup', { port: 8080 })).toThrow(); + }); - test.done(); - }, - - 'imported network load balancer with vpc does not throw error when calling addTargets'(test: Test) { + test('imported network load balancer with vpc does not throw error when calling addTargets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -256,12 +243,10 @@ export = { }); // WHEN const listener = nlb.addListener('Listener', { port: 80 }); - test.doesNotThrow(() => listener.addTargets('targetgroup', { port: 8080 })); - - test.done(); - }, + expect(() => listener.addTargets('targetgroup', { port: 8080 })).not.toThrow(); + }); - 'Trivial construction: internal with Isolated subnets only'(test: Test) { + test('Trivial construction: internal with Isolated subnets only', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC', { @@ -279,18 +264,16 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'VPCIsolatedSubnet1SubnetEBD00FC6' }, { Ref: 'VPCIsolatedSubnet2Subnet4B1C8CAA' }, ], Type: 'network', - })); - - test.done(); - }, - 'Internal with Public, Private, and Isolated subnets'(test: Test) { + }); + }); + test('Internal with Public, Private, and Isolated subnets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC', { @@ -316,18 +299,16 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], Type: 'network', - })); - - test.done(); - }, - 'Internet-facing with Public, Private, and Isolated subnets'(test: Test) { + }); + }); + test('Internet-facing with Public, Private, and Isolated subnets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC', { @@ -353,18 +334,16 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internet-facing', Subnets: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, { Ref: 'VPCPublicSubnet2Subnet74179F39' }, ], Type: 'network', - })); - - test.done(); - }, - 'Internal load balancer supplying public subnets'(test: Test) { + }); + }); + test('Internal load balancer supplying public subnets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -377,18 +356,16 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, { Ref: 'VPCPublicSubnet2Subnet74179F39' }, ], Type: 'network', - })); - - test.done(); - }, - 'Internal load balancer supplying isolated subnets'(test: Test) { + }); + }); + test('Internal load balancer supplying isolated subnets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC', { @@ -415,15 +392,13 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { Scheme: 'internal', Subnets: [ { Ref: 'VPCIsolatedSubnet1SubnetEBD00FC6' }, { Ref: 'VPCIsolatedSubnet2Subnet4B1C8CAA' }, ], Type: 'network', - })); - - test.done(); - }, -}; + }); + }); +}); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts similarity index 62% rename from packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.target-group.ts rename to packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts index 08f7d8396b662..692d76e5f42ac 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/target-group.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as elbv2 from '../../lib'; -export = { - 'Enable proxy protocol v2 attribute for target group'(test: Test) { +describe('tests', () => { + test('Enable proxy protocol v2 attribute for target group', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -18,19 +17,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'proxy_protocol_v2.enabled', Value: 'true', }, ], - })); - - test.done(); - }, + }); + }); - 'Disable proxy protocol v2 for attribute target group'(test: Test) { + test('Disable proxy protocol v2 for attribute target group', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -43,19 +40,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { TargetGroupAttributes: [ { Key: 'proxy_protocol_v2.enabled', Value: 'false', }, ], - })); - - test.done(); - }, + }); + }); - 'Configure protocols for target group'(test: Test) { + test('Configure protocols for target group', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -65,14 +60,12 @@ export = { protocol: elbv2.Protocol.UDP, }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Protocol: 'UDP', - })); - - test.done(); - }, + }); + }); - 'Target group defaults to TCP'(test: Test) { + test('Target group defaults to TCP', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -81,25 +74,21 @@ export = { port: 80, }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Protocol: 'TCP', - })); - - test.done(); - }, + }); + }); - 'Throws error for unacceptable protocol'(test: Test) { + test('Throws error for unacceptable protocol', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); - test.throws(() => { + expect(() => { new elbv2.NetworkTargetGroup(stack, 'Group', { vpc, port: 80, protocol: elbv2.Protocol.HTTPS, }); - }); - - test.done(); - }, -}; \ No newline at end of file + }).toThrow(); + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index 094b206eba5d4..48cfc6407e7a4 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -104,21 +104,21 @@ same version will be used for installation. If a lock file is detected (`package `yarn.lock`) it will be used along with the right installer (`npm` or `yarn`). ### Local bundling -If Parcel v2 is available it will be used to bundle your code in your environment. Otherwise, +If Parcel v2.0.0-beta.1 is available it will be used to bundle your code in your environment. Otherwise, bundling will happen in a [Lambda compatible Docker container](https://hub.docker.com/r/amazon/aws-sam-cli-build-image-nodejs12.x). For macOS the recommendend approach is to install Parcel as Docker volume performance is really poor. -Parcel v2 can be installed with: +Parcel v2.0.0-beta.1 can be installed with: ```bash -$ npm install --save-dev parcel@next +$ npm install --save-dev --save-exact parcel@2.0.0-beta.1 ``` OR ```bash -$ yarn add --dev @parcel@next +$ yarn add --dev --exact parcel@2.0.0-beta.1 ``` To force bundling in a Docker container, set the `forceDockerBundling` prop to `true`. This diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundlers.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundlers.ts index d161283685a21..51319927f70be 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundlers.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundlers.ts @@ -5,6 +5,8 @@ import { Runtime } from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import { exec } from './util'; +const PARCEL_VERSION = '2.0.0-beta.1'; + interface BundlerProps { relativeEntryPath: string; cacheDir?: string; @@ -31,14 +33,18 @@ export class LocalBundler implements cdk.ILocalBundling { } try { const parcel = spawnSync(require.resolve('parcel'), ['--version']); - LocalBundler._runsLocally = /^2/.test(parcel.stdout.toString().trim()); // Cache result to avoid unnecessary spawns + const version = parcel.stdout.toString().trim(); + LocalBundler._runsLocally = new RegExp(`^${PARCEL_VERSION}`).test(version); // Cache result to avoid unnecessary spawns + if (!LocalBundler._runsLocally) { + process.stderr.write(`Incorrect parcel version detected: ${version} <> ${PARCEL_VERSION}. Switching to Docker bundling.\n`); + } return LocalBundler._runsLocally; } catch { return false; } } - private static _runsLocally?: boolean; + public static _runsLocally?: boolean; // public for testing purposes constructor(private readonly props: LocalBundlerProps) {} @@ -64,6 +70,7 @@ export class LocalBundler implements cdk.ILocalBundling { process.stderr, // redirect stdout to stderr 'inherit', // inherit stderr ], + cwd: path.dirname(path.join(this.props.projectRoot, this.props.relativeEntryPath)), }); return true; } @@ -88,7 +95,7 @@ export class DockerBundler { buildArgs: { ...props.buildArgs ?? {}, IMAGE: props.runtime.bundlingDockerImage.image, - PARCEL_VERSION: props.parcelVersion ?? '2.0.0-beta.1', + PARCEL_VERSION: props.parcelVersion ?? PARCEL_VERSION, }, }) : cdk.BundlingDockerImage.fromRegistry('dummy'); // Do not build if we don't need to @@ -107,7 +114,7 @@ export class DockerBundler { image, command: ['bash', '-c', command], environment: props.environment, - workingDirectory: path.dirname(path.join(cdk.AssetStaging.BUNDLING_INPUT_DIR, props.relativeEntryPath)), + workingDirectory: path.dirname(path.join(cdk.AssetStaging.BUNDLING_INPUT_DIR, props.relativeEntryPath)).replace(/\\/g, '/'), // Always use POSIX paths in the container, }; } } diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index e0cdc0ba44ba1..064834e6204a0 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -241,9 +241,43 @@ test('Local bundling', () => { ], expect.objectContaining({ env: expect.objectContaining({ KEY: 'value' }), + cwd: '/project/folder', }), ); // Docker image is not built expect(fromAssetMock).not.toHaveBeenCalled(); }); + +test('LocalBundler.runsLocally checks parcel version and caches results', () => { + LocalBundler._runsLocally = undefined; + + const spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValue({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('2.0.0-beta.1'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + expect(LocalBundler.runsLocally).toBe(true); + expect(LocalBundler.runsLocally).toBe(true); + expect(spawnSyncMock).toHaveBeenCalledTimes(1); + expect(spawnSyncMock).toHaveBeenCalledWith(expect.stringContaining('parcel'), ['--version']); +}); + +test('LocalBundler.runsLocally with incorrect parcel version', () => { + LocalBundler._runsLocally = undefined; + + jest.spyOn(child_process, 'spawnSync').mockReturnValue({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('3.5.1'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + expect(LocalBundler.runsLocally).toBe(false); +}); diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json index 7466ec4d88601..8e0748d1ec6db 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3Bucket0552B5BB" + "Ref": "AssetParametersf7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4S3Bucket3D64A262" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10" + "Ref": "AssetParametersf7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4S3VersionKey676CE5F0" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10" + "Ref": "AssetParametersf7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4S3VersionKey676CE5F0" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3Bucket0552B5BB": { + "AssetParametersf7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4S3Bucket3D64A262": { "Type": "String", - "Description": "S3 bucket for asset \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + "Description": "S3 bucket for asset \"f7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4\"" }, - "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10": { + "AssetParametersf7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4S3VersionKey676CE5F0": { "Type": "String", - "Description": "S3 key for asset version \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + "Description": "S3 key for asset version \"f7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4\"" }, - "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeArtifactHash3CE06D09": { + "AssetParametersf7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4ArtifactHashAFB301FE": { "Type": "String", - "Description": "Artifact hash for asset \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + "Description": "Artifact hash for asset \"f7ff2d5c8b3e609d156f7eccbc393419969e89acef6fcc4be4a7091684c1e4d4\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json index 56d8661ab73d8..b422560243832 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3Bucket0552B5BB" + "Ref": "AssetParameters951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576S3Bucket66CABF48" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10" + "Ref": "AssetParameters951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576S3VersionKey46E55A2B" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10" + "Ref": "AssetParameters951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576S3VersionKey46E55A2B" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3Bucket0552B5BB": { + "AssetParameters951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576S3Bucket66CABF48": { "Type": "String", - "Description": "S3 bucket for asset \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + "Description": "S3 bucket for asset \"951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576\"" }, - "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeS3VersionKey9522AC10": { + "AssetParameters951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576S3VersionKey46E55A2B": { "Type": "String", - "Description": "S3 key for asset version \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + "Description": "S3 key for asset version \"951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576\"" }, - "AssetParameters822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1adeArtifactHash3CE06D09": { + "AssetParameters951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576ArtifactHash0B035A28": { "Type": "String", - "Description": "Artifact hash for asset \"822ec544787c04e6b27b02f2408e4c322d48fbd352adb46d5aa2d6b3553a1ade\"" + "Description": "Artifact hash for asset \"951d4d5feb891818f169fae0666f355ee76ca00ff218f5b171de713d8b8da576\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py index 25d1fc8520d7f..c033f37560534 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py +++ b/packages/@aws-cdk/aws-lambda-python/test/lambda-handler/index.py @@ -2,8 +2,8 @@ from PIL import Image def handler(event, context): - response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True).raw - img = Image.open(response) + response = requests.get('https://a0.awsstatic.com/main/images/logos/aws_smile-header-desktop-en-white_59x35.png', stream=True) + img = Image.open(response.raw) print(response.status_code) print(img.size) diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 02b0e5d3285b1..f7b048969b185 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -194,6 +194,14 @@ export abstract class FunctionBase extends Resource implements IFunction { */ protected _connections?: ec2.Connections; + private _latestVersion?: LatestVersion; + + /** + * Mapping of invocation principals to grants. Used to de-dupe `grantInvoke()` calls. + * @internal + */ + protected _invocationGrants: Record = {}; + /** * Adds a permission to the Lambda resource policy. * @param id The id ƒor the permission construct @@ -244,8 +252,10 @@ export abstract class FunctionBase extends Resource implements IFunction { } public get latestVersion(): IVersion { - // Dynamic to avoid infinite recursion when creating the LatestVersion instance... - return new LatestVersion(this); + if (!this._latestVersion) { + this._latestVersion = new LatestVersion(this); + } + return this._latestVersion; } /** @@ -268,29 +278,36 @@ export abstract class FunctionBase extends Resource implements IFunction { * Grant the given identity permissions to invoke this Lambda */ public grantInvoke(grantee: iam.IGrantable): iam.Grant { - return iam.Grant.addToPrincipalOrResource({ - grantee, - actions: ['lambda:InvokeFunction'], - resourceArns: [this.functionArn], - - // Fake resource-like object on which to call addToResourcePolicy(), which actually - // calls addPermission() - resource: { - addToResourcePolicy: (_statement) => { - // Couldn't add permissions to the principal, so add them locally. - const identifier = `Invoke${grantee.grantPrincipal}`; // calls the .toString() of the princpal - this.addPermission(identifier, { - principal: grantee.grantPrincipal!, - action: 'lambda:InvokeFunction', - }); - - return { statementAdded: true, policyDependable: this._functionNode().findChild(identifier) } as iam.AddToResourcePolicyResult; + const identifier = `Invoke${grantee.grantPrincipal}`; // calls the .toString() of the principal + + // Memoize the result so subsequent grantInvoke() calls are idempotent + let grant = this._invocationGrants[identifier]; + if (!grant) { + grant = iam.Grant.addToPrincipalOrResource({ + grantee, + actions: ['lambda:InvokeFunction'], + resourceArns: [this.functionArn], + + // Fake resource-like object on which to call addToResourcePolicy(), which actually + // calls addPermission() + resource: { + addToResourcePolicy: (_statement) => { + // Couldn't add permissions to the principal, so add them locally. + this.addPermission(identifier, { + principal: grantee.grantPrincipal!, + action: 'lambda:InvokeFunction', + }); + + return { statementAdded: true, policyDependable: this._functionNode().findChild(identifier) } as iam.AddToResourcePolicyResult; + }, + node: this.node, + stack: this.stack, + env: this.env, }, - node: this.node, - stack: this.stack, - env: this.env, - }, - }); + }); + this._invocationGrants[identifier] = grant; + } + return grant; } /** @@ -320,15 +337,6 @@ export abstract class FunctionBase extends Resource implements IFunction { }); } - /** - * Checks whether this function is compatible for Lambda@Edge. - * - * @internal - */ - public _checkEdgeCompatibility(): void { - return; - } - /** * Returns the construct tree node that corresponds to the lambda function. * For use internally for constructs, when the tree is set up in non-standard ways. Ex: SingletonFunction. diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts index 17791a8d2ff7e..34ef68dc93e1a 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts @@ -3,7 +3,7 @@ import { Construct, Fn, Lazy, RemovalPolicy } from '@aws-cdk/core'; import { Alias, AliasOptions } from './alias'; import { EventInvokeConfigOptions } from './event-invoke-config'; import { Function } from './function'; -import { FunctionBase, IFunction, QualifiedFunctionBase } from './function-base'; +import { IFunction, QualifiedFunctionBase } from './function-base'; import { CfnVersion } from './lambda.generated'; import { addAlias } from './util'; @@ -253,7 +253,7 @@ export class Version extends QualifiedFunctionBase implements IVersion { return Lazy.stringValue({ produce: () => { // Validate that the underlying function can be used for Lambda@Edge - if (this.lambda instanceof FunctionBase) { + if (this.lambda instanceof Function) { this.lambda._checkEdgeCompatibility(); } diff --git a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts index 5cb9a84f88613..9a8e54472232a 100644 --- a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts @@ -1,7 +1,8 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Function as LambdaFunction, FunctionProps } from './function'; -import { FunctionBase, IFunction } from './function-base'; +import { FunctionBase } from './function-base'; +import { Version } from './lambda-version'; import { Permission } from './permission'; /** @@ -45,7 +46,7 @@ export class SingletonFunction extends FunctionBase { public readonly role?: iam.IRole; public readonly permissionsNode: cdk.ConstructNode; protected readonly canCreatePermissions: boolean; - private lambdaFunction: IFunction; + private lambdaFunction: LambdaFunction; constructor(scope: cdk.Construct, id: string, props: SingletonFunctionProps) { super(scope, id); @@ -61,6 +62,18 @@ export class SingletonFunction extends FunctionBase { this.canCreatePermissions = true; // Doesn't matter, addPermission is overriden anyway } + /** + * Returns a `lambda.Version` which represents the current version of this + * singleton Lambda function. A new version will be created every time the + * function's configuration changes. + * + * You can specify options for this version using the `currentVersionOptions` + * prop when initializing the `lambda.SingletonFunction`. + */ + public get currentVersion(): Version { + return this.lambdaFunction.currentVersion; + } + public addPermission(name: string, permission: Permission) { return this.lambdaFunction.addPermission(name, permission); } @@ -83,9 +96,7 @@ export class SingletonFunction extends FunctionBase { /** @internal */ public _checkEdgeCompatibility() { - if (this.lambdaFunction instanceof FunctionBase) { - return this.lambdaFunction._checkEdgeCompatibility(); - } + return this.lambdaFunction._checkEdgeCompatibility(); } /** @@ -96,12 +107,12 @@ export class SingletonFunction extends FunctionBase { return this.lambdaFunction.node; } - private ensureLambda(props: SingletonFunctionProps): IFunction { + private ensureLambda(props: SingletonFunctionProps): LambdaFunction { const constructName = (props.lambdaPurpose || 'SingletonLambda') + slugify(props.uuid); const existing = cdk.Stack.of(this).node.tryFindChild(constructName); if (existing) { // Just assume this is true - return existing as FunctionBase; + return existing as LambdaFunction; } return new LambdaFunction(cdk.Stack.of(this), constructName, props); diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json index aa5a63c7a3c3d..f91b4ba673c9e 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3Bucket6365D8AA" + "Ref": "AssetParametersfbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1S3BucketF4EA3D4A" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7" + "Ref": "AssetParametersfbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1S3VersionKey50AB224E" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7" + "Ref": "AssetParametersfbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1S3VersionKey50AB224E" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3Bucket6365D8AA": { + "AssetParametersfbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1S3BucketF4EA3D4A": { "Type": "String", - "Description": "S3 bucket for asset \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + "Description": "S3 bucket for asset \"fbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1\"" }, - "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7": { + "AssetParametersfbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1S3VersionKey50AB224E": { "Type": "String", - "Description": "S3 key for asset version \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + "Description": "S3 key for asset version \"fbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1\"" }, - "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdArtifactHashEEC2ED67": { + "AssetParametersfbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1ArtifactHashDD1BB80E": { "Type": "String", - "Description": "Artifact hash for asset \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + "Description": "Artifact hash for asset \"fbf992a3e922c30f9aed74db034a1ec115ec16544160e6820e5d2463bf1e29d1\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda/test/test.function.ts b/packages/@aws-cdk/aws-lambda/test/test.function.ts index 352d438e9c547..94c878161221b 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.function.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.function.ts @@ -359,6 +359,31 @@ export = testCase({ test.done(); }, + 'multiple calls to latestVersion returns the same version'(test: Test) { + const stack = new cdk.Stack(); + + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('hello()'), + handler: 'index.hello', + runtime: lambda.Runtime.NODEJS_10_X, + }); + + const version1 = fn.latestVersion; + const version2 = fn.latestVersion; + + const expectedArn = { + 'Fn::Join': ['', [ + { 'Fn::GetAtt': ['MyLambdaCCE802FB', 'Arn'] }, + ':$LATEST', + ]], + }; + test.equal(version1, version2); + test.deepEqual(stack.resolve(version1.functionArn), expectedArn); + test.deepEqual(stack.resolve(version2.functionArn), expectedArn); + + test.done(); + }, + 'currentVersion': { // see test.function-hash.ts for more coverage for this 'logical id of version is based on the function hash'(test: Test) { diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts index 8c9c4a45fd8e3..e06ce385522ea 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda-version.ts @@ -151,10 +151,10 @@ export = { handler: 'index.handler', code: lambda.Code.fromInline('foo'), }); - const version = fn.addVersion('1'); + const version = fn.currentVersion; // THEN - test.deepEqual(stack.resolve(version.edgeArn), { Ref: 'FnVersion1C3F5F93D' }); + test.deepEqual(stack.resolve(version.edgeArn), { Ref: 'FnCurrentVersion17A89ABB19ed45993ff69fd011ae9fd4ab6e2005' }); test.done(); }, @@ -179,7 +179,7 @@ export = { handler: 'index.handler', code: lambda.Code.fromInline('foo'), }); - const version = fn.addVersion('1'); + const version = fn.currentVersion; // WHEN new lambda.Function(stack, 'OtherFn', { diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 651b5b192203c..2a86ee4bbfbe9 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1020,120 +1020,152 @@ export = { test.done(); }, - 'grantInvoke adds iam:InvokeFunction'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const role = new iam.Role(stack, 'Role', { - assumedBy: new iam.AccountPrincipal('1234'), - }); - const fn = new lambda.Function(stack, 'Function', { - code: lambda.Code.fromInline('xxx'), - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, - }); + 'grantInvoke': { - // WHEN - fn.grantInvoke(role); + 'adds iam:InvokeFunction'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AccountPrincipal('1234'), + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); - // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: 'lambda:InvokeFunction', - Effect: 'Allow', - Resource: { 'Fn::GetAtt': ['Function76856677', 'Arn'] }, - }, - ], - }, - })); + // WHEN + fn.grantInvoke(role); - test.done(); - }, + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Function76856677', 'Arn'] }, + }, + ], + }, + })); - 'grantInvoke with a service principal'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const fn = new lambda.Function(stack, 'Function', { - code: lambda.Code.fromInline('xxx'), - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, - }); - const service = new iam.ServicePrincipal('apigateway.amazonaws.com'); + test.done(); + }, - // WHEN - fn.grantInvoke(service); + 'with a service principal'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); + const service = new iam.ServicePrincipal('apigateway.amazonaws.com'); - // THEN - expect(stack).to(haveResource('AWS::Lambda::Permission', { - Action: 'lambda:InvokeFunction', - FunctionName: { - 'Fn::GetAtt': [ - 'Function76856677', - 'Arn', - ], - }, - Principal: 'apigateway.amazonaws.com', - })); + // WHEN + fn.grantInvoke(service); - test.done(); - }, + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'apigateway.amazonaws.com', + })); - 'grantInvoke with an account principal'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const fn = new lambda.Function(stack, 'Function', { - code: lambda.Code.fromInline('xxx'), - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, - }); - const account = new iam.AccountPrincipal('123456789012'); + test.done(); + }, - // WHEN - fn.grantInvoke(account); + 'with an account principal'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); + const account = new iam.AccountPrincipal('123456789012'); - // THEN - expect(stack).to(haveResource('AWS::Lambda::Permission', { - Action: 'lambda:InvokeFunction', - FunctionName: { - 'Fn::GetAtt': [ - 'Function76856677', - 'Arn', - ], - }, - Principal: '123456789012', - })); + // WHEN + fn.grantInvoke(account); - test.done(); - }, + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: '123456789012', + })); - 'grantInvoke with an arn principal'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const fn = new lambda.Function(stack, 'Function', { - code: lambda.Code.fromInline('xxx'), - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, - }); - const account = new iam.ArnPrincipal('arn:aws:iam::123456789012:role/someRole'); + test.done(); + }, - // WHEN - fn.grantInvoke(account); + 'with an arn principal'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); + const account = new iam.ArnPrincipal('arn:aws:iam::123456789012:role/someRole'); - // THEN - expect(stack).to(haveResource('AWS::Lambda::Permission', { - Action: 'lambda:InvokeFunction', - FunctionName: { - 'Fn::GetAtt': [ - 'Function76856677', - 'Arn', - ], - }, - Principal: 'arn:aws:iam::123456789012:role/someRole', - })); + // WHEN + fn.grantInvoke(account); - test.done(); + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'arn:aws:iam::123456789012:role/someRole', + })); + + test.done(); + }, + + 'can be called twice for the same service principal'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); + const service = new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com'); + + // WHEN + fn.grantInvoke(service); + fn.grantInvoke(service); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'elasticloadbalancing.amazonaws.com', + })); + + test.done(); + }, }, 'Can use metricErrors on a lambda Function'(test: Test) { diff --git a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts index 6df79c0a278eb..0183e78badd27 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts @@ -160,4 +160,28 @@ export = { test.done(); }, + + 'current version of a singleton function'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const singleton = new lambda.SingletonFunction(stack, 'Singleton', { + uuid: '84c0de93-353f-4217-9b0b-45b6c993251a', + code: new lambda.InlineCode('foo'), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + }); + + // WHEN + const version = singleton.currentVersion; + version.addAlias('foo'); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Version', { + FunctionName: { + Ref: 'SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', + }, + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-logs/README.md b/packages/@aws-cdk/aws-logs/README.md index 8d86dfa65e1a7..f505023e1814b 100644 --- a/packages/@aws-cdk/aws-logs/README.md +++ b/packages/@aws-cdk/aws-logs/README.md @@ -246,4 +246,4 @@ const pattern = FilterPattern.spaceDelimited('time', 'component', '...', 'result Be aware that Log Group ARNs will always have the string `:*` appended to them, to match the behavior of [the CloudFormation `AWS::Logs::LogGroup` -resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#aws-resource-logs-loggroup-return-values). \ No newline at end of file +resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#aws-resource-logs-loggroup-return-values). diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index 567b0a3708dee..1fbeeb100e682 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.715.0", + "aws-sdk": "^2.739.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-redshift/lib/cluster.ts b/packages/@aws-cdk/aws-redshift/lib/cluster.ts index 48caa7aabf1db..d21dd186ed8d3 100644 --- a/packages/@aws-cdk/aws-redshift/lib/cluster.ts +++ b/packages/@aws-cdk/aws-redshift/lib/cluster.ts @@ -183,11 +183,11 @@ export interface ClusterProps { readonly parameterGroup?: IClusterParameterGroup; /** - * Number of compute nodes in the cluster + * Number of compute nodes in the cluster. Only specify this property for multi-node clusters. * - * Value must be at least 1 and no more than 100. + * Value must be at least 2 and no more than 100. * - * @default 1 + * @default - 2 if `clusterType` is ClusterType.MULTI_NODE, undefined otherwise */ readonly numberOfNodes?: number; @@ -425,11 +425,7 @@ export class Cluster extends ClusterBase { } const clusterType = props.clusterType || ClusterType.MULTI_NODE; - const nodeCount = props.numberOfNodes !== undefined ? props.numberOfNodes : (clusterType === ClusterType.MULTI_NODE ? 2 : 1); - - if (clusterType === ClusterType.MULTI_NODE && nodeCount < 2) { - throw new Error('Number of nodes for cluster type multi-node must be at least 2'); - } + const nodeCount = this.validateNodeCount(clusterType, props.numberOfNodes); if (props.encrypted === false && props.encryptionKey !== undefined) { throw new Error('Cannot set property encryptionKey without enabling encryption!'); @@ -537,4 +533,20 @@ export class Cluster extends ClusterBase { target: this, }); } + + private validateNodeCount(clusterType: ClusterType, numberOfNodes?: number): number | undefined { + if (clusterType === ClusterType.SINGLE_NODE) { + // This property must not be set for single-node clusters; be generous and treat a value of 1 node as undefined. + if (numberOfNodes !== undefined && numberOfNodes !== 1) { + throw new Error('Number of nodes must be not be supplied or be 1 for cluster type single-node'); + } + return undefined; + } else { + const nodeCount = numberOfNodes ?? 2; + if (nodeCount < 2 || nodeCount > 100) { + throw new Error('Number of nodes for cluster type multi-node must be at least 2 and no more than 100'); + } + return nodeCount; + } + } } diff --git a/packages/@aws-cdk/aws-redshift/test/cluster.test.ts b/packages/@aws-cdk/aws-redshift/test/cluster.test.ts index 385a2f53208b5..d74a8bc2c9ad8 100644 --- a/packages/@aws-cdk/aws-redshift/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-redshift/test/cluster.test.ts @@ -1,17 +1,20 @@ -import { expect as cdkExpect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { ABSENT, expect as cdkExpect, haveResource, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import { Cluster, ClusterParameterGroup, ClusterType } from '../lib'; -import { Cluster, ClusterParameterGroup, ClusterType, NodeType } from '../lib'; +let stack: cdk.Stack; +let vpc: ec2.IVpc; -test('check that instantiation works', () => { - // GIVEN - const stack = testStack(); - const vpc = new ec2.Vpc(stack, 'VPC'); +beforeEach(() => { + stack = testStack(); + vpc = new ec2.Vpc(stack, 'VPC'); +}); +test('check that instantiation works', () => { // WHEN new Cluster(stack, 'Redshift', { masterUser: { @@ -57,8 +60,7 @@ test('check that instantiation works', () => { test('can create a cluster with imported vpc and security group', () => { // GIVEN - const stack = testStack(); - const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { + vpc = ec2.Vpc.fromLookup(stack, 'ImportedVPC', { vpcId: 'VPC12345', }); const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'SecurityGroupId12345'); @@ -83,10 +85,6 @@ test('can create a cluster with imported vpc and security group', () => { }); test('creates a secret when master credentials are not specified', () => { - // GIVEN - const stack = testStack(); - const vpc = new ec2.Vpc(stack, 'VPC'); - // WHEN new Cluster(stack, 'Redshift', { masterUser: { @@ -133,34 +131,88 @@ test('creates a secret when master credentials are not specified', () => { })); }); -test('SIngle Node CLusters spawn only single node', () => { - // GIVEN - const stack = testStack(); - const vpc = new ec2.Vpc(stack, 'VPC'); +describe('node count', () => { + + test('Single Node Clusters do not define node count', () => { + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + clusterType: ClusterType.SINGLE_NODE, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterType: 'single-node', + NumberOfNodes: ABSENT, + })); + }); - // WHEN - new Cluster(stack, 'Redshift', { - masterUser: { - masterUsername: 'admin', - }, - vpc, - nodeType: NodeType.DC1_8XLARGE, - clusterType: ClusterType.SINGLE_NODE, + test('Single Node Clusters treat 1 node as undefined', () => { + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + clusterType: ClusterType.SINGLE_NODE, + numberOfNodes: 1, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterType: 'single-node', + NumberOfNodes: ABSENT, + })); }); - // THEN - cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { - ClusterType: 'single-node', - NodeType: 'dc1.8xlarge', - NumberOfNodes: 1, - })); + test('Single Node Clusters throw if any other node count is specified', () => { + expect(() => { + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + clusterType: ClusterType.SINGLE_NODE, + numberOfNodes: 2, + }); + }).toThrow(/Number of nodes must be not be supplied or be 1 for cluster type single-node/); + }); + + test('Multi-Node Clusters default to 2 nodes', () => { + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + clusterType: ClusterType.MULTI_NODE, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterType: 'multi-node', + NumberOfNodes: 2, + })); + }); + + test.each([0, 1, -1, 101])('Multi-Node Clusters throw with %s nodes', (numberOfNodes: number) => { + expect(() => { + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + clusterType: ClusterType.MULTI_NODE, + numberOfNodes, + }); + }).toThrow(/Number of nodes for cluster type multi-node must be at least 2 and no more than 100/); + }); }); test('create an encrypted cluster with custom KMS key', () => { - // GIVEN - const stack = testStack(); - const vpc = new ec2.Vpc(stack, 'VPC'); - // WHEN new Cluster(stack, 'Redshift', { masterUser: { @@ -182,10 +234,6 @@ test('create an encrypted cluster with custom KMS key', () => { }); test('cluster with parameter group', () => { - // GIVEN - const stack = testStack(); - const vpc = new ec2.Vpc(stack, 'VPC'); - // WHEN const group = new ClusterParameterGroup(stack, 'Params', { description: 'bye', @@ -211,8 +259,6 @@ test('cluster with parameter group', () => { test('imported cluster with imported security group honors allowAllOutbound', () => { // GIVEN - const stack = testStack(); - const cluster = Cluster.fromClusterAttributes(stack, 'Database', { clusterEndpointAddress: 'addr', clusterName: 'identifier', @@ -235,8 +281,6 @@ test('imported cluster with imported security group honors allowAllOutbound', () test('can create a cluster with logging enabled', () => { // GIVEN - const stack = testStack(); - const vpc = new ec2.Vpc(stack, 'VPC'); const bucket = s3.Bucket.fromBucketName(stack, 'bucket', 'logging-bucket'); // WHEN @@ -259,10 +303,6 @@ test('can create a cluster with logging enabled', () => { }); test('throws when trying to add rotation to a cluster without secret', () => { - // GIVEN - const stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'VPC'); - // WHEN const cluster = new Cluster(stack, 'Redshift', { masterUser: { @@ -281,8 +321,6 @@ test('throws when trying to add rotation to a cluster without secret', () => { test('throws validation error when trying to set encryptionKey without enabling encryption', () => { // GIVEN - const stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'VPC'); const key = new kms.Key(stack, 'kms-key'); // WHEN @@ -304,8 +342,6 @@ test('throws validation error when trying to set encryptionKey without enabling test('throws when trying to add single user rotation multiple times', () => { // GIVEN - const stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'VPC'); const cluster = new Cluster(stack, 'Redshift', { masterUser: { masterUsername: 'admin', @@ -323,7 +359,7 @@ test('throws when trying to add single user rotation multiple times', () => { }); function testStack() { - const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); - stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); - return stack; -} \ No newline at end of file + const newTestStack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + newTestStack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + return newTestStack; +} diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index 95e9b0b612c52..5519d2a05ff8c 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -322,11 +322,31 @@ export class TxtRecord extends RecordSet { super(scope, id, { ...props, recordType: RecordType.TXT, - target: RecordTarget.fromValues(...props.values.map(v => JSON.stringify(v))), + target: RecordTarget.fromValues(...props.values.map(v => formatTxt(v))), }); } } +/** + * Formats a text value for use in a TXT record + * + * Use `JSON.stringify` to correctly escape and enclose in double quotes (""). + * + * DNS TXT records can contain up to 255 characters in a single string. TXT + * record strings over 255 characters must be split into multiple text strings + * within the same record. + * + * @see https://aws.amazon.com/premiumsupport/knowledge-center/route53-resolve-dkim-text-record-error/ + */ +function formatTxt(string: string): string { + const result = []; + let idx = 0; + while (idx < string.length) { + result.push(string.slice(idx, idx += 255)); // chunks of 255 characters long + } + return result.map(r => JSON.stringify(r)).join(''); +} + /** * Properties for a SRV record value. */ diff --git a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json index 94a87180fa758..39ca5a6f7ab75 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json @@ -298,6 +298,20 @@ ], "TTL": "1800" } + }, + "TXT0D5C5ACF": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "cdk.test.", + "Type": "TXT", + "HostedZoneId": { + "Ref": "PublicZone2E1C4E34" + }, + "ResourceRecords": [ + "\"this is a very long stringthis is a very long stringthis is a very long stringthis is a very long stringthis is a very long stringthis is a very long stringthis is a very long stringthis is a very long stringthis is a very long stringthis is a very long s\"\"tring\"" + ], + "TTL": "1800" + } } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-route53/test/integ.route53.ts b/packages/@aws-cdk/aws-route53/test/integ.route53.ts index e2e2c6e6c0aea..dd959fb50373d 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.ts @@ -46,6 +46,13 @@ new CaaAmazonRecord(stack, 'CaaAmazon', { zone: publicZone, }); +new TxtRecord(stack, 'TXT', { + zone: publicZone, + values: [ + 'this is a very long string'.repeat(10), + ], +}); + new cdk.CfnOutput(stack, 'PrivateZoneId', { value: privateZone.hostedZoneId }); new cdk.CfnOutput(stack, 'PublicZoneId', { value: publicZone.hostedZoneId }); diff --git a/packages/@aws-cdk/aws-route53/test/test.record-set.ts b/packages/@aws-cdk/aws-route53/test/test.record-set.ts index dd95cbe80cc4c..8da38f60d9720 100644 --- a/packages/@aws-cdk/aws-route53/test/test.record-set.ts +++ b/packages/@aws-cdk/aws-route53/test/test.record-set.ts @@ -295,6 +295,36 @@ export = { test.done(); }, + 'TXT record with value longer than 255 chars'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone', + }); + + // WHEN + new route53.TxtRecord(stack, 'TXT', { + zone, + recordName: 'www', + values: ['hello'.repeat(52)], + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: 'www.myzone.', + Type: 'TXT', + HostedZoneId: { + Ref: 'HostedZoneDB99F866', + }, + ResourceRecords: [ + '"hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello""hello"', + ], + TTL: '1800', + })); + test.done(); + }, + 'SRV record'(test: Test) { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json index 21d2d76dbd488..3f1cef1b0c4f4 100644 --- a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json +++ b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json @@ -1,16 +1,16 @@ { "Parameters": { - "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B": { + "AssetParameters8995e9405bdcae88dc6fc76b4fc224fecfd00ef93663cb759b491c6a13cc59c2S3Bucket32756583": { "Type": "String", - "Description": "S3 bucket for asset \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + "Description": "S3 bucket for asset \"8995e9405bdcae88dc6fc76b4fc224fecfd00ef93663cb759b491c6a13cc59c2\"" }, - "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3VersionKeyA9EAF743": { + "AssetParameters8995e9405bdcae88dc6fc76b4fc224fecfd00ef93663cb759b491c6a13cc59c2S3VersionKey39CCFFC8": { "Type": "String", - "Description": "S3 key for asset version \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + "Description": "S3 key for asset version \"8995e9405bdcae88dc6fc76b4fc224fecfd00ef93663cb759b491c6a13cc59c2\"" }, - "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8ArtifactHashBAE492DD": { + "AssetParameters8995e9405bdcae88dc6fc76b4fc224fecfd00ef93663cb759b491c6a13cc59c2ArtifactHashDF68D9B9": { "Type": "String", - "Description": "Artifact hash for asset \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + "Description": "Artifact hash for asset \"8995e9405bdcae88dc6fc76b4fc224fecfd00ef93663cb759b491c6a13cc59c2\"" } }, "Resources": { @@ -40,7 +40,7 @@ }, ":s3:::", { - "Ref": "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B" + "Ref": "AssetParameters8995e9405bdcae88dc6fc76b4fc224fecfd00ef93663cb759b491c6a13cc59c2S3Bucket32756583" } ] ] @@ -55,7 +55,7 @@ }, ":s3:::", { - "Ref": "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B" + "Ref": "AssetParameters8995e9405bdcae88dc6fc76b4fc224fecfd00ef93663cb759b491c6a13cc59c2S3Bucket32756583" }, "/*" ] diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 4c16b960bbb12..6ab46cb987f1d 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -9,8 +9,6 @@ import { Construct } from './construct-compat'; import { FileSystem, FingerprintOptions } from './fs'; import { Stage } from './stage'; -const STAGING_TMP = '.cdk.staging'; - /** * Initialization properties for `AssetStaging`. */ @@ -89,27 +87,40 @@ export class AssetStaging extends Construct { this.sourcePath = props.sourcePath; this.fingerprintOptions = props; - if (props.bundling) { - this.bundleDir = this.bundle(props.bundling); + const outdir = Stage.of(this)?.outdir; + if (!outdir) { + throw new Error('unable to determine cloud assembly output directory. Assets must be defined indirectly within a "Stage" or an "App" scope'); } - this.assetHash = this.calculateHash(props); + // Determine the hash type based on the props as props.assetHashType is + // optional from a caller perspective. + const hashType = determineHashType(props.assetHashType, props.assetHash); - const stagingDisabled = this.node.tryGetContext(cxapi.DISABLE_ASSET_STAGING_CONTEXT); - if (stagingDisabled) { - this.stagedPath = this.bundleDir ?? this.sourcePath; - } else { - this.relativePath = `asset.${this.assetHash}${path.extname(this.bundleDir ?? this.sourcePath)}`; + if (props.bundling) { + // Determine the source hash in advance of bundling if the asset hash type + // is SOURCE so that the bundler can opt to re-use its previous output. + const sourceHash = hashType === AssetHashType.SOURCE + ? this.calculateHash(hashType, props.assetHash, props.bundling) + : undefined; + + this.bundleDir = this.bundle(props.bundling, outdir, sourceHash); + this.assetHash = sourceHash ?? this.calculateHash(hashType, props.assetHash, props.bundling); + this.relativePath = renderAssetFilename(this.assetHash); this.stagedPath = this.relativePath; + } else { + this.assetHash = this.calculateHash(hashType, props.assetHash); + + const stagingDisabled = this.node.tryGetContext(cxapi.DISABLE_ASSET_STAGING_CONTEXT); + if (stagingDisabled) { + this.stagedPath = this.sourcePath; + } else { + this.relativePath = renderAssetFilename(this.assetHash, path.extname(this.sourcePath)); + this.stagedPath = this.relativePath; + } } this.sourceHash = this.assetHash; - const outdir = Stage.of(this)?.outdir; - if (!outdir) { - throw new Error('unable to determine cloud assembly output directory. Assets must be defined indirectly within a "Stage" or an "App" scope'); - } - this.stageAsset(outdir); } @@ -121,15 +132,23 @@ export class AssetStaging extends Construct { const targetPath = path.join(outdir, this.relativePath); - // Already staged - if (fs.existsSync(targetPath)) { + // Staging the bundling asset. + if (this.bundleDir) { + const isAlreadyStaged = fs.existsSync(targetPath); + + if (isAlreadyStaged && path.resolve(this.bundleDir) !== path.resolve(targetPath)) { + // When an identical asset is already staged and the bundler used an + // intermediate bundling directory, we remove the extra directory. + fs.removeSync(this.bundleDir); + } else if (!isAlreadyStaged) { + fs.renameSync(this.bundleDir, targetPath); + } + return; } - // Asset has been bundled - if (this.bundleDir) { - // Move bundling directory to staging directory - fs.moveSync(this.bundleDir, targetPath); + // Already staged + if (fs.existsSync(targetPath)) { return; } @@ -145,15 +164,40 @@ export class AssetStaging extends Construct { } } - private bundle(options: BundlingOptions): string { - // Temp staging directory in the working directory - const stagingTmp = path.join('.', STAGING_TMP); - fs.ensureDirSync(stagingTmp); + /** + * Bundles an asset and provides the emitted asset's directory in return. + * + * @param options Bundling options + * @param outdir Parent directory to create the bundle output directory in + * @param sourceHash The asset source hash if known in advance. If this field + * is provided, the bundler may opt to skip bundling, providing any already- + * emitted bundle. If this field is not provided, the bundler uses an + * intermediate directory in outdir. + * @returns The fully resolved bundle output directory. + */ + private bundle(options: BundlingOptions, outdir: string, sourceHash?: string): string { + let bundleDir: string; + if (sourceHash) { + // When an asset hash is known in advance of bundling, the bundler outputs + // directly to the assembly output directory. + bundleDir = path.resolve(path.join(outdir, renderAssetFilename(sourceHash))); + + if (fs.existsSync(bundleDir)) { + // Pre-existing bundle directory. The bundle has already been generated + // once before, so we'll give the caller nothing. + return bundleDir; + } + + fs.ensureDirSync(bundleDir); + } else { + // When the asset hash isn't known in advance, bundler outputs to an + // intermediate directory. - // Create temp directory for bundling inside the temp staging directory - const bundleDir = path.resolve(fs.mkdtempSync(path.join(stagingTmp, 'asset-bundle-'))); - // Chmod the bundleDir to full access. - fs.chmodSync(bundleDir, 0o777); + // Create temp directory for bundling inside the temp staging directory + bundleDir = path.resolve(fs.mkdtempSync(path.join(outdir, 'bundling-temp-'))); + // Chmod the bundleDir to full access. + fs.chmodSync(bundleDir, 0o777); + } let user: string; if (options.user) { @@ -193,7 +237,17 @@ export class AssetStaging extends Construct { }); } } catch (err) { - throw new Error(`Failed to bundle asset ${this.node.path}: ${err}`); + // When bundling fails, keep the bundle output for diagnosability, but + // rename it out of the way so that the next run doesn't assume it has a + // valid bundleDir. + const bundleErrorDir = bundleDir + '-error'; + if (fs.existsSync(bundleErrorDir)) { + // Remove the last bundleErrorDir. + fs.removeSync(bundleErrorDir); + } + + fs.renameSync(bundleDir, bundleErrorDir); + throw new Error(`Failed to bundle asset ${this.node.path}, bundle output is located at ${bundleErrorDir}: ${err}`); } if (FileSystem.isEmpty(bundleDir)) { @@ -204,18 +258,26 @@ export class AssetStaging extends Construct { return bundleDir; } - private calculateHash(props: AssetStagingProps): string { - let hashType: AssetHashType; + private calculateHash(hashType: AssetHashType, assetHash?: string, bundling?: BundlingOptions): string { + if (hashType === AssetHashType.CUSTOM && !assetHash) { + throw new Error('`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`.'); + } + + // When bundling a CUSTOM or SOURCE asset hash type, we want the hash to include + // the bundling configuration. We handle CUSTOM and bundled SOURCE hash types + // as a special case to preserve existing user asset hashes in all other cases. + if (hashType == AssetHashType.CUSTOM || (hashType == AssetHashType.SOURCE && bundling)) { + const hash = crypto.createHash('sha256'); + + // if asset hash is provided by user, use it, otherwise fingerprint the source. + hash.update(assetHash ?? FileSystem.fingerprint(this.sourcePath, this.fingerprintOptions)); - if (props.assetHash) { - if (props.assetHashType && props.assetHashType !== AssetHashType.CUSTOM) { - throw new Error(`Cannot specify \`${props.assetHashType}\` for \`assetHashType\` when \`assetHash\` is specified. Use \`CUSTOM\` or leave \`undefined\`.`); + // If we're bundling an asset, include the bundling configuration in the hash + if (bundling) { + hash.update(JSON.stringify(bundling)); } - hashType = AssetHashType.CUSTOM; - } else if (props.assetHashType) { - hashType = props.assetHashType; - } else { - hashType = AssetHashType.SOURCE; + + return hash.digest('hex'); } switch (hashType) { @@ -226,15 +288,31 @@ export class AssetStaging extends Construct { throw new Error('Cannot use `AssetHashType.BUNDLE` when `bundling` is not specified.'); } return FileSystem.fingerprint(this.bundleDir, this.fingerprintOptions); - case AssetHashType.CUSTOM: - if (!props.assetHash) { - throw new Error('`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`.'); - } - // Hash the hash to make sure we can use it in a file/directory name. - // The resulting hash will also have the same length as for the other hash types. - return crypto.createHash('sha256').update(props.assetHash).digest('hex'); default: throw new Error('Unknown asset hash type.'); } } } + +function renderAssetFilename(assetHash: string, extension = '') { + return `asset.${assetHash}${extension}`; +} + +/** + * Determines the hash type from user-given prop values. + * + * @param assetHashType Asset hash type construct prop + * @param assetHash Asset hash given in the construct props + */ +function determineHashType(assetHashType?: AssetHashType, assetHash?: string) { + if (assetHash) { + if (assetHashType && assetHashType !== AssetHashType.CUSTOM) { + throw new Error(`Cannot specify \`${assetHashType}\` for \`assetHashType\` when \`assetHash\` is specified. Use \`CUSTOM\` or leave \`undefined\`.`); + } + return AssetHashType.CUSTOM; + } else if (assetHashType) { + return assetHashType; + } else { + return AssetHashType.SOURCE; + } +} diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index 392b4bd77c519..103d405c1bb2e 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -1,4 +1,5 @@ import { spawnSync, SpawnSyncOptions } from 'child_process'; +import { FileSystem } from './fs'; /** * Bundling options @@ -121,11 +122,26 @@ export class BundlingDockerImage { throw new Error('Failed to extract image ID from Docker build output'); } - return new BundlingDockerImage(match[1]); + // Fingerprints the directory containing the Dockerfile we're building and + // differentiates the fingerprint based on build arguments. We do this so + // we can provide a stable image hash. Otherwise, the image ID will be + // different every time the Docker layer cache is cleared, due primarily to + // timestamps. + const hash = FileSystem.fingerprint(path, { extraHash: JSON.stringify(options) }); + return new BundlingDockerImage(match[1], hash); } /** @param image The Docker image */ - private constructor(public readonly image: string) {} + private constructor(public readonly image: string, private readonly _imageHash?: string) {} + + /** + * Provides a stable representation of this image for JSON serialization. + * + * @return The overridden image name if set or image hash name in that order + */ + public toJSON() { + return this._imageHash ?? this.image; + } /** * Runs a Docker image diff --git a/packages/@aws-cdk/core/test/docker-stub.sh b/packages/@aws-cdk/core/test/docker-stub.sh index 45a78ef881ebd..fe48e93d4a207 100755 --- a/packages/@aws-cdk/core/test/docker-stub.sh +++ b/packages/@aws-cdk/core/test/docker-stub.sh @@ -6,6 +6,7 @@ set -euo pipefail # `/tmp/docker-stub.input` and accepts one of 3 commands that impact it's # behavior. +echo "$@" >> /tmp/docker-stub.input.concat echo "$@" > /tmp/docker-stub.input if echo "$@" | grep "DOCKER_STUB_SUCCESS_NO_OUTPUT"; then diff --git a/packages/@aws-cdk/core/test/test.bundling.ts b/packages/@aws-cdk/core/test/test.bundling.ts index 23be648de03b7..566e5c5008c25 100644 --- a/packages/@aws-cdk/core/test/test.bundling.ts +++ b/packages/@aws-cdk/core/test/test.bundling.ts @@ -1,7 +1,7 @@ import * as child_process from 'child_process'; import { Test } from 'nodeunit'; import * as sinon from 'sinon'; -import { BundlingDockerImage } from '../lib'; +import { BundlingDockerImage, FileSystem } from '../lib'; export = { 'tearDown'(callback: any) { @@ -55,6 +55,10 @@ export = { signal: null, }); + const imageHash = '123456abcdef'; + const fingerprintStub = sinon.stub(FileSystem, 'fingerprint'); + fingerprintStub.callsFake(() => imageHash); + const image = BundlingDockerImage.fromAsset('docker-path', { buildArgs: { TEST_ARG: 'cdk-test', @@ -119,4 +123,33 @@ export = { test.throws(() => image._run(), /\[Status -1\]/); test.done(); }, + + 'BundlerDockerImage json is the bundler image name by default'(test: Test) { + const image = BundlingDockerImage.fromRegistry('alpine'); + + test.equals(image.toJSON(), 'alpine'); + test.done(); + }, + + 'BundlerDockerImage json is the bundler image if building an image'(test: Test) { + const imageId = 'abcdef123456'; + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from(`sha256:${imageId}`), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + const imageHash = '123456abcdef'; + const fingerprintStub = sinon.stub(FileSystem, 'fingerprint'); + fingerprintStub.callsFake(() => imageHash); + + const image = BundlingDockerImage.fromAsset('docker-path'); + + test.equals(image.image, imageId); + test.equals(image.toJSON(), imageHash); + test.ok(fingerprintStub.calledWith('docker-path', sinon.match({ extraHash: JSON.stringify({}) }))); + test.done(); + }, }; diff --git a/packages/@aws-cdk/core/test/test.staging.ts b/packages/@aws-cdk/core/test/test.staging.ts index c2d050ba85e43..8b6c831231d8a 100644 --- a/packages/@aws-cdk/core/test/test.staging.ts +++ b/packages/@aws-cdk/core/test/test.staging.ts @@ -7,6 +7,7 @@ import * as sinon from 'sinon'; import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, Stack } from '../lib'; const STUB_INPUT_FILE = '/tmp/docker-stub.input'; +const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; enum DockerStubCommand { SUCCESS = 'DOCKER_STUB_SUCCESS', @@ -26,6 +27,9 @@ export = { if (fs.existsSync(STUB_INPUT_FILE)) { fs.unlinkSync(STUB_INPUT_FILE); } + if (fs.existsSync(STUB_INPUT_CONCAT_FILE)) { + fs.unlinkSync(STUB_INPUT_CONCAT_FILE); + } cb(); sinon.restore(); }, @@ -105,9 +109,6 @@ export = { const app = new App(); const stack = new Stack(app, 'stack'); const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); - const ensureDirSyncSpy = sinon.spy(fs, 'ensureDirSync'); - const mkdtempSyncSpy = sinon.spy(fs, 'mkdtempSync'); - const chmodSyncSpy = sinon.spy(fs, 'chmodSync'); const processStdErrWriteSpy = sinon.spy(process.stderr, 'write'); // WHEN @@ -126,25 +127,256 @@ export = { `run --rm ${USER_ARG} -v /input:/asset-input:delegated -v /output:/asset-output:delegated -w /asset-input alpine DOCKER_STUB_SUCCESS`, ); test.deepEqual(fs.readdirSync(assembly.directory), [ - 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00', + 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', 'cdk.out', 'manifest.json', 'stack.template.json', 'tree.json', ]); - // asset is bundled in a directory inside .cdk.staging - const stagingTmp = path.join('.', '.cdk.staging'); - test.ok(ensureDirSyncSpy.calledWith(stagingTmp)); - test.ok(mkdtempSyncSpy.calledWith(sinon.match(path.join(stagingTmp, 'asset-bundle-')))); - test.ok(chmodSyncSpy.calledWith(sinon.match(path.join(stagingTmp, 'asset-bundle-')), 0o777)); - // shows a message before bundling test.ok(processStdErrWriteSpy.calledWith('Bundling asset stack/Asset...\n')); test.done(); }, + 'bundler succeeds when staging is disabled'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + stack.node.setContext(cxapi.DISABLE_ASSET_STAGING_CONTEXT, true); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + }, + }); + + // THEN + const assembly = app.synth(); + + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + + test.done(); + }, + + 'bundler reuses its output when it can'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + }, + }); + + new AssetStaging(stack, 'AssetDuplicate', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + }, + }); + + // THEN + const assembly = app.synth(); + + // We're testing that docker was run exactly once even though there are two bundling assets. + test.deepEqual( + readDockerStubInputConcat(), + `run --rm ${USER_ARG} -v /input:/asset-input:delegated -v /output:/asset-output:delegated -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); + + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + + test.done(); + }, + + 'bundler considers its options when reusing bundle output'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + }, + }); + + new AssetStaging(stack, 'AssetWithDifferentBundlingOptions', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + environment: { + UNIQUE_ENV_VAR: 'SOMEVALUE', + }, + }, + }); + + // THEN + const assembly = app.synth(); + + // We're testing that docker was run twice - once for each set of bundler options + // operating on the same source asset. + test.deepEqual( + readDockerStubInputConcat(), + `run --rm ${USER_ARG} -v /input:/asset-input:delegated -v /output:/asset-output:delegated -w /asset-input alpine DOCKER_STUB_SUCCESS\n` + + `run --rm ${USER_ARG} -v /input:/asset-input:delegated -v /output:/asset-output:delegated --env UNIQUE_ENV_VAR=SOMEVALUE -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); + + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', // 'Asset' + 'asset.e80bb8f931b87e84975de193f5a7ecddd7558d3caf3d35d3a536d9ae6539234f', // 'AssetWithDifferentBundlingOptions' + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + + test.done(); + }, + + 'bundler outputs to intermediate dir and renames to asset'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + const mkdtempSyncSpy = sinon.spy(fs, 'mkdtempSync'); + const chmodSyncSpy = sinon.spy(fs, 'chmodSync'); + const renameSyncSpy = sinon.spy(fs, 'renameSync'); + + // WHEN + new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.BUNDLE, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + }, + }); + + // THEN + const assembly = app.synth(); + + test.ok(mkdtempSyncSpy.calledWith(sinon.match(path.join(assembly.directory, 'bundling-temp-')))); + test.ok(chmodSyncSpy.calledWith(sinon.match(path.join(assembly.directory, 'bundling-temp-')), 0o777)); + test.ok(renameSyncSpy.calledWith(sinon.match(path.join(assembly.directory, 'bundling-temp-')), sinon.match(path.join(assembly.directory, 'asset.')))); + + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f', // 'Asset' + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + + test.done(); + }, + + 'bundling failure preserves the bundleDir for diagnosability'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.FAIL], + }, + }), /Failed.*bundl.*asset.*-error/); + + // THEN + const assembly = app.synth(); + + const dir = fs.readdirSync(assembly.directory); + test.ok(dir.some(entry => entry.match(/asset.*-error/))); + + test.done(); + }, + + 'bundler re-uses assets from previous synths'(test: Test) { + // GIVEN + const TEST_OUTDIR = path.join(__dirname, 'cdk.out'); + if (fs.existsSync(TEST_OUTDIR)) { + fs.removeSync(TEST_OUTDIR); + } + + const app = new App({ outdir: TEST_OUTDIR }); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + }, + }); + + // GIVEN + const app2 = new App({ outdir: TEST_OUTDIR }); + const stack2 = new Stack(app2, 'stack'); + + // WHEN + new AssetStaging(stack2, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + }, + }); + + // THEN + const appAssembly = app.synth(); + const app2Assembly = app2.synth(); + + test.deepEqual( + readDockerStubInputConcat(), + `run --rm ${USER_ARG} -v /input:/asset-input:delegated -v /output:/asset-output:delegated -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); + + test.equals(appAssembly.directory, app2Assembly.directory); + test.deepEqual(fs.readdirSync(appAssembly.directory), [ + 'asset.b1e32e86b3523f2fa512eb99180ee2975a50a4439e63e8badd153f2a68d61aa4', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + + test.done(); + }, + 'bundling throws when /asset-ouput is empty'(test: Test) { // GIVEN const app = new App(); @@ -228,10 +460,6 @@ export = { assetHash: 'my-custom-hash', assetHashType: AssetHashType.BUNDLE, }), /Cannot specify `bundle` for `assetHashType`/); - test.equal( - readDockerStubInput(), - `run --rm ${USER_ARG} -v /input:/asset-input:delegated -v /output:/asset-output:delegated -w /asset-input alpine DOCKER_STUB_SUCCESS`, - ); test.done(); }, @@ -316,7 +544,7 @@ export = { }); // THEN - test.ok(dir && /asset-bundle-/.test(dir)); + test.ok(dir && /asset.[0-9a-f]{16,}/.test(dir)); test.equals(opts?.command?.[0], DockerStubCommand.SUCCESS); test.throws(() => readDockerStubInput()); @@ -354,9 +582,20 @@ export = { }, }; +// Reads a docker stub and cleans the volume paths out of the stub. +function readAndCleanDockerStubInput(file: string) { + return fs + .readFileSync(file, 'utf-8') + .trim() + .replace(/-v ([^:]+):\/asset-input/g, '-v /input:/asset-input') + .replace(/-v ([^:]+):\/asset-output/g, '-v /output:/asset-output'); +} + +// Last docker input since last teardown function readDockerStubInput() { - const out = fs.readFileSync(STUB_INPUT_FILE, 'utf-8').trim(); - return out - .replace(/-v ([^:]+):\/asset-input/, '-v /input:/asset-input') - .replace(/-v ([^:]+):\/asset-output/, '-v /output:/asset-output'); + return readAndCleanDockerStubInput(STUB_INPUT_FILE); +} +// Concatenated docker inputs since last teardown +function readDockerStubInputConcat() { + return readAndCleanDockerStubInput(STUB_INPUT_CONCAT_FILE); } diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 7a4695cf8280c..e6fc465932013 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -227,6 +227,33 @@ Example `outputs.json` after deployment of multiple stacks } ``` +##### Deployment Progress + +By default, stack deployment events are displayed as a progress bar with the events for the resource +currently being deployed. + +Set the `--progress` flag to request the complete history which includes all CloudFormation events +```console +$ cdk deploy --progress events +``` + +Alternatively, the `progress` key can be specified in the project config (`cdk.json`). + +The following shows a sample `cdk.json` where the `progress` key is set to *events*. +When `cdk deploy` is executed, deployment events will include the complete history. +``` +{ + "app": "npx ts-node bin/myproject.ts", + "context": { + "@aws-cdk/core:enableStackNameDuplicates": "true", + "aws-cdk:enableDiffNoFail": "true", + "@aws-cdk/core:stackRelativeExports": "true" + }, + "progress": "events" +} +``` +The `progress` key can also be specified as a user setting (`~/.cdk.json`) + #### `cdk destroy` Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were configured with a `DeletionPolicy` of `Retain`). During the stack destruction, the command will output progress diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 016a0855494be..d1b0418a86c4a 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -10,6 +10,7 @@ import { SdkProvider } from '../lib/api/aws-auth'; import { CloudFormationDeployments } from '../lib/api/cloudformation-deployments'; import { CloudExecutable } from '../lib/api/cxapp/cloud-executable'; import { execProgram } from '../lib/api/cxapp/exec'; +import { StackActivityProgress } from '../lib/api/util/cloudformation/stack-activity-monitor'; import { CdkToolkit } from '../lib/cdk-toolkit'; import { RequireApproval } from '../lib/diff'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; @@ -89,7 +90,8 @@ async function parseCommandLineArguments() { .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) - .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }), + .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) + .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events.' }), ) .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only destroy requested stacks, don\'t include dependees' }) @@ -279,6 +281,7 @@ async function initCommandLine() { parameters: parameterMap, usePreviousParameters: args['previous-parameters'], outputsFile: args.outputsFile, + progress: configuration.settings.get(['progress']), ci: args.ci, }); diff --git a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts index 9182e291fee17..53a38108b7d87 100644 --- a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts +++ b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts @@ -6,7 +6,7 @@ import * as fs from 'fs-extra'; import { Mode, SdkProvider } from '../aws-auth'; import { deployStack, DeployStackResult } from '../deploy-stack'; import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; -import { BOOTSTRAP_VERSION_OUTPUT, BootstrapEnvironmentOptions } from './bootstrap-props'; +import { BOOTSTRAP_VERSION_OUTPUT, BootstrapEnvironmentOptions, BOOTSTRAP_VERSION_RESOURCE } from './bootstrap-props'; /** * Perform the actual deployment of a bootstrap stack, given a template and some parameters @@ -61,7 +61,7 @@ export async function deployBootstrapStack( function bootstrapVersionFromTemplate(template: any): number { const versionSources = [ template.Outputs?.[BOOTSTRAP_VERSION_OUTPUT]?.Value, - template.Resources?.[BOOTSTRAP_VERSION_OUTPUT]?.Properties?.Value, + template.Resources?.[BOOTSTRAP_VERSION_RESOURCE]?.Properties?.Value, ]; for (const vs of versionSources) { diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index dac91e4b9e5e6..8b21203ec2a7e 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -7,6 +7,7 @@ import { Mode, SdkProvider } from './aws-auth'; import { deployStack, DeployStackResult, destroyStack } from './deploy-stack'; import { ToolkitInfo } from './toolkit-info'; import { CloudFormationStack, Template } from './util/cloudformation'; +import { StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; export interface DeployStackOptions { /** @@ -89,6 +90,14 @@ export interface DeployStackOptions { */ usePreviousParameters?: boolean; + /** + * Display mode for stack deployment progress. + * + * @default - StackActivityProgress.Bar - stack events will be displayed for + * the resource currently being deployed. + */ + progress?: StackActivityProgress; + /** * Whether we are on a CI system * @@ -163,6 +172,7 @@ export class CloudFormationDeployments { force: options.force, parameters: options.parameters, usePreviousParameters: options.usePreviousParameters, + progress: options.progress, ci: options.ci, }); } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 6fab563802bd3..51f6b7aa11ec4 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -11,7 +11,7 @@ import { contentHash } from '../util/content-hash'; import { ISDK, SdkProvider } from './aws-auth'; import { ToolkitInfo } from './toolkit-info'; import { changeSetHasNoChanges, CloudFormationStack, StackParameters, TemplateParameters, waitForChangeSet, waitForStackDeploy, waitForStackDelete } from './util/cloudformation'; -import { StackActivityMonitor } from './util/cloudformation/stack-activity-monitor'; +import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; // We need to map regions to domain suffixes, and the SDK already has a function to do this. // It's not part of the public API, but it's also unlikely to go away. @@ -153,6 +153,14 @@ export interface DeployStackOptions { */ usePreviousParameters?: boolean; + /** + * Display mode for stack deployment progress. + * + * @default StackActivityProgress.Bar stack events will be displayed for + * the resource currently being deployed. + */ + progress?: StackActivityProgress; + /** * Deploy even if the deployed template is identical to the one we are about to deploy. * @default false @@ -267,6 +275,7 @@ export async function deployStack(options: DeployStackOptions): Promise