diff --git a/.github/workflows/yarn-upgrade.yml b/.github/workflows/yarn-upgrade.yml index 1b0d2e66c0594..bc42c35967a1d 100644 --- a/.github/workflows/yarn-upgrade.yml +++ b/.github/workflows/yarn-upgrade.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: 10 diff --git a/CHANGELOG.md b/CHANGELOG.md index fdc62b8761765..8c2c236e6eb4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ 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.114.0](https://github.com/aws/aws-cdk/compare/v1.113.0...v1.114.0) (2021-07-15) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **appmesh:** `prefixPath` property in `HttpGatewayRouteMatch` has been renamed to `path`, and its type changed from `string` to `HttpGatewayRoutePathMatch` +* **servicecatalog:** `AcceptLanguage` enum has been renamed to `MessageLanguage`, and fields that accepted this enum have been updated to reflect this change. +* **servicecatalog:** property `acceptLanguage` in `PortfolioShareOptions` has been renamed to `messageLanguage`. +* **servicecatalog:** property `acceptLanguage` in `PortfolioProps` has been renamed to `messageLanguage`. +* **servicecatalog:** property `acceptLanguage` in `CloudFormationProductProps` has been renamed `messageLanguage`. +* **appmesh:** `prefixPath` property in `HttpRouteMatch` has been renamed to `path`, and its type changed from `string` to `HttpRoutePathMatch` + +### Features + +* **appmesh:** add Route matching on path, query parameters, metadata, and method name ([#15470](https://github.com/aws/aws-cdk/issues/15470)) ([eeeec5d](https://github.com/aws/aws-cdk/commit/eeeec5d14aa03dbaeeb08fc664c26e82a447f7da)) +* **appmesh:** add support for Gateway Route request matching and path rewriting ([#15527](https://github.com/aws/aws-cdk/issues/15527)) ([1589ff8](https://github.com/aws/aws-cdk/commit/1589ff859e3816e1326b25e4fc855be86f76ffc8)), closes [#15305](https://github.com/aws/aws-cdk/issues/15305) +* **appmesh:** the App Mesh Construct Library is now Generally Available (stable) ([#15560](https://github.com/aws/aws-cdk/issues/15560)) ([718d143](https://github.com/aws/aws-cdk/commit/718d143a376893fb168121b0ff9b57f8a057281e)), closes [#9489](https://github.com/aws/aws-cdk/issues/9489) +* **aws-ecs:** New CDK constructs for ECS Anywhere task and service definitions ([#14931](https://github.com/aws/aws-cdk/issues/14931)) ([3592b26](https://github.com/aws/aws-cdk/commit/3592b26c5806cc31cd6ad0ebba32cbf4d09b9abf)) +* **bootstrap:** widen lookup role permissions for future extension ([#15423](https://github.com/aws/aws-cdk/issues/15423)) ([cafdd3c](https://github.com/aws/aws-cdk/commit/cafdd3c0a619be69c9b6af08664af8e641d4c69b)) +* **cfnspec:** cloudformation spec v39.5.0 ([#15536](https://github.com/aws/aws-cdk/issues/15536)) ([c98e40e](https://github.com/aws/aws-cdk/commit/c98e40e963964ae01b6ad15898a6809687d6a5e3)) +* **pipelines:** revised version of the API ([#12326](https://github.com/aws/aws-cdk/issues/12326)) ([165ee3a](https://github.com/aws/aws-cdk/commit/165ee3aa89bda7c18fcb4820c0bf2f6905adc4ed)), closes [#10872](https://github.com/aws/aws-cdk/issues/10872) +* **servicecatalog:** Add portfolio-product association and tag update constraint ([#15452](https://github.com/aws/aws-cdk/issues/15452)) ([b06f7bf](https://github.com/aws/aws-cdk/commit/b06f7bf8ee59379a3478e4200b941635174c777e)) + + +### Bug Fixes + +* **ecr-assets:** There is already a Construct with name 'Staging' when using tarball image ([#15540](https://github.com/aws/aws-cdk/issues/15540)) ([594d7c6](https://github.com/aws/aws-cdk/commit/594d7c664abed631163ec6b5cfede0a61acb0602)) + ## [1.113.0](https://github.com/aws/aws-cdk/compare/v1.112.0...v1.113.0) (2021-07-12) diff --git a/package.json b/package.json index 17695602e3758..73af824089036 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "fs-extra": "^9.1.0", "graceful-fs": "^4.2.6", "jest-junit": "^12.2.0", - "jsii-diff": "^1.30.0", - "jsii-pacmak": "^1.30.0", - "jsii-reflect": "^1.30.0", - "jsii-rosetta": "^1.30.0", + "jsii-diff": "^1.31.0", + "jsii-pacmak": "^1.31.0", + "jsii-reflect": "^1.31.0", + "jsii-rosetta": "^1.31.0", "lerna": "^4.0.0", "patch-package": "^6.4.7", "standard-version": "^9.3.0", diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/package.json b/packages/@aws-cdk-containers/ecs-service-extensions/package.json index 8ffd15583803a..0d99dadbedb02 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/package.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/package.json @@ -37,7 +37,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/alexa-ask/package.json b/packages/@aws-cdk/alexa-ask/package.json index e9749a1ab0054..6462fd3fcc4ee 100644 --- a/packages/@aws-cdk/alexa-ask/package.json +++ b/packages/@aws-cdk/alexa-ask/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 8ebce872fa08a..45694d1f602c0 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -61,7 +61,7 @@ }, "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^2.17.0", diff --git a/packages/@aws-cdk/assert-internal/package.json b/packages/@aws-cdk/assert-internal/package.json index 4bb9686cb61e9..4d9ae5a6a5ec8 100644 --- a/packages/@aws-cdk/assert-internal/package.json +++ b/packages/@aws-cdk/assert-internal/package.json @@ -24,7 +24,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 43c1d1215a61f..92a9fd2f88bb8 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -36,7 +36,7 @@ "license": "Apache-2.0", "devDependencies": { "aws-cdk-migration": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "constructs": "^3.3.69", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/assertions/package.json b/packages/@aws-cdk/assertions/package.json index d8ea3c6435f45..952063b3423f6 100644 --- a/packages/@aws-cdk/assertions/package.json +++ b/packages/@aws-cdk/assertions/package.json @@ -61,7 +61,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "@aws-cdk/cfnspec": "0.0.0", "constructs": "^3.3.69", diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index f2dc2b033ecdd..f3d8de9b44039 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -69,7 +69,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "@types/sinon": "^9.0.11", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-accessanalyzer/package.json b/packages/@aws-cdk/aws-accessanalyzer/package.json index 6d69476663ea4..dd207f1859f38 100644 --- a/packages/@aws-cdk/aws-accessanalyzer/package.json +++ b/packages/@aws-cdk/aws-accessanalyzer/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-acmpca/package.json b/packages/@aws-cdk/aws-acmpca/package.json index a27f60a2a3ca8..771c64edeb0a8 100644 --- a/packages/@aws-cdk/aws-acmpca/package.json +++ b/packages/@aws-cdk/aws-acmpca/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-amazonmq/package.json b/packages/@aws-cdk/aws-amazonmq/package.json index da7a1697fd0d4..4cdda66e3dbf4 100644 --- a/packages/@aws-cdk/aws-amazonmq/package.json +++ b/packages/@aws-cdk/aws-amazonmq/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-amplify/package.json b/packages/@aws-cdk/aws-amplify/package.json index f8ac33c11dd36..35a569393b9c0 100644 --- a/packages/@aws-cdk/aws-amplify/package.json +++ b/packages/@aws-cdk/aws-amplify/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 1d3d39eda18fd..414d689d1a6e3 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json index 12b16c9674dcc..23690776093aa 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/aws-lambda": "^8.10.77", + "@types/jest": "^26.0.24", + "@types/aws-lambda": "^8.10.78", "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json index 500719dd8a600..e9d7dfdf5b8fc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json @@ -71,8 +71,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/nodeunit": "^0.0.31", + "@types/jest": "^26.0.24", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 31688bfcc0c90..20d03097c4ae7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -78,7 +78,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-appconfig/package.json b/packages/@aws-cdk/aws-appconfig/package.json index 9bdfa88e6bfe2..7ae2918e7a24a 100644 --- a/packages/@aws-cdk/aws-appconfig/package.json +++ b/packages/@aws-cdk/aws-appconfig/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-appflow/package.json b/packages/@aws-cdk/aws-appflow/package.json index 43533eb7a1f22..d3892bc22a18e 100644 --- a/packages/@aws-cdk/aws-appflow/package.json +++ b/packages/@aws-cdk/aws-appflow/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-appintegrations/package.json b/packages/@aws-cdk/aws-appintegrations/package.json index bcde772da06db..cc372cc4fb1f7 100644 --- a/packages/@aws-cdk/aws-appintegrations/package.json +++ b/packages/@aws-cdk/aws-appintegrations/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 9005625667352..a48c9b098229f 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "fast-check": "^2.17.0", diff --git a/packages/@aws-cdk/aws-applicationinsights/package.json b/packages/@aws-cdk/aws-applicationinsights/package.json index b5bc3d711ac9b..0dcc760a0ef2b 100644 --- a/packages/@aws-cdk/aws-applicationinsights/package.json +++ b/packages/@aws-cdk/aws-applicationinsights/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 9b228e77bbb86..6354289442d16 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -5,17 +5,7 @@ ![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) -> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. -> -> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib - -![cdk-constructs: Developer Preview](https://img.shields.io/badge/cdk--constructs-developer--preview-informational.svg?style=for-the-badge) - -> The APIs of higher level constructs in this module are in **developer preview** before they -> become stable. We will only make breaking changes to address unforeseen API issues. Therefore, -> these APIs are not subject to [Semantic Versioning](https://semver.org/), and breaking changes -> will be announced in release notes. This means that while you may use them, you may need to -> update your source code when upgrading to a newer version of this package. +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) --- @@ -406,6 +396,18 @@ A `route` is associated with a virtual router, and it's used to match requests f If your `route` matches a request, you can distribute traffic to one or more target virtual nodes with relative weighting. +The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs. +The `tcp()`, `http()`, `http2()`, and `grpc()` methods provide the spec necessary to define a protocol specific spec. + +For HTTP based routes, the match field can be used to match on +path (prefix, exact, or regex), HTTP method, scheme, HTTP headers, and query parameters. +By default, an HTTP based route will match all requests. + +For gRPC based routes, the match field can be used to match on service name, method name, and metadata. +When specifying the method name, service name must also be specified. + +For example, here's how to add an HTTP route that matches based on a prefix of the URL path: + ```ts router.addRoute('route-http', { routeSpec: appmesh.RouteSpec.http({ @@ -415,13 +417,14 @@ router.addRoute('route-http', { }, ], match: { - prefixPath: '/path-to-app', + // Path that is passed to this method must start with '/'. + path: appmesh.HttpRoutePathMatch.startsWith('/path-to-app'), }, }), }); ``` -Add an HTTP2 route that matches based on method, scheme and header: +Add an HTTP2 route that matches based on exact path, method, scheme, headers, and query parameters: ```ts router.addRoute('route-http2', { @@ -432,14 +435,18 @@ router.addRoute('route-http2', { }, ], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.exactly('/exact'), method: appmesh.HttpRouteMethod.POST, protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ // All specified headers must match for the route to match. appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), appmesh.HeaderMatch.valueIsNot('Content-Type', 'application/json'), - ] + ], + queryParameters: [ + // All specified query parameters must match for the route to match. + appmesh.QueryParameterMatch.valueIs('query-field', 'value') + ], }, }), }); @@ -461,7 +468,7 @@ router.addRoute('route-http', { }, ], match: { - prefixPath: '/path-to-app', + path: appmesh.HttpRoutePathMatch.startsWith('/path-to-app'), }, }), }); @@ -511,12 +518,27 @@ router.addRoute('route-grpc-retry', { }); ``` -The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs. -The `tcp()`, `http()` and `http2()` methods provide the spec necessary to define a protocol specific spec. +Add an gRPC route that matches based on method name and metadata: + +```ts +router.addRoute('route-grpc-retry', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode: node }], + match: { + // When method name is specified, service name must be also specified. + methodName: 'methodname', + serviceName: 'servicename', + metadata: [ + // All specified metadata must match for the route to match. + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + ], + }, + }), +}); +``` -For HTTP based routes, the match field can be used to match on a route prefix. -By default, an HTTP based route will match on `/`. All matches must start with a leading `/`. -The timeout field can also be specified for `idle` and `perRequest` timeouts. +Add a gRPC route with time out: ```ts router.addRoute('route-http', { @@ -599,29 +621,87 @@ The `backendDefaults` property is added to the node while creating the virtual g A _gateway route_ is attached to a virtual gateway and routes traffic to an existing virtual service. If a route matches a request, it can distribute traffic to a target virtual service. -For HTTP based routes, the match field can be used to match on a route prefix. -By default, an HTTP based route will match on `/`. All matches must start with a leading `/`. +For HTTP based gateway routes, the match field can be used to match on +path (prefix, exact, or regex), HTTP method, host name, HTTP headers, and query parameters. +By default, an HTTP based route will match all requests. ```ts gateway.addGatewayRoute('gateway-route-http', { routeSpec: appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { - prefixMatch: '/', + path: appmesh.HttpGatewayRoutePathMatch.regex('regex'), }, }), }); ``` -For GRPC based routes, the match field can be used to match on service names. -You cannot omit the field, and must specify a match for these routes. +For gRPC based gateway routes, the match field can be used to match on service name, host name, and metadata. ```ts gateway.addGatewayRoute('gateway-route-grpc', { routeSpec: appmesh.GatewayRouteSpec.grpc({ routeTarget: virtualService, match: { - serviceName: 'my-service.default.svc.cluster.local', + hostname: appmesh.GatewayRouteHostnameMatch.endsWith('.example.com'), + }, + }), +}); +``` + +For HTTP based gateway routes, App Mesh automatically rewrites the matched prefix path in Gateway Route to “/”. +This automatic rewrite configuration can be overwritten in following ways: + +```ts +gateway.addGatewayRoute('gateway-route-http', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + // This disables the default rewrite to '/', and retains original path. + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/path-to-app/', ''), + }, + }), +}); + +gateway.addGatewayRoute('gateway-route-http-1', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + // If the request full path is '/path-to-app/xxxxx', this rewrites the path to '/rewrittenUri/xxxxx'. + // Please note both `prefixPathMatch` and `rewriteTo` must start and end with the `/` character. + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/path-to-app/', '/rewrittenUri/'), + }, + }), +}); +``` + +If matching other path (exact or regex), only specific rewrite path can be specified. +Unlike `startsWith()` method above, no default rewrite is performed. + +```ts +gateway.addGatewayRoute('gateway-route-http-2', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + // This rewrites the path from '/test' to '/rewrittenPath'. + path: appmesh.HttpGatewayRoutePathMatch.exactly('/test', '/rewrittenPath'), + }, + }), +}); +``` + +For HTTP/gRPC based routes, App Mesh automatically rewrites +the original request received at the Virtual Gateway to the destination Virtual Service name. +This default host name rewrite can be configured by specifying the rewrite rule as one of the `match` property: + +```ts +gateway.addGatewayRoute('gateway-route-grpc', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), + // This disables the default rewrite to virtual service name and retain original request. + rewriteRequestHostname: false, }, }), }); diff --git a/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts index 0a90fb1632ff9..9e8f6315a2356 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts @@ -1,4 +1,9 @@ import { CfnGatewayRoute } from './appmesh.generated'; +import { HeaderMatch } from './header-match'; +import { HttpRouteMethod } from './http-route-method'; +import { HttpGatewayRoutePathMatch } from './http-route-path-match'; +import { validateGrpcMatchArrayLength, validateGrpcGatewayRouteMatch } from './private/utils'; +import { QueryParameterMatch } from './query-parameter-match'; import { Protocol } from './shared-interfaces'; import { IVirtualService } from './virtual-service'; @@ -7,16 +12,105 @@ import { IVirtualService } from './virtual-service'; import { Construct } from '@aws-cdk/core'; /** - * The criterion for determining a request match for this GatewayRoute + * Configuration for gateway route host name match. + */ +export interface GatewayRouteHostnameMatchConfig { + /** + * GatewayRoute CFN configuration for host name match. + */ + readonly hostnameMatch: CfnGatewayRoute.GatewayRouteHostnameMatchProperty; +} + +/** + * Used to generate host name matching methods. + */ +export abstract class GatewayRouteHostnameMatch { + /** + * The value of the host name must match the specified value exactly. + * + * @param name The exact host name to match on + */ + public static exactly(name: string): GatewayRouteHostnameMatch { + return new GatewayRouteHostnameMatchImpl({ exact: name }); + } + + /** + * The value of the host name with the given name must end with the specified characters. + * + * @param suffix The specified ending characters of the host name to match on + */ + public static endsWith(suffix: string): GatewayRouteHostnameMatch { + return new GatewayRouteHostnameMatchImpl({ suffix }); + } + + /** + * Returns the gateway route host name match configuration. + */ + public abstract bind(scope: Construct): GatewayRouteHostnameMatchConfig; +} + +class GatewayRouteHostnameMatchImpl extends GatewayRouteHostnameMatch { + constructor( + private readonly matchProperty: CfnGatewayRoute.GatewayRouteHostnameMatchProperty, + ) { + super(); + } + + bind(_scope: Construct): GatewayRouteHostnameMatchConfig { + return { + hostnameMatch: this.matchProperty, + }; + } +} + +/** + * The criterion for determining a request match for this GatewayRoute. */ export interface HttpGatewayRouteMatch { /** - * Specifies the path to match requests with. - * This parameter must always start with /, which by itself matches all requests to the virtual service name. - * You can also match for path-based routing of requests. For example, if your virtual service name is my-service.local - * and you want the route to match requests to my-service.local/metrics, your prefix should be /metrics. + * Specify how to match requests based on the 'path' part of their URL. + * + * @default - matches requests with any path + */ + readonly path?: HttpGatewayRoutePathMatch; + + /** + * Specifies the client request headers to match on. All specified headers + * must match for the gateway route to match. + * + * @default - do not match on headers + */ + readonly headers?: HeaderMatch[]; + + /** + * The gateway route host name to be matched on. + * + * @default - do not match on host name + */ + readonly hostname?: GatewayRouteHostnameMatch; + + /** + * The method to match on. + * + * @default - do not match on method + */ + readonly method?: HttpRouteMethod; + + /** + * The query parameters to match on. + * All specified query parameters must match for the route to match. + * + * @default - do not match on query parameters */ - readonly prefixPath: string; + readonly queryParameters?: QueryParameterMatch[]; + + /** + * When `true`, rewrites the original request received at the Virtual Gateway to the destination Virtual Service name. + * When `false`, retains the original hostname from the request. + * + * @default true + */ + readonly rewriteRequestHostname?: boolean; } /** @@ -24,9 +118,34 @@ export interface HttpGatewayRouteMatch { */ export interface GrpcGatewayRouteMatch { /** - * The fully qualified domain name for the service to match from the request + * Create service name based gRPC gateway route match. + * + * @default - no matching on service name + */ + readonly serviceName?: string; + + /** + * Create host name based gRPC gateway route match. + * + * @default - no matching on host name + */ + readonly hostname?: GatewayRouteHostnameMatch; + + /** + * Create metadata based gRPC gateway route match. + * All specified metadata must match for the route to match. + * + * @default - no matching on metadata + */ + readonly metadata?: HeaderMatch[]; + + /** + * When `true`, rewrites the original request received at the Virtual Gateway to the destination Virtual Service name. + * When `false`, retains the original hostname from the request. + * + * @default true */ - readonly serviceName: string; + readonly rewriteRequestHostname?: boolean; } /** @@ -34,9 +153,10 @@ export interface GrpcGatewayRouteMatch { */ export interface HttpGatewayRouteSpecOptions { /** - * The criterion for determining a request match for this GatewayRoute + * The criterion for determining a request match for this GatewayRoute. + * When path match is defined, this may optionally determine the path rewrite configuration. * - * @default - matches on '/' + * @default - matches any path and automatically rewrites the path to '/' */ readonly match?: HttpGatewayRouteMatch; @@ -47,7 +167,7 @@ export interface HttpGatewayRouteSpecOptions { } /** - * Properties specific for a GRPC GatewayRoute + * Properties specific for a gRPC GatewayRoute */ export interface GrpcGatewayRouteSpecOptions { /** @@ -110,7 +230,7 @@ export abstract class GatewayRouteSpec { } /** - * Creates an GRPC Based GatewayRoute + * Creates an gRPC Based GatewayRoute * * @param options - no grpc gateway route */ @@ -126,11 +246,6 @@ export abstract class GatewayRouteSpec { } class HttpGatewayRouteSpec extends GatewayRouteSpec { - /** - * The criterion for determining a request match for this GatewayRoute. - * - * @default - matches on '/' - */ readonly match?: HttpGatewayRouteMatch; /** @@ -150,14 +265,21 @@ class HttpGatewayRouteSpec extends GatewayRouteSpec { this.match = options.match; } - public bind(_scope: Construct): GatewayRouteSpecConfig { - const prefixPath = this.match ? this.match.prefixPath : '/'; - if (prefixPath[0] != '/') { - throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); - } + public bind(scope: Construct): GatewayRouteSpecConfig { + const pathMatchConfig = (this.match?.path ?? HttpGatewayRoutePathMatch.startsWith('/')).bind(scope); + const rewriteRequestHostname = this.match?.rewriteRequestHostname; + + const prefixPathRewrite = pathMatchConfig.prefixPathRewrite; + const wholePathRewrite = pathMatchConfig.wholePathRewrite; + const httpConfig: CfnGatewayRoute.HttpGatewayRouteProperty = { match: { - prefix: prefixPath, + prefix: pathMatchConfig.prefixPathMatch, + path: pathMatchConfig.wholePathMatch, + hostname: this.match?.hostname?.bind(scope).hostnameMatch, + method: this.match?.method, + headers: this.match?.headers?.map(header => header.bind(scope).headerMatch), + queryParameters: this.match?.queryParameters?.map(queryParameter => queryParameter.bind(scope).queryParameterMatch), }, action: { target: { @@ -165,6 +287,17 @@ class HttpGatewayRouteSpec extends GatewayRouteSpec { virtualServiceName: this.routeTarget.virtualServiceName, }, }, + rewrite: rewriteRequestHostname !== undefined || prefixPathRewrite || wholePathRewrite + ? { + hostname: rewriteRequestHostname === undefined + ? undefined + : { + defaultTargetHostname: rewriteRequestHostname? 'ENABLED' : 'DISABLED', + }, + prefix: prefixPathRewrite, + path: wholePathRewrite, + } + : undefined, }, }; return { @@ -175,11 +308,6 @@ class HttpGatewayRouteSpec extends GatewayRouteSpec { } class GrpcGatewayRouteSpec extends GatewayRouteSpec { - /** - * The criterion for determining a request match for this GatewayRoute. - * - * @default - no default - */ readonly match: GrpcGatewayRouteMatch; /** @@ -193,18 +321,32 @@ class GrpcGatewayRouteSpec extends GatewayRouteSpec { this.routeTarget = options.routeTarget; } - public bind(_scope: Construct): GatewayRouteSpecConfig { + public bind(scope: Construct): GatewayRouteSpecConfig { + const metadataMatch = this.match.metadata; + + validateGrpcGatewayRouteMatch(this.match); + validateGrpcMatchArrayLength(metadataMatch); + return { grpcSpecConfig: { + match: { + serviceName: this.match.serviceName, + hostname: this.match.hostname?.bind(scope).hostnameMatch, + metadata: metadataMatch?.map(metadata => metadata.bind(scope).headerMatch), + }, action: { target: { virtualService: { virtualServiceName: this.routeTarget.virtualServiceName, }, }, - }, - match: { - serviceName: this.match.serviceName, + rewrite: this.match.rewriteRequestHostname === undefined + ? undefined + : { + hostname: { + defaultTargetHostname: this.match.rewriteRequestHostname ? 'ENABLED' : 'DISABLED', + }, + }, }, }, }; diff --git a/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts b/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts new file mode 100644 index 0000000000000..7131063ac9615 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts @@ -0,0 +1,243 @@ +import { CfnGatewayRoute, CfnRoute } from './appmesh.generated'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * The type returned from the `bind()` method in {@link HttpRoutePathMatch}. + */ +export interface HttpRoutePathMatchConfig { + /** + * Route configuration for matching on the complete URL path of the request. + * + * @default - no matching will be performed on the complete URL path + */ + readonly wholePathMatch?: CfnRoute.HttpPathMatchProperty; + + /** + * Route configuration for matching on the prefix of the URL path of the request. + * + * @default - no matching will be performed on the prefix of the URL path + */ + readonly prefixPathMatch?: string; +} + +/** + * Defines HTTP route matching based on the URL path of the request. + */ +export abstract class HttpRoutePathMatch { + /** + * The value of the path must match the specified value exactly. + * The provided `path` must start with the '/' character. + * + * @param path the exact path to match on + */ + public static exactly(path: string): HttpRoutePathMatch { + return new HttpRouteWholePathMatch({ exact: path }); + } + + /** + * The value of the path must match the specified regex. + * + * @param regex the regex used to match the path + */ + public static regex(regex: string): HttpRoutePathMatch { + return new HttpRouteWholePathMatch({ regex: regex }); + } + + /** + * The value of the path must match the specified prefix. + * + * @param prefix the value to use to match the beginning of the path part of the URL of the request. + * It must start with the '/' character. If provided as "/", matches all requests. + * For example, if your virtual service name is "my-service.local" + * and you want the route to match requests to "my-service.local/metrics", your prefix should be "/metrics". + */ + public static startsWith(prefix: string): HttpRoutePathMatch { + return new HttpRoutePrefixPathMatch(prefix); + } + + /** + * Returns the route path match configuration. + */ + public abstract bind(scope: Construct): HttpRoutePathMatchConfig; +} + +class HttpRoutePrefixPathMatch extends HttpRoutePathMatch { + constructor(private readonly prefix: string) { + super(); + + if (prefix && prefix[0] !== '/') { + throw new Error(`Prefix Path for the match must start with \'/\', got: ${prefix}`); + } + } + + bind(_scope: Construct): HttpRoutePathMatchConfig { + return { + prefixPathMatch: this.prefix, + }; + } +} + +class HttpRouteWholePathMatch extends HttpRoutePathMatch { + constructor(private readonly match: CfnRoute.HttpPathMatchProperty) { + super(); + + if (match.exact && match.exact[0] !== '/') { + throw new Error(`Exact Path for the match must start with \'/\', got: ${match.exact}`); + } + } + + bind(_scope: Construct): HttpRoutePathMatchConfig { + return { + wholePathMatch: this.match, + }; + } +} + +/** + * The type returned from the `bind()` method in {@link HttpGatewayRoutePathMatch}. + */ +export interface HttpGatewayRoutePathMatchConfig { + /** + * Gateway route configuration for matching on the complete URL path of the request. + * + * @default - no matching will be performed on the complete URL path + */ + readonly wholePathMatch?: CfnGatewayRoute.HttpPathMatchProperty; + + /** + * Gateway route configuration for matching on the prefix of the URL path of the request. + * + * @default - no matching will be performed on the prefix of the URL path + */ + readonly prefixPathMatch?: string; + + /** + * Gateway route configuration for rewriting the complete URL path of the request.. + * + * @default - no rewrite will be performed on the request's complete URL path + */ + readonly wholePathRewrite?: CfnGatewayRoute.HttpGatewayRoutePathRewriteProperty; + + /** + * Gateway route configuration for rewriting the prefix of the URL path of the request. + * + * @default - rewrites the request's URL path to '/' + */ + readonly prefixPathRewrite?: CfnGatewayRoute.HttpGatewayRoutePrefixRewriteProperty; +} + +/** + * Defines HTTP gateway route matching based on the URL path of the request. + */ +export abstract class HttpGatewayRoutePathMatch { + /** + * The value of the path must match the specified prefix. + * + * @param prefix the value to use to match the beginning of the path part of the URL of the request. + * It must start with the '/' character. + * When `rewriteTo` is provided, it must also end with the '/' character. + * If provided as "/", matches all requests. + * For example, if your virtual service name is "my-service.local" + * and you want the route to match requests to "my-service.local/metrics", your prefix should be "/metrics". + * @param rewriteTo Specify either disabling automatic rewrite or rewriting to specified prefix path. + * To disable automatic rewrite, provide `''`. + * As a default, request's URL path is automatically rewritten to '/'. + */ + public static startsWith(prefix: string, rewriteTo?: string): HttpGatewayRoutePathMatch { + return new HttpGatewayRoutePrefixPathMatch(prefix, rewriteTo); + } + + /** + * The value of the path must match the specified value exactly. + * The provided `path` must start with the '/' character. + * + * @param path the exact path to match on + * @param rewriteTo the value to substitute for the matched part of the path of the gateway request URL + * As a default, retains original request's URL path. + */ + public static exactly(path: string, rewriteTo?: string): HttpGatewayRoutePathMatch { + return new HttpGatewayRouteWholePathMatch({ exact: path }, rewriteTo); + } + + /** + * The value of the path must match the specified regex. + * + * @param regex the regex used to match the path + * @param rewriteTo the value to substitute for the matched part of the path of the gateway request URL + * As a default, retains original request's URL path. + */ + public static regex(regex: string, rewriteTo?: string): HttpGatewayRoutePathMatch { + return new HttpGatewayRouteWholePathMatch({ regex }, rewriteTo); + } + + /** + * Returns the gateway route path match configuration. + */ + public abstract bind(scope: Construct): HttpGatewayRoutePathMatchConfig; +} + +class HttpGatewayRoutePrefixPathMatch extends HttpGatewayRoutePathMatch { + constructor( + private readonly prefixPathMatch: string, + private readonly rewriteTo?: string, + ) { + super(); + + if (prefixPathMatch[0] !== '/') { + throw new Error('Prefix path for the match must start with \'/\', ' + + `got: ${prefixPathMatch}`); + } + + if (rewriteTo) { + if (prefixPathMatch[prefixPathMatch.length - 1] !== '/') { + throw new Error('When prefix path for the rewrite is specified, prefix path for the match must end with \'/\', ' + + `got: ${prefixPathMatch}`); + } + if (rewriteTo[0] !== '/' || rewriteTo[rewriteTo.length - 1] !== '/') { + throw new Error('Prefix path for the rewrite must start and end with \'/\', ' + + `got: ${rewriteTo}`); + } + } + } + + bind(_scope: Construct): HttpGatewayRoutePathMatchConfig { + return { + prefixPathMatch: this.prefixPathMatch, + prefixPathRewrite: this.rewriteTo === undefined + ? undefined + : { + defaultPrefix: this.rewriteTo === '' ? 'DISABLED' : undefined, + value: this.rewriteTo === '' ? undefined : this.rewriteTo, + }, + }; + } +} + +class HttpGatewayRouteWholePathMatch extends HttpGatewayRoutePathMatch { + constructor( + private readonly wholePathMatch: CfnGatewayRoute.HttpPathMatchProperty, + private readonly exactPathRewrite?: string | undefined, + ) { + super(); + + if (wholePathMatch.exact && wholePathMatch.exact[0] !== '/') { + throw new Error(`Exact Path for the match must start with \'/\', got: ${ wholePathMatch.exact }`); + } + if (exactPathRewrite === '') { + throw new Error('Exact Path for the rewrite cannot be empty. Unlike startsWith() method, no automatic rewrite on whole path match'); + } + if (exactPathRewrite && exactPathRewrite[0] !== '/') { + throw new Error(`Exact Path for the rewrite must start with \'/\', got: ${ exactPathRewrite }`); + } + } + + bind(_scope: Construct): HttpGatewayRoutePathMatchConfig { + return { + wholePathMatch: this.wholePathMatch, + wholePathRewrite: this.exactPathRewrite === undefined ? undefined : { exact: this.exactPathRewrite }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index dbdd9d79bb610..d5ea8fedb70d6 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -21,3 +21,5 @@ export * from './tls-validation'; export * from './tls-client-policy'; export * from './http-route-method'; export * from './header-match'; +export * from './query-parameter-match'; +export * from './http-route-path-match'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts index cb442ac34044f..0820ae42c41aa 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -1,7 +1,10 @@ import { Token, TokenComparison } from '@aws-cdk/core'; import { CfnVirtualNode } from '../appmesh.generated'; +import { GrpcGatewayRouteMatch } from '../gateway-route-spec'; import { HeaderMatch } from '../header-match'; import { ListenerTlsOptions } from '../listener-tls-options'; +import { QueryParameterMatch } from '../query-parameter-match'; +import { GrpcRouteMatch } from '../route-spec'; import { TlsClientPolicy } from '../tls-client-policy'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -95,13 +98,47 @@ export function renderMeshOwner(resourceAccount: string, meshAccount: string) : } /** - * This is the helper method to validate the length of match array when it is specified. + * This is the helper method to validate the length of HTTP match array when it is specified. */ -export function validateMatchArrayLength(headers?: HeaderMatch[]) { +export function validateHttpMatchArrayLength(headers?: HeaderMatch[], queryParameters?: QueryParameterMatch[]) { const MIN_LENGTH = 1; const MAX_LENGTH = 10; if (headers && (headers.length < MIN_LENGTH || headers.length > MAX_LENGTH)) { throw new Error(`Number of headers provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${headers.length}`); } + + if (queryParameters && (queryParameters.length < MIN_LENGTH || queryParameters.length > MAX_LENGTH)) { + throw new Error(`Number of query parameters provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${queryParameters.length}`); + } +} + +/** + * This is the helper method to validate the length of gRPC match array when it is specified. + */ +export function validateGrpcMatchArrayLength(metadata?: HeaderMatch[]): void { + const MIN_LENGTH = 1; + const MAX_LENGTH = 10; + + if (metadata && (metadata.length < MIN_LENGTH || metadata.length > MAX_LENGTH)) { + throw new Error(`Number of metadata provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${metadata.length}`); + } +} + +/** + * This is the helper method to validate at least one of gRPC route match type is defined. + */ +export function validateGrpcRouteMatch(match: GrpcRouteMatch): void { + if (match.serviceName === undefined && match.metadata === undefined && match.methodName === undefined) { + throw new Error('At least one gRPC route match property must be provided'); + } +} + +/** + * This is the helper method to validate at least one of gRPC gateway route match type is defined. + */ +export function validateGrpcGatewayRouteMatch(match: GrpcGatewayRouteMatch): void { + if (match.serviceName === undefined && match.metadata === undefined && match.hostname === undefined) { + throw new Error('At least one gRPC gateway route match property beside rewriteRequestHostname must be provided'); + } } diff --git a/packages/@aws-cdk/aws-appmesh/lib/query-parameter-match.ts b/packages/@aws-cdk/aws-appmesh/lib/query-parameter-match.ts new file mode 100644 index 0000000000000..585d810cef051 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/query-parameter-match.ts @@ -0,0 +1,54 @@ +import { CfnRoute } from './appmesh.generated'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Configuration for `QueryParameterMatch` + */ +export interface QueryParameterMatchConfig { + /** + * Route CFN configuration for route query parameter match. + */ + readonly queryParameterMatch: CfnRoute.QueryParameterProperty; +} + +/** + * Used to generate query parameter matching methods. + */ +export abstract class QueryParameterMatch { + /** + * The value of the query parameter with the given name in the request must match the + * specified value exactly. + * + * @param queryParameterName the name of the query parameter to match against + * @param queryParameterValue The exact value to test against + */ + static valueIs(queryParameterName: string, queryParameterValue: string): QueryParameterMatch { + return new QueryParameterMatchImpl(queryParameterName, { exact: queryParameterValue }); + } + + /** + * Returns the query parameter match configuration. + */ + public abstract bind(scope: Construct): QueryParameterMatchConfig; +} + +class QueryParameterMatchImpl extends QueryParameterMatch { + constructor( + private readonly queryParameterName: string, + private readonly matchProperty: CfnRoute.HttpQueryParameterMatchProperty, + ) { + super(); + } + + bind(_scope: Construct): QueryParameterMatchConfig { + return { + queryParameterMatch: { + match: this.matchProperty, + name: this.queryParameterName, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index bdaa8a5fc486b..a27d589a61ded 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -2,7 +2,9 @@ import * as cdk from '@aws-cdk/core'; import { CfnRoute } from './appmesh.generated'; import { HeaderMatch } from './header-match'; import { HttpRouteMethod } from './http-route-method'; -import { validateMatchArrayLength } from './private/utils'; +import { HttpRoutePathMatch } from './http-route-path-match'; +import { validateGrpcRouteMatch, validateGrpcMatchArrayLength, validateHttpMatchArrayLength } from './private/utils'; +import { QueryParameterMatch } from './query-parameter-match'; import { GrpcTimeout, HttpTimeout, Protocol, TcpTimeout } from './shared-interfaces'; import { IVirtualNode } from './virtual-node'; @@ -28,16 +30,15 @@ export interface WeightedTarget { } /** - * The criterion for determining a request match for this GatewayRoute + * The criterion for determining a request match for this Route */ export interface HttpRouteMatch { /** - * Specifies the path to match requests with. - * This parameter must always start with /, which by itself matches all requests to the virtual service name. - * You can also match for path-based routing of requests. For example, if your virtual service name is my-service.local - * and you want the route to match requests to my-service.local/metrics, your prefix should be /metrics. + * Specifies how is the request matched based on the path part of its URL. + * + * @default - matches requests with all paths */ - readonly prefixPath: string; + readonly path?: HttpRoutePathMatch; /** * Specifies the client request headers to match on. All specified headers @@ -60,6 +61,14 @@ export interface HttpRouteMatch { * @default - do not match on HTTP2 request protocol */ readonly protocol?: HttpRouteProtocol; + + /** + * The query parameters to match on. + * All specified query parameters must match for the route to match. + * + * @default - do not match on query parameters + */ + readonly queryParameters?: QueryParameterMatch[]; } /** @@ -78,13 +87,32 @@ export enum HttpRouteProtocol { } /** - * The criterion for determining a request match for this GatewayRoute + * The criterion for determining a request match for this Route. + * At least one match type must be selected. */ export interface GrpcRouteMatch { /** - * The fully qualified domain name for the service to match from the request + * Create service name based gRPC route match. + * + * @default - do not match on service name + */ + readonly serviceName?: string; + + /** + * Create metadata based gRPC route match. + * All specified metadata must match for the route to match. + * + * @default - do not match on metadata */ - readonly serviceName: string; + readonly metadata?: HeaderMatch[]; + + /** + * The method name to match from the request. + * If the method name is specified, service name must be also provided. + * + * @default - do not match on method name + */ + readonly methodName?: string; } /** @@ -297,7 +325,7 @@ export enum GrpcRetryEvent { } /** - * All Properties for GatewayRoute Specs + * All Properties for Route Specs */ export interface RouteSpecConfig { /** @@ -371,7 +399,7 @@ export abstract class RouteSpec { } /** - * Called when the GatewayRouteSpec type is initialized. Can be used to enforce + * Called when the RouteSpec type is initialized. Can be used to enforce * mutual exclusivity with future properties */ public abstract bind(scope: Construct): RouteSpecConfig; @@ -415,23 +443,25 @@ class HttpRouteSpec extends RouteSpec { } public bind(scope: Construct): RouteSpecConfig { - const prefixPath = this.match ? this.match.prefixPath : '/'; - if (prefixPath[0] != '/') { - throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); - } + const pathMatchConfig = (this.match?.path ?? HttpRoutePathMatch.startsWith('/')).bind(scope); + // Set prefix path match to '/' if none of path matches are defined. const headers = this.match?.headers; - validateMatchArrayLength(headers); + const queryParameters = this.match?.queryParameters; + + validateHttpMatchArrayLength(headers, queryParameters); const httpConfig: CfnRoute.HttpRouteProperty = { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), }, match: { - prefix: prefixPath, + prefix: pathMatchConfig.prefixPathMatch, + path: pathMatchConfig.wholePathMatch, headers: headers?.map(header => header.bind(scope).headerMatch), method: this.match?.method, scheme: this.match?.protocol, + queryParameters: queryParameters?.map(queryParameter => queryParameter.bind(scope).queryParameterMatch), }, timeout: renderTimeout(this.timeout), retryPolicy: this.retryPolicy ? renderHttpRetryPolicy(this.retryPolicy) : undefined, @@ -520,7 +550,18 @@ class GrpcRouteSpec extends RouteSpec { } } - public bind(_scope: Construct): RouteSpecConfig { + public bind(scope: Construct): RouteSpecConfig { + const serviceName = this.match.serviceName; + const methodName = this.match.methodName; + const metadata = this.match.metadata; + + validateGrpcRouteMatch(this.match); + validateGrpcMatchArrayLength(metadata); + + if (methodName && !serviceName) { + throw new Error('If you specify a method name, you must also specify a service name'); + } + return { priority: this.priority, grpcRouteSpec: { @@ -528,7 +569,9 @@ class GrpcRouteSpec extends RouteSpec { weightedTargets: renderWeightedTargets(this.weightedTargets), }, match: { - serviceName: this.match.serviceName, + serviceName: serviceName, + methodName: methodName, + metadata: metadata?.map(singleMetadata => singleMetadata.bind(scope).headerMatch), }, timeout: renderTimeout(this.timeout), retryPolicy: this.retryPolicy ? renderGrpcRetryPolicy(this.retryPolicy) : undefined, @@ -538,8 +581,8 @@ class GrpcRouteSpec extends RouteSpec { } /** -* Utility method to add weighted route targets to an existing route -*/ + * Utility method to add weighted route targets to an existing route + */ function renderWeightedTargets(weightedTargets: WeightedTarget[]): CfnRoute.WeightedTargetProperty[] { const renderedTargets: CfnRoute.WeightedTargetProperty[] = []; for (const t of weightedTargets) { diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index cd4af5016fb1a..d4d9a0bdfe84c 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -189,8 +189,8 @@ "no-unused-type:@aws-cdk/aws-appmesh.Protocol" ] }, - "stability": "experimental", - "maturity": "developer-preview", + "stability": "stable", + "maturity": "stable", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index 3744fe7583cf1..4b6c3e54f543e 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -931,6 +931,102 @@ "RouteName": "route-grpc-retry" } }, + "meshrouterroute699804AE1": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode2092BA426", + "VirtualNodeName" + ] + }, + "Weight": 30 + } + ] + }, + "Match": { + "Path": { + "Regex": "regex" + }, + "QueryParameters": [ + { + "Match": { + "Exact": "value" + }, + "Name": "query-field" + } + ] + } + } + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-6" + } + }, + "meshrouterroute76C21E6E7": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "GrpcRoute": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode4AE87F692", + "VirtualNodeName" + ] + }, + "Weight": 20 + } + ] + }, + "Match": { + "Metadata": [ + { + "Invert": false, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + } + ], + "MethodName": "test-method", + "ServiceName": "test-service" + } + } + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-7" + } + }, "meshnode726C787D": { "Type": "AWS::AppMesh::VirtualNode", "Properties": { @@ -1276,6 +1372,141 @@ "GatewayRouteName": "meshstackmeshgateway1gateway1routehttpBA921D42" } }, + "meshgateway1gateway1routehttp2B672D43F": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "HttpRoute": { + "Action": { + "Rewrite": { + "Hostname": { + "DefaultTargetHostname": "ENABLED" + }, + "Prefix": { + "DefaultPrefix": "DISABLED" + } + }, + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "service6D174F83", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Headers": [ + { + "Invert": false, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Prefix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Suffix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Regex": "application/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Prefix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Suffix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Regex": "application/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Content-Type" + } + ], + "Hostname": { + "Exact": "example.com" + }, + "Method": "POST", + "Prefix": "/", + "QueryParameters": [ + { + "Match": { + "Exact": "value" + }, + "Name": "query-field" + } + ] + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp27F17263B" + } + }, "meshgateway1gateway1routehttp2FD69C306": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { @@ -1313,6 +1544,89 @@ "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp255781963" } }, + "meshgateway1gateway1routehttp2225001508": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "Rewrite": { + "Path": { + "Exact": "/rewrittenpath" + } + }, + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "service6D174F83", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Path": { + "Exact": "/exact" + } + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp22BD49AE9D" + } + }, + "meshgateway1gateway1routehttp2376EB99D6": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "service6D174F83", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Path": { + "Regex": "regex" + } + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp23E44F5774" + } + }, "meshgateway1gateway1routegrpc76486062": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { @@ -1355,6 +1669,128 @@ "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpcCD4D891D" } }, + "meshgateway1gateway1routegrpc2FAC1FF36": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "GrpcRoute": { + "Action": { + "Rewrite": { + "Hostname": { + "DefaultTargetHostname": "DISABLED" + } + }, + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "service6D174F83", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Hostname": { + "Exact": "example.com" + }, + "Metadata": [ + { + "Invert": false, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Exact": "text/html" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Prefix": "application/" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Prefix": "text/" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Suffix": "/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Suffix": "/json+foobar" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Regex": "application/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Regex": "text/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Max-Forward" + }, + { + "Invert": true, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Max-Forward" + } + ] + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpc2AE8379FD" + } + }, "service6D174F83": { "Type": "AWS::AppMesh::VirtualService", "Properties": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 45c1a2c4917fe..b01bb32cde119 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -55,7 +55,7 @@ router.addRoute('route-1', { }, ], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), }, timeout: { idle: cdk.Duration.seconds(10), @@ -158,7 +158,7 @@ router.addRoute('route-2', { }, ], match: { - prefixPath: '/path2', + path: appmesh.HttpRoutePathMatch.startsWith('/path2'), }, timeout: { idle: cdk.Duration.seconds(11), @@ -202,7 +202,7 @@ router.addRoute('route-matching', { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode: node3 }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), method: appmesh.HttpRouteMethod.POST, protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ @@ -254,6 +254,41 @@ router.addRoute('route-grpc-retry', { }), }); +router.addRoute('route-6', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [ + { + virtualNode: node2, + weight: 30, + }, + ], + match: { + path: appmesh.HttpRoutePathMatch.regex('regex'), + queryParameters: [ + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + ], + }, + }), +}); + +router.addRoute('route-7', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [ + { + virtualNode: node4, + weight: 20, + }, + ], + match: { + serviceName: 'test-service', + methodName: 'test-method', + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + ], + }, + }), +}); + const gateway = mesh.addVirtualGateway('gateway1', { accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), virtualGatewayName: 'gateway1', @@ -304,12 +339,57 @@ gateway.addGatewayRoute('gateway1-route-http', { }), }); +gateway.addGatewayRoute('gateway1-route-http-2', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/', ''), + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), + method: appmesh.HttpRouteMethod.POST, + headers: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valuesIsInRange('Content-Type', 1, 5), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valuesIsNotInRange('Content-Type', 1, 5), + ], + queryParameters: [ + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + ], + rewriteRequestHostname: true, + }, + }), +}); + gateway.addGatewayRoute('gateway1-route-http2', { routeSpec: appmesh.GatewayRouteSpec.http2({ routeTarget: virtualService, }), }); +gateway.addGatewayRoute('gateway1-route-http2-2', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.exactly('/exact', '/rewrittenpath'), + }, + }), +}); + +gateway.addGatewayRoute('gateway1-route-http2-3', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.regex('regex'), + }, + }), +}); + gateway.addGatewayRoute('gateway1-route-grpc', { routeSpec: appmesh.GatewayRouteSpec.grpc({ routeTarget: virtualService, @@ -318,3 +398,25 @@ gateway.addGatewayRoute('gateway1-route-grpc', { }, }), }); + +gateway.addGatewayRoute('gateway1-route-grpc-2', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + rewriteRequestHostname: false, + }, + }), +}); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts index 76f325e99dd10..eb5bf6145282d 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts @@ -1,7 +1,6 @@ import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; - import * as appmesh from '../lib'; export = { @@ -128,10 +127,10 @@ export = { test.throws(() => appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { - prefixPath: 'wrong', + path: appmesh.HttpRoutePathMatch.startsWith('wrong'), }, }).bind(stack), - /Prefix Path must start with \'\/\', got: wrong/); + /Prefix Path for the match must start with \'\/\', got: wrong/); test.done(); }, @@ -172,6 +171,961 @@ export = { test.done(); }, }, + + 'with host name rewrite': { + 'should set default target host name'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + rewriteRequestHostname: true, + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + serviceName: virtualService.virtualServiceName, + rewriteRequestHostname: false, + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Action: { + Rewrite: { + Hostname: { + DefaultTargetHostname: 'ENABLED', + }, + }, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-grpc-route', + Spec: { + GrpcRoute: { + Action: { + Rewrite: { + Hostname: { + DefaultTargetHostname: 'DISABLED', + }, + }, + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with wholePath rewrite': { + 'should set exact path'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP2 Route + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + method: appmesh.HttpRouteMethod.GET, + path: appmesh.HttpGatewayRoutePathMatch.exactly('/test', '/rewrittenPath'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route', + Spec: { + Http2Route: { + Action: { + Rewrite: { + Path: { + Exact: '/rewrittenPath', + }, + }, + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with prefix rewrite': { + 'should set default prefix or value'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/test/', ''), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // Add an HTTP2 Route + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/test/', '/rewrittenUri/'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + + // Add an HTTP2 Route + virtualGateway.addGatewayRoute('gateway-http2-route-1', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/test/'), + }, + }), + gatewayRouteName: 'gateway-http2-route-1', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Action: { + Rewrite: { + Prefix: { + DefaultPrefix: 'DISABLED', + }, + }, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route', + Spec: { + Http2Route: { + Action: { + Rewrite: { + Prefix: { + Value: '/rewrittenUri/', + }, + }, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route-1', + Spec: { + Http2Route: { + Action: { + Rewrite: ABSENT, + }, + }, + }, + })); + + test.done(); + }, + + "should throw an error if the prefix match does not start and end with '/'"(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // WHEN + THEN + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('test/', '/rewrittenUri/'), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + }, /Prefix path for the match must start with \'\/\', got: test\//); + + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/test', '/rewrittenUri/'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + }, /When prefix path for the rewrite is specified, prefix path for the match must end with \'\/\', got: \/test/); + + test.done(); + }, + + "should throw an error if the custom prefix does not start and end with '/'"(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // WHEN + THEN + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/', 'rewrittenUri/'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + }, /Prefix path for the rewrite must start and end with \'\/\', got: rewrittenUri\//); + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http2-route-1', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/', '/rewrittenUri'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + }, /Prefix path for the rewrite must start and end with \'\/\', got: \/rewrittenUri/); + + test.done(); + }, + }, + + 'with host name match': { + 'should match based on host name'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + hostname: appmesh.GatewayRouteHostnameMatch.endsWith('.example.com'), + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Hostname: { + Exact: 'example.com', + }, + }, + Action: { + Rewrite: ABSENT, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-grpc-route', + Spec: { + GrpcRoute: { + Match: { + Hostname: { + Suffix: '.example.com', + }, + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with metadata match': { + 'should match based on metadata'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-grpc-route', + Spec: { + GrpcRoute: { + Match: { + Metadata: [ + { + Invert: false, + Match: { Exact: 'application/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Exact: 'text/html' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Prefix: 'application/' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Prefix: 'text/' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Suffix: '/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Suffix: '/json+foobar' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Regex: 'application/.*' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Regex: 'text/.*' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + { + Invert: true, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + ], + }, + }, + }, + })); + + test.done(); + }, + + 'should throw an error if the array length is invalid'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + // size 0 array + metadata: [ + ], + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + }, /Number of metadata provided for matching must be between 1 and 10/); + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-grpc-route-1', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + // size 11 array + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + }, /Number of metadata provided for matching must be between 1 and 10/); + + test.done(); + }, + }, + + 'with header match': { + 'should match based on header'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + headers: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Headers: [ + { + Invert: false, + Match: { Exact: 'application/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Exact: 'text/html' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Prefix: 'application/' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Prefix: 'text/' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Suffix: '/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Suffix: '/json+foobar' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Regex: 'application/.*' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Regex: 'text/.*' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + { + Invert: true, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + ], + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with method match': { + 'should match based on method'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + method: appmesh.HttpRouteMethod.DELETE, + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Method: 'DELETE', + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with path match': { + 'should match based on path'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.exactly('/exact', undefined), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // Add an HTTP2 Route + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.regex('regex'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Path: { + Exact: '/exact', + }, + }, + Action: { + Rewrite: ABSENT, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route', + Spec: { + Http2Route: { + Match: { + Path: { + Regex: 'regex', + }, + }, + }, + }, + })); + + test.done(); + }, + + 'should throw an error if empty string is passed'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // WHEN + THEN + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.exactly('/exact', ''), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + }, /Exact Path for the rewrite cannot be empty. Unlike startsWith\(\) method, no automatic rewrite on whole path match/); + + test.done(); + }, + }, + + 'with query paramater match': { + 'should match based on query parameter'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + queryParameters: [ + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + ], + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + QueryParameters: [ + { + Name: 'query-field', + Match: { + Exact: 'value', + }, + }, + ], + }, + }, + }, + })); + + test.done(); + }, + }, + }, + + 'with empty HTTP/HTTP2match': { + 'should match based on prefix'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Prefix: '/', + }, + }, + }, + })); + + test.done(); + }, }, 'Can import Gateway Routes using an ARN'(test: Test) { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index 3e268fdbc8bd2..f6e2a86d6d651 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -237,7 +237,7 @@ export = { }, ], match: { - prefixPath: '/node', + path: appmesh.HttpRoutePathMatch.startsWith('/node'), }, timeout: { idle: cdk.Duration.seconds(10), @@ -635,7 +635,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), headers: [ appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), @@ -749,7 +749,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), method: appmesh.HttpRouteMethod.GET, }, }), @@ -791,7 +791,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), protocol: appmesh.HttpRouteProtocol.HTTP, }, }), @@ -812,6 +812,281 @@ export = { test.done(); }, + 'should match routes based on metadata'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + new appmesh.Route(stack, 'test-grpc-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode }], + match: { + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + GrpcRoute: { + Match: { + Metadata: [ + { + Invert: false, + Match: { Exact: 'application/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Exact: 'text/html' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Prefix: 'application/' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Prefix: 'text/' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Suffix: '/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Suffix: '/json+foobar' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Regex: 'application/.*' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Regex: 'text/.*' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + { + Invert: true, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + ], + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based on path'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.http({ + weightedTargets: [{ virtualNode }], + match: { + path: appmesh.HttpRoutePathMatch.exactly('/exact'), + }, + }), + }); + + new appmesh.Route(stack, 'test-http2-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + path: appmesh.HttpRoutePathMatch.regex('regex'), + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + HttpRoute: { + Match: { + Path: { + Exact: '/exact', + }, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Http2Route: { + Match: { + Path: { + Regex: 'regex', + }, + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based query parameter'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.http({ + weightedTargets: [{ virtualNode }], + match: { + queryParameters: [appmesh.QueryParameterMatch.valueIs('query-field', 'value')], + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + HttpRoute: { + Match: { + QueryParameters: [ + { + Name: 'query-field', + Match: { + Exact: 'value', + }, + }, + ], + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based method name'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + serviceName: 'test', + methodName: 'testMethod', + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + GrpcRoute: { + Match: { + ServiceName: 'test', + MethodName: 'testMethod', + }, + }, + }, + })); + + test.done(); + }, + 'should throw an error with invalid number of headers'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -834,7 +1109,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), // Empty header headers: [], }, @@ -847,7 +1122,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), // 11 headers headers: [ appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), @@ -870,6 +1145,191 @@ export = { test.done(); }, + 'should throw an error with invalid number of query parameters'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + path: appmesh.HttpRoutePathMatch.startsWith('/'), + // Empty header + queryParameters: [], + }, + }), + }); + }, /Number of query parameters provided for matching must be between 1 and 10, got: 0/); + + test.throws(() => { + router.addRoute('route2', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + path: appmesh.HttpRoutePathMatch.startsWith('/'), + // 11 headers + queryParameters: [ + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + ], + }, + }), + }); + }, /Number of query parameters provided for matching must be between 1 and 10, got: 11/); + + test.done(); + }, + + 'should throw an error with invalid number of metadata'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + metadata: [], + }, + }), + }); + }, /Number of metadata provided for matching must be between 1 and 10, got: 0/); + + // WHEN + THEN + test.throws(() => { + new appmesh.Route(stack, 'test-http-route-1', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + }); + }, /Number of metadata provided for matching must be between 1 and 10, got: 11/); + + test.done(); + }, + + 'should throw an error if no gRPC match type is defined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: {}, + }), + }); + }, /At least one gRPC route match property must be provided/); + + test.done(); + }, + + 'should throw an error if method name is specified without service name'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + methodName: 'test_method', + }, + }), + }); + }, /If you specify a method name, you must also specify a service name/); + + test.done(); + }, + 'should allow route priority'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts index 4999b4425a1c7..3f7070121db4e 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts @@ -1,7 +1,6 @@ import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; - import * as appmesh from '../lib'; export = { @@ -149,7 +148,7 @@ export = { }, ], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), }, }), }); @@ -236,7 +235,7 @@ export = { }, ], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), }, }), }); @@ -250,7 +249,7 @@ export = { }, ], match: { - prefixPath: '/path2', + path: appmesh.HttpRoutePathMatch.startsWith('/path2'), }, }), }); @@ -264,7 +263,7 @@ export = { }, ], match: { - prefixPath: '/path3', + path: appmesh.HttpRoutePathMatch.startsWith('/path3'), }, }), }); diff --git a/packages/@aws-cdk/aws-apprunner/package.json b/packages/@aws-cdk/aws-apprunner/package.json index 9585624e0d57c..20db35c7a3264 100644 --- a/packages/@aws-cdk/aws-apprunner/package.json +++ b/packages/@aws-cdk/aws-apprunner/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-appstream/package.json b/packages/@aws-cdk/aws-appstream/package.json index 6e46c47abd07a..2bdd6def0c6f8 100644 --- a/packages/@aws-cdk/aws-appstream/package.json +++ b/packages/@aws-cdk/aws-appstream/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index ba8b8145f799e..13267c04844cc 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-athena/package.json b/packages/@aws-cdk/aws-athena/package.json index 7b536cc646622..0170da3267a71 100644 --- a/packages/@aws-cdk/aws-athena/package.json +++ b/packages/@aws-cdk/aws-athena/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit-shim": "0.0.0", diff --git a/packages/@aws-cdk/aws-auditmanager/package.json b/packages/@aws-cdk/aws-auditmanager/package.json index bff117d2e2073..e1c4d99497507 100644 --- a/packages/@aws-cdk/aws-auditmanager/package.json +++ b/packages/@aws-cdk/aws-auditmanager/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index 3d5837beabb85..59f8594332346 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^2.17.0", diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json index 42c9c30f26923..08a1b4120aee1 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index a14de1d88510d..c2ece82fdb8b0 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscalingplans/package.json b/packages/@aws-cdk/aws-autoscalingplans/package.json index bba4866c26bc3..e665150f6b980 100644 --- a/packages/@aws-cdk/aws-autoscalingplans/package.json +++ b/packages/@aws-cdk/aws-autoscalingplans/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-backup/package.json b/packages/@aws-cdk/aws-backup/package.json index 5f16b236e39e4..3c906382aa9b2 100644 --- a/packages/@aws-cdk/aws-backup/package.json +++ b/packages/@aws-cdk/aws-backup/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-batch/package.json b/packages/@aws-cdk/aws-batch/package.json index 0581557a70e1b..158edc9ff76ff 100644 --- a/packages/@aws-cdk/aws-batch/package.json +++ b/packages/@aws-cdk/aws-batch/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-budgets/package.json b/packages/@aws-cdk/aws-budgets/package.json index 7eec1b16415a8..b3d03d5d5d194 100644 --- a/packages/@aws-cdk/aws-budgets/package.json +++ b/packages/@aws-cdk/aws-budgets/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-cassandra/package.json b/packages/@aws-cdk/aws-cassandra/package.json index 825c114741063..94d5a49670f90 100644 --- a/packages/@aws-cdk/aws-cassandra/package.json +++ b/packages/@aws-cdk/aws-cassandra/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-ce/package.json b/packages/@aws-cdk/aws-ce/package.json index f4eda68f8d287..1e0345dc6fb9c 100644 --- a/packages/@aws-cdk/aws-ce/package.json +++ b/packages/@aws-cdk/aws-ce/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json index b91db6bcc680f..e3ab68c8a7dd6 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json @@ -29,12 +29,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", + "@types/aws-lambda": "^8.10.78", "@types/sinon": "^9.0.11", "cdk-build-tools": "0.0.0", "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.2.1", - "eslint": "^7.29.0", + "eslint": "^7.30.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", @@ -43,7 +43,7 @@ "jest": "^26.6.3", "lambda-tester": "^3.6.0", "sinon": "^9.2.4", - "nock": "^13.1.0", + "nock": "^13.1.1", "ts-jest": "^26.5.6" } } diff --git a/packages/@aws-cdk/aws-certificatemanager/package.json b/packages/@aws-cdk/aws-certificatemanager/package.json index c903bb46c9181..f84f775f108de 100644 --- a/packages/@aws-cdk/aws-certificatemanager/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-chatbot/package.json b/packages/@aws-cdk/aws-chatbot/package.json index e7db76bc38cf2..9fcdfedfcda21 100644 --- a/packages/@aws-cdk/aws-chatbot/package.json +++ b/packages/@aws-cdk/aws-chatbot/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index 077748fc3b08b..8aac346d87398 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-codecommit": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index cdd4583356787..f0d492aace535 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -74,8 +74,8 @@ "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", - "@types/aws-lambda": "^8.10.77", - "@types/nodeunit": "^0.0.31", + "@types/aws-lambda": "^8.10.78", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudfront-origins/package.json b/packages/@aws-cdk/aws-cloudfront-origins/package.json index 4a7e3e697fb06..e7ef1eb01a94d 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/package.json +++ b/packages/@aws-cdk/aws-cloudfront-origins/package.json @@ -71,7 +71,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 676bf3bfe68d9..cd8e8f17f9b5d 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 2ab7e02127c41..526442259cddb 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/package.json b/packages/@aws-cdk/aws-cloudwatch-actions/package.json index 3afd447c24a5d..0869c60941c8f 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/package.json +++ b/packages/@aws-cdk/aws-cloudwatch-actions/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index 85ccf7516fe87..c57fc64d6f8a0 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codeartifact/package.json b/packages/@aws-cdk/aws-codeartifact/package.json index 4fe7c94427035..b1d8e76ef79d4 100644 --- a/packages/@aws-cdk/aws-codeartifact/package.json +++ b/packages/@aws-cdk/aws-codeartifact/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index db40c989dfd01..e7591a3ab4fbb 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -78,7 +78,7 @@ "devDependencies": { "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index cefdffbc2655c..6d622f85d076a 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-sns": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 89546a6116ae1..89f9706dd9f3e 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codeguruprofiler/package.json b/packages/@aws-cdk/aws-codeguruprofiler/package.json index 7249bcb919a38..1c72b538d5056 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/package.json +++ b/packages/@aws-cdk/aws-codeguruprofiler/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codegurureviewer/package.json b/packages/@aws-cdk/aws-codegurureviewer/package.json index 5b976bab70db6..f7a900d2aef8b 100644 --- a/packages/@aws-cdk/aws-codegurureviewer/package.json +++ b/packages/@aws-cdk/aws-codegurureviewer/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 9cd001d3d3bba..fafc221e72351 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -68,11 +68,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-cloudtrail": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "@types/lodash": "^4.14.170", + "@types/lodash": "^4.14.171", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "lodash": "^4.17.21", diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index c1e4df100aa23..65b7e84abbf61 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -479,7 +479,7 @@ export class Pipeline extends PipelineBase { } /** @internal */ - public _attachActionToPipeline(stage: Stage, action: IAction, actionScope: CoreConstruct): FullActionDescriptor { + public _attachActionToPipeline(stage: Stage, action: IAction, actionScope: Construct): FullActionDescriptor { const richAction = new RichAction(action, this); // handle cross-region actions here @@ -491,8 +491,8 @@ export class Pipeline extends PipelineBase { // // CodePipeline Variables validateNamespaceName(richAction.actionProperties.variablesNamespace); - // bind the Action - const actionConfig = richAction.bind(actionScope, stage, { + // bind the Action (type h4x) + const actionConfig = richAction.bind(actionScope as CoreConstruct, stage, { role: actionRole ? actionRole : this.role, bucket: crossRegionInfo.artifactBucket, }); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts index e9ed5a6995f02..b5f5aa86dc1c4 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts @@ -1,5 +1,6 @@ import * as events from '@aws-cdk/aws-events'; import * as cdk from '@aws-cdk/core'; +import { Construct, Node } from 'constructs'; import { IAction, IPipeline, IStage } from '../action'; import { Artifact } from '../artifact'; import { CfnPipeline } from '../codepipeline.generated'; @@ -137,7 +138,15 @@ export class Stage implements IStage { private attachActionToPipeline(action: IAction): FullActionDescriptor { // notify the Pipeline of the new Action - const actionScope = new cdk.Construct(this.scope, action.actionProperties.actionName); + // + // It may be that a construct already exists with the given action name (CDK Pipelines + // may do this to maintain construct tree compatibility between versions). + // + // If so, we simply reuse it. + let actionScope = Node.of(this.scope).tryFindChild(action.actionProperties.actionName) as Construct | undefined; + if (!actionScope) { + actionScope = new cdk.Construct(this.scope, action.actionProperties.actionName); + } return this._pipeline._attachActionToPipeline(this, action, actionScope); } diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 82c75be1eea46..95b30ad58ae03 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -78,9 +78,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit-shim": "0.0.0", diff --git a/packages/@aws-cdk/aws-codestar/package.json b/packages/@aws-cdk/aws-codestar/package.json index 69ebd5ad2897a..8b943f821668d 100644 --- a/packages/@aws-cdk/aws-codestar/package.json +++ b/packages/@aws-cdk/aws-codestar/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codestarconnections/package.json b/packages/@aws-cdk/aws-codestarconnections/package.json index ab3ea1d507011..d45abecfe6ccd 100644 --- a/packages/@aws-cdk/aws-codestarconnections/package.json +++ b/packages/@aws-cdk/aws-codestarconnections/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-codestarnotifications/package.json b/packages/@aws-cdk/aws-codestarnotifications/package.json index 8e96eef73d458..81460dd341e68 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/package.json +++ b/packages/@aws-cdk/aws-codestarnotifications/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index bf68fec018127..56cd465f17680 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/punycode": "^2.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index 92f17b421cfd9..c03852d4321e1 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -73,7 +73,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-events-targets": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-connect/package.json b/packages/@aws-cdk/aws-connect/package.json index c63ca8004bdee..02e16855e466b 100644 --- a/packages/@aws-cdk/aws-connect/package.json +++ b/packages/@aws-cdk/aws-connect/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.22", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cur/package.json b/packages/@aws-cdk/aws-cur/package.json index 31b36c3bf5612..ec2fb98a0d60c 100644 --- a/packages/@aws-cdk/aws-cur/package.json +++ b/packages/@aws-cdk/aws-cur/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-customerprofiles/package.json b/packages/@aws-cdk/aws-customerprofiles/package.json index 9a6e23405a069..387a792b7a373 100644 --- a/packages/@aws-cdk/aws-customerprofiles/package.json +++ b/packages/@aws-cdk/aws-customerprofiles/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert-internal": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/aws-databrew/package.json b/packages/@aws-cdk/aws-databrew/package.json index 12d47c939b79f..9f36870538f10 100644 --- a/packages/@aws-cdk/aws-databrew/package.json +++ b/packages/@aws-cdk/aws-databrew/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-datapipeline/package.json b/packages/@aws-cdk/aws-datapipeline/package.json index b60950c929ac4..f368c68baa360 100644 --- a/packages/@aws-cdk/aws-datapipeline/package.json +++ b/packages/@aws-cdk/aws-datapipeline/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-datasync/package.json b/packages/@aws-cdk/aws-datasync/package.json index b1212ec1789e6..d63f373cfeef8 100644 --- a/packages/@aws-cdk/aws-datasync/package.json +++ b/packages/@aws-cdk/aws-datasync/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-dax/package.json b/packages/@aws-cdk/aws-dax/package.json index c0023d1cf329b..bd4d0c94dc3d3 100644 --- a/packages/@aws-cdk/aws-dax/package.json +++ b/packages/@aws-cdk/aws-dax/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-detective/package.json b/packages/@aws-cdk/aws-detective/package.json index 110d3dd70dc26..b5bd1c2a903df 100644 --- a/packages/@aws-cdk/aws-detective/package.json +++ b/packages/@aws-cdk/aws-detective/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-devopsguru/package.json b/packages/@aws-cdk/aws-devopsguru/package.json index 329cf5f069bd5..6fc91c263db06 100644 --- a/packages/@aws-cdk/aws-devopsguru/package.json +++ b/packages/@aws-cdk/aws-devopsguru/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-directoryservice/package.json b/packages/@aws-cdk/aws-directoryservice/package.json index 5c58ea6187881..179e72a6a4a2a 100644 --- a/packages/@aws-cdk/aws-directoryservice/package.json +++ b/packages/@aws-cdk/aws-directoryservice/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-dlm/package.json b/packages/@aws-cdk/aws-dlm/package.json index 4db4be0d3e694..09d10c116da40 100644 --- a/packages/@aws-cdk/aws-dlm/package.json +++ b/packages/@aws-cdk/aws-dlm/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-dms/package.json b/packages/@aws-cdk/aws-dms/package.json index 41b7a99b024b8..7e06f8859e421 100644 --- a/packages/@aws-cdk/aws-dms/package.json +++ b/packages/@aws-cdk/aws-dms/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-docdb/package.json b/packages/@aws-cdk/aws-docdb/package.json index 401e97ae696b3..472f2a229215c 100644 --- a/packages/@aws-cdk/aws-docdb/package.json +++ b/packages/@aws-cdk/aws-docdb/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json index 907e490b7625b..5df277b6064ca 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json @@ -31,7 +31,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.2.1", - "eslint": "^7.29.0", + "eslint": "^7.30.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", @@ -39,6 +39,6 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.1.0" + "nock": "^13.1.1" } } diff --git a/packages/@aws-cdk/aws-dynamodb-global/package.json b/packages/@aws-cdk/aws-dynamodb-global/package.json index 7f095891c0375..77e92e5d5aa17 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/package.json @@ -56,7 +56,7 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 0523bd4157aa9..a9ffc922c528e 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 83ce458230cb0..52dede95bb318 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts index 48af505e1148e..bb7c40617b4ea 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts @@ -60,7 +60,7 @@ export class TarballImageAsset extends CoreConstruct implements IAsset { throw new Error(`Cannot find file at ${props.tarballFile}`); } - const stagedTarball = new AssetStaging(scope, 'Staging', { sourcePath: props.tarballFile }); + const stagedTarball = new AssetStaging(this, 'Staging', { sourcePath: props.tarballFile }); this.sourceHash = stagedTarball.assetHash; this.assetHash = stagedTarball.assetHash; diff --git a/packages/@aws-cdk/aws-ecr-assets/package.json b/packages/@aws-cdk/aws-ecr-assets/package.json index 828f489e6d7b0..edf101cdf28b2 100644 --- a/packages/@aws-cdk/aws-ecr-assets/package.json +++ b/packages/@aws-cdk/aws-ecr-assets/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/proxyquire": "^1.3.28", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts index c4654fed87044..20d7517e23915 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts @@ -16,7 +16,7 @@ describe('image asset', () => { testFutureBehavior('test instantiating Asset Image', flags, App, (app) => { // GIVEN const stack = new Stack(app); - const assset = new TarballImageAsset(stack, 'Image', { + const asset = new TarballImageAsset(stack, 'Image', { tarballFile: __dirname + '/demo-tarball/empty.tar', }); @@ -30,23 +30,26 @@ describe('image asset', () => { expect(Object.keys(manifest.files ?? {}).length).toBe(1); expect(Object.keys(manifest.dockerImages ?? {}).length).toBe(1); - expect(manifest.dockerImages?.[assset.assetHash]?.destinations?.['current_account-current_region']).toStrictEqual( + expect(manifest.dockerImages?.[asset.assetHash]?.destinations?.['current_account-current_region']).toStrictEqual( { assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}', - imageTag: assset.assetHash, + imageTag: asset.assetHash, repositoryName: 'cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}', }, ); - expect(manifest.dockerImages?.[assset.assetHash]?.source).toStrictEqual( + expect(manifest.dockerImages?.[asset.assetHash]?.source).toStrictEqual( { executable: [ 'sh', '-c', - `docker load -i asset.${assset.assetHash}.tar | sed "s/Loaded image: //g"`, + `docker load -i asset.${asset.assetHash}.tar | sed "s/Loaded image: //g"`, ], }, ); + + // AssetStaging in TarballImageAsset uses `this` as scope' + expect(asset.node.tryFindChild('Staging')).toBeDefined(); }); testFutureBehavior('asset.repository.grantPull can be used to grant a principal permissions to use the image', flags, App, (app) => { diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index 5134e382edd90..d2eecfc93b1ba 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert-internal": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index 259a800e92d8d..2bed9fe6b9cc8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 7edb8e0584370..9e8170f86d826 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -60,6 +60,7 @@ one to run tasks on AWS Fargate. - Use the `Ec2TaskDefinition` and `Ec2Service` constructs to run tasks on Amazon EC2 instances running in your account. - Use the `FargateTaskDefinition` and `FargateService` constructs to run tasks on instances that are managed for you by AWS. +- Use the `ExternalTaskDefinition` and `ExternalService` constructs to run AWS ECS Anywhere tasks on self-managed infrastructure. Here are the main differences: @@ -73,10 +74,12 @@ Here are the main differences: Application/Network Load Balancers. Only the AWS log driver is supported. Many host features are not supported such as adding kernel capabilities and mounting host devices/volumes inside the container. +- **AWS ECSAnywhere**: tasks are run and managed by AWS ECS Anywhere on infrastructure owned by the customer. Only Bridge networking mode is supported. Does not support autoscaling, load balancing, cloudmap or attachment of volumes. -For more information on Amazon EC2 vs AWS Fargate and networking see the AWS Documentation: -[AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) and -[Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html). +For more information on Amazon EC2 vs AWS Fargate, networking and ECS Anywhere see the AWS Documentation: +[AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html), +[Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html), +[ECS Anywhere](https://aws.amazon.com/ecs/anywhere/) ## Clusters @@ -211,8 +214,8 @@ some supporting containers which are used to support the main container, doings things like upload logs or metrics to monitoring services. To run a task or service with Amazon EC2 launch type, use the `Ec2TaskDefinition`. For AWS Fargate tasks/services, use the -`FargateTaskDefinition`. These classes provide a simplified API that only contain -properties relevant for that specific launch type. +`FargateTaskDefinition`. For AWS ECS Anywhere use the `ExternalTaskDefinition`. These classes +provide simplified APIs that only contain properties relevant for each specific launch type. For a `FargateTaskDefinition`, specify the task size (`memoryLimitMiB` and `cpu`): @@ -248,6 +251,19 @@ const container = ec2TaskDefinition.addContainer("WebContainer", { }); ``` +For an `ExternalTaskDefinition`: + +```ts +const externalTaskDefinition = new ecs.ExternalTaskDefinition(this, 'TaskDef'); + +const container = externalTaskDefinition.addContainer("WebContainer", { + // Use an image from DockerHub + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024 + // ... other options here ... +}); +``` + You can specify container properties when you add them to the task definition, or with various methods, e.g.: To add a port mapping when adding a container to the task definition, specify the `portMappings` option: @@ -283,6 +299,8 @@ const volume = { const container = fargateTaskDefinition.addVolume("mydatavolume"); ``` +> Note: ECS Anywhere doesn't support volume attachments in the task definition. + To use a TaskDefinition that can be used with either Amazon EC2 or AWS Fargate launch types, use the `TaskDefinition` construct. @@ -360,6 +378,18 @@ const service = new ecs.FargateService(this, 'Service', { }); ``` +ECS Anywhere service definition looks like: + +```ts +const taskDefinition; + +const service = new ecs.ExternalService(this, 'Service', { + cluster, + taskDefinition, + desiredCount: 5 +}); +``` + `Services` by default will create a security group if not provided. If you'd like to specify which security groups to use you can override the `securityGroups` property. @@ -378,6 +408,8 @@ const service = new ecs.FargateService(stack, 'Service', { }); ``` +> Note: ECS Anywhere doesn't support deployment circuit breakers and rollback. + ### Include an application/network load balancer `Services` are load balancing targets and can be added to a target group, which will be attached to an application/network load balancers: @@ -402,6 +434,8 @@ const targetGroup2 = listener.addTargets('ECS2', { }); ``` +> Note: ECS Anywhere doesn't support application/network load balancers. + Note that in the example above, the default `service` only allows you to register the first essential container or the first mapped port on the container as a target and add it to a new target group. To have more control over which container and port to register as targets, you can use `service.loadBalancerTarget()` to return a load balancing target for a specific container and port. Alternatively, you can also create all load balancer targets to be registered in this service, add them to target groups, and attach target groups to listeners accordingly. diff --git a/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts b/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts new file mode 100644 index 0000000000000..741ba0a7b2b29 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts @@ -0,0 +1,190 @@ +import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cloudmap from '@aws-cdk/aws-servicediscovery'; +import { Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { AssociateCloudMapServiceOptions, BaseService, BaseServiceOptions, CloudMapOptions, DeploymentControllerType, EcsTarget, IBaseService, IEcsLoadBalancerTarget, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; +import { fromServiceAtrributes } from '../base/from-service-attributes'; +import { ScalableTaskCount } from '../base/scalable-task-count'; +import { Compatibility, LoadBalancerTargetOptions, TaskDefinition } from '../base/task-definition'; +import { ICluster } from '../cluster'; +/** + * The properties for defining a service using the External launch type. + */ +export interface ExternalServiceProps extends BaseServiceOptions { + /** + * The task definition to use for tasks in the service. + * + * [disable-awslint:ref-via-interface] + */ + readonly taskDefinition: TaskDefinition; + + /** + * The security groups to associate with the service. If you do not specify a security group, the default security group for the VPC is used. + * + * + * @default - A new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; +} + +/** + * The interface for a service using the External launch type on an ECS cluster. + */ +export interface IExternalService extends IService { + +} + +/** + * The properties to import from the service using the External launch type. + */ +export interface ExternalServiceAttributes { + /** + * The cluster that hosts the service. + */ + readonly cluster: ICluster; + + /** + * The service ARN. + * + * @default - either this, or {@link serviceName}, is required + */ + readonly serviceArn?: string; + + /** + * The name of the service. + * + * @default - either this, or {@link serviceArn}, is required + */ + readonly serviceName?: string; +} + +/** + * This creates a service using the External launch type on an ECS cluster. + * + * @resource AWS::ECS::Service + */ +export class ExternalService extends BaseService implements IExternalService { + + /** + * Imports from the specified service ARN. + */ + public static fromExternalServiceArn(scope: Construct, id: string, externalServiceArn: string): IExternalService { + class Import extends Resource implements IExternalService { + public readonly serviceArn = externalServiceArn; + public readonly serviceName = Stack.of(scope).parseArn(externalServiceArn).resourceName as string; + } + return new Import(scope, id); + } + + /** + * Imports from the specified service attrributes. + */ + public static fromExternalServiceAttributes(scope: Construct, id: string, attrs: ExternalServiceAttributes): IBaseService { + return fromServiceAtrributes(scope, id, attrs); + } + + /** + * Constructs a new instance of the ExternalService class. + */ + constructor(scope: Construct, id: string, props: ExternalServiceProps) { + if (props.minHealthyPercent !== undefined && props.maxHealthyPercent !== undefined && props.minHealthyPercent >= props.maxHealthyPercent) { + throw new Error('Minimum healthy percent must be less than maximum healthy percent.'); + } + + if (props.taskDefinition.compatibility !== Compatibility.EXTERNAL) { + throw new Error('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); + } + + if (props.cluster.defaultCloudMapNamespace !== undefined) { + throw new Error (`Cloud map integration is not supported for External service ${props.cluster.defaultCloudMapNamespace}`); + } + + if (props.cloudMapOptions !== undefined) { + throw new Error ('Cloud map options are not supported for External service'); + } + + if (props.enableExecuteCommand !== undefined) { + throw new Error ('Enable Execute Command options are not supported for External service'); + } + + if (props.capacityProviderStrategies !== undefined) { + throw new Error ('Capacity Providers are not supported for External service'); + } + + const propagateTagsFromSource = props.propagateTags ?? PropagatedTagSource.NONE; + + super(scope, id, { + ...props, + desiredCount: props.desiredCount, + maxHealthyPercent: props.maxHealthyPercent === undefined ? 100 : props.maxHealthyPercent, + minHealthyPercent: props.minHealthyPercent === undefined ? 0 : props.minHealthyPercent, + launchType: LaunchType.EXTERNAL, + propagateTags: propagateTagsFromSource, + enableECSManagedTags: props.enableECSManagedTags, + }, + { + cluster: props.cluster.clusterName, + taskDefinition: props.deploymentController?.type === DeploymentControllerType.EXTERNAL ? undefined : props.taskDefinition.taskDefinitionArn, + }, props.taskDefinition); + + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); + + this.node.addValidation({ + validate: () => this.networkConfiguration !== undefined ? ['Network configurations not supported for an external service'] : [], + }); + } + + /** + * Overriden method to throw error as `attachToApplicationTargetGroup` is not supported for external service + */ + public attachToApplicationTargetGroup(_targetGroup: elbv2.IApplicationTargetGroup): elbv2.LoadBalancerTargetProps { + throw new Error ('Application load balancer cannot be attached to an external service'); + } + + /** + * Overriden method to throw error as `loadBalancerTarget` is not supported for external service + */ + public loadBalancerTarget(_options: LoadBalancerTargetOptions): IEcsLoadBalancerTarget { + throw new Error ('External service cannot be attached as load balancer targets'); + } + + /** + * Overriden method to throw error as `registerLoadBalancerTargets` is not supported for external service + */ + public registerLoadBalancerTargets(..._targets: EcsTarget[]) { + throw new Error ('External service cannot be registered as load balancer targets'); + } + + /** + * Overriden method to throw error as `configureAwsVpcNetworkingWithSecurityGroups` is not supported for external service + */ + // eslint-disable-next-line max-len, no-unused-vars + protected configureAwsVpcNetworkingWithSecurityGroups(_vpc: ec2.IVpc, _assignPublicIp?: boolean, _vpcSubnets?: ec2.SubnetSelection, _securityGroups?: ec2.ISecurityGroup[]) { + throw new Error ('Only Bridge network mode is supported for external service'); + } + + /** + * Overriden method to throw error as `autoScaleTaskCount` is not supported for external service + */ + public autoScaleTaskCount(_props: appscaling.EnableScalingProps): ScalableTaskCount { + throw new Error ('Autoscaling not supported for external service'); + } + + /** + * Overriden method to throw error as `enableCloudMap` is not supported for external service + */ + public enableCloudMap(_options: CloudMapOptions): cloudmap.Service { + throw new Error ('Cloud map integration not supported for an external service'); + } + + /** + * Overriden method to throw error as `associateCloudMapService` is not supported for external service + */ + public associateCloudMapService(_options: AssociateCloudMapServiceOptions): void { + throw new Error ('Cloud map service association is not supported for an external service'); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts new file mode 100644 index 0000000000000..de9fa8b87e9dc --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts @@ -0,0 +1,91 @@ +import { Construct } from 'constructs'; +import { ImportedTaskDefinition } from '../../lib/base/_imported-task-definition'; +import { + CommonTaskDefinitionAttributes, + CommonTaskDefinitionProps, + Compatibility, + InferenceAccelerator, + ITaskDefinition, + NetworkMode, + TaskDefinition, + Volume, +} from '../base/task-definition'; + +/** + * The properties for a task definition run on an External cluster. + */ +export interface ExternalTaskDefinitionProps extends CommonTaskDefinitionProps { + +} + +/** + * The interface of a task definition run on an External cluster. + */ +export interface IExternalTaskDefinition extends ITaskDefinition { + +} + +/** + * Attributes used to import an existing External task definition + */ +export interface ExternalTaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + +} + +/** + * The details of a task definition run on an External cluster. + * + * @resource AWS::ECS::TaskDefinition + */ +export class ExternalTaskDefinition extends TaskDefinition implements IExternalTaskDefinition { + + /** + * Imports a task definition from the specified task definition ARN. + */ + public static fromEc2TaskDefinitionArn(scope: Construct, id: string, externalTaskDefinitionArn: string): IExternalTaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: externalTaskDefinitionArn, + }); + } + + /** + * Imports an existing External task definition from its attributes + */ + public static fromExternalTaskDefinitionAttributes( + scope: Construct, + id: string, + attrs: ExternalTaskDefinitionAttributes, + ): IExternalTaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: Compatibility.EXTERNAL, + networkMode: NetworkMode.BRIDGE, + taskRole: attrs.taskRole, + }); + } + + /** + * Constructs a new instance of the ExternalTaskDefinition class. + */ + constructor(scope: Construct, id: string, props: ExternalTaskDefinitionProps = {}) { + super(scope, id, { + ...props, + compatibility: Compatibility.EXTERNAL, + networkMode: NetworkMode.BRIDGE, + }); + } + + /** + * Overridden method to throw error, as volumes are not supported for external task definitions + */ + public addVolume(_volume: Volume) { + throw new Error('External task definitions doesnt support volumes'); + } + + /** + * Overriden method to throw error as interface accelerators are not supported for external tasks + */ + public addInferenceAccelerator(_inferenceAccelerator: InferenceAccelerator) { + throw new Error('Cannot use inference accelerators on tasks that run on External service'); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 7b16d7be07827..0c1cee2a56ff9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -15,6 +15,9 @@ export * from './ec2/ec2-task-definition'; export * from './fargate/fargate-service'; export * from './fargate/fargate-task-definition'; +export * from './external/external-service'; +export * from './external/external-task-definition'; + export * from './linux-parameters'; export * from './images/asset-image'; diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index dad900b0c039b..1348aef9b9d62 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -73,10 +73,10 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-s3-deployment": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "@types/proxyquire": "^1.3.28", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -151,6 +151,7 @@ "props-physical-name:@aws-cdk/aws-ecs.TaskDefinitionProps", "props-physical-name:@aws-cdk/aws-ecs.Ec2TaskDefinitionProps", "props-physical-name:@aws-cdk/aws-ecs.FargateTaskDefinitionProps", + "props-physical-name:@aws-cdk/aws-ecs.ExternalTaskDefinitionProps", "docs-public-apis:@aws-cdk/aws-ecs.GelfCompressionType.GZIP", "docs-public-apis:@aws-cdk/aws-ecs.WindowsOptimizedVersion.SERVER_2016", "docs-public-apis:@aws-cdk/aws-ecs.WindowsOptimizedVersion.SERVER_2019", diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts new file mode 100644 index 0000000000000..d01eaa14f11b9 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts @@ -0,0 +1,528 @@ +import '@aws-cdk/assert-internal/jest'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cloudmap from '@aws-cdk/aws-servicediscovery'; +import * as cdk from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as ecs from '../../lib'; +import { LaunchType } from '../../lib/base/base-service'; + +nodeunitShim({ + 'When creating an External Service': { + 'with only required properties set, it correctly sets default properties'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DeploymentConfiguration: { + MaximumPercent: 100, + MinimumHealthyPercent: 0, + }, + EnableECSManagedTags: false, + LaunchType: LaunchType.EXTERNAL, + }); + + test.notEqual(service.node.defaultChild, undefined); + + test.done(); + }, + }, + + 'with all properties set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // WHEN + new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + healthCheckGracePeriod: cdk.Duration.seconds(60), + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bob', + vpc, + })], + serviceName: 'bonjour', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 55, + }, + DesiredCount: 2, + LaunchType: LaunchType.EXTERNAL, + ServiceName: 'bonjour', + }); + + test.done(); + }, + + 'with cloudmap set on cluster, throw error'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + healthCheckGracePeriod: cdk.Duration.seconds(60), + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bob', + vpc, + })], + serviceName: 'bonjour', + })).toThrow('Cloud map integration is not supported for External service' ); + + test.done(); + }, + + 'with multiple security groups, it correctly updates the cfn template'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + const securityGroup1 = new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bingo', + vpc, + }); + const securityGroup2 = new ec2.SecurityGroup(stack, 'SecurityGroup2', { + allowAllOutbound: false, + description: 'Example', + securityGroupName: 'Rolly', + vpc, + }); + + // WHEN + new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + securityGroups: [securityGroup1, securityGroup2], + serviceName: 'bonjour', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DesiredCount: 2, + LaunchType: LaunchType.EXTERNAL, + ServiceName: 'bonjour', + }); + + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Bingo', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + }); + + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Rolly', + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + FromPort: 252, + IpProtocol: 'icmp', + ToPort: 86, + }, + ], + }); + + test.done(); + }, + + 'throws when task definition is not External compatible'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.TaskDefinition(stack, 'FargateTaskDef', { + compatibility: ecs.Compatibility.FARGATE, + cpu: '256', + memoryMiB: '512', + }); + taskDefinition.addContainer('BaseContainer', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryReservationMiB: 10, + }); + + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + })).toThrow('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); + + test.done(); + }, + + 'errors if minimum not less than maximum'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('BaseContainer', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryReservationMiB: 10, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + minHealthyPercent: 100, + maxHealthyPercent: 100, + })).toThrow('Minimum healthy percent must be less than maximum healthy percent.'); + + test.done(); + }, + + 'error if cloudmap options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + cloudMapOptions: { + name: 'myApp', + }, + })).toThrow('Cloud map options are not supported for External service'); + + // THEN + test.done(); + }, + + 'error if enableExecuteCommand options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + enableExecuteCommand: true, + })).toThrow('Enable Execute Command options are not supported for External service'); + + // THEN + test.done(); + }, + + 'error if capacityProviderStrategies options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // WHEN + const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: new ec2.InstanceType('bogus'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), + }); + + const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + enableManagedTerminationProtection: false, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + capacityProviderStrategies: [{ + capacityProvider: capacityProvider.capacityProviderName, + }], + })).toThrow('Capacity Providers are not supported for External service'); + + // THEN + test.done(); + }, + + 'error when performing attachToApplicationTargetGroup to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + const targetGroup = listener.addTargets('target', { + port: 80, + }); + + // THEN + expect(() => service.attachToApplicationTargetGroup(targetGroup)).toThrow('Application load balancer cannot be attached to an external service'); + + // THEN + test.done(); + }, + + 'error when performing loadBalancerTarget to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.loadBalancerTarget({ + containerName: 'MainContainer', + })).toThrow('External service cannot be attached as load balancer targets'); + + // THEN + test.done(); + }, + + 'error when performing registerLoadBalancerTargets to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.applicationListener(listener), + newTargetGroupId: 'target1', + }, + )).toThrow('External service cannot be registered as load balancer targets'); + + // THEN + test.done(); + }, + + 'error when performing autoScaleTaskCount to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.autoScaleTaskCount({ + maxCapacity: 2, + minCapacity: 1, + })).toThrow('Autoscaling not supported for external service'); + + // THEN + test.done(); + }, + + 'error when performing enableCloudMap to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.enableCloudMap({})).toThrow('Cloud map integration not supported for an external service'); + + // THEN + test.done(); + }, + + 'error when performing associateCloudMapService to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + const cloudMapNamespace = new cloudmap.PrivateDnsNamespace(stack, 'TestCloudMapNamespace', { + name: 'scorekeep.com', + vpc, + }); + + const cloudMapService = new cloudmap.Service(stack, 'Service', { + name: 'service-name', + namespace: cloudMapNamespace, + dnsRecordType: cloudmap.DnsRecordType.SRV, + }); + + // THEN + expect(() => service.associateCloudMapService({ + service: cloudMapService, + container: container, + containerPort: 8000, + })).toThrow('Cloud map service association is not supported for an external service'); + + // THEN + test.done(); + }, +}); diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts new file mode 100644 index 0000000000000..c5fd8f942f1f0 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts @@ -0,0 +1,638 @@ +import '@aws-cdk/assert-internal/jest'; +import * as path from 'path'; +import { Protocol } from '@aws-cdk/aws-ec2'; +import { Repository } from '@aws-cdk/aws-ecr'; +import * as iam from '@aws-cdk/aws-iam'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as ssm from '@aws-cdk/aws-ssm'; +import * as cdk from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as ecs from '../../lib'; + +nodeunitShim({ + 'When creating an External TaskDefinition': { + 'with only required properties set, it correctly sets default properties'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + }); + + test.done(); + }, + + 'with all properties set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', { + executionRole: new iam.Role(stack, 'ExecutionRole', { + path: '/', + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('ecs.amazonaws.com'), + new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + ), + }), + family: 'ecs-tasks', + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ExecutionRoleArn: { + 'Fn::GetAtt': [ + 'ExecutionRole605A040B', + 'Arn', + ], + }, + Family: 'ecs-tasks', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: [ + 'EXTERNAL', + ], + TaskRoleArn: { + 'Fn::GetAtt': [ + 'TaskRole30FC0FBB', + 'Arn', + ], + }, + }); + + test.done(); + }, + + 'correctly sets containers'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, // add validation? + }); + + container.addPortMappings({ + containerPort: 3000, + }); + + container.addUlimits({ + hardLimit: 128, + name: ecs.UlimitName.RSS, + softLimit: 128, + }); + + container.addToExecutionPolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['ecs:*'], + })); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: 'amazon/amazon-ecs-sample', + Name: 'web', + PortMappings: [{ + ContainerPort: 3000, + HostPort: 0, + Protocol: Protocol.TCP, + }], + Ulimits: [ + { + HardLimit: 128, + Name: 'rss', + SoftLimit: 128, + }, + ], + }], + }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'ecs:*', + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + + test.done(); + }, + + 'all container definition options defined'(test: Test) { + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/name', + version: 1, + }); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 2048, + cpu: 256, + disableNetworking: true, + command: ['CMD env'], + dnsSearchDomains: ['0.0.0.0'], + dnsServers: ['1.1.1.1'], + dockerLabels: { LABEL: 'label' }, + dockerSecurityOptions: ['ECS_SELINUX_CAPABLE=true'], + entryPoint: ['/app/node_modules/.bin/cdk'], + environment: { TEST_ENVIRONMENT_VARIABLE: 'test environment variable value' }, + environmentFiles: [ecs.EnvironmentFile.fromAsset(path.join(__dirname, '../demo-envfiles/test-envfile.env'))], + essential: true, + extraHosts: { EXTRAHOST: 'extra host' }, + healthCheck: { + command: ['curl localhost:8000'], + interval: cdk.Duration.seconds(20), + retries: 5, + startPeriod: cdk.Duration.seconds(10), + }, + hostname: 'webHost', + linuxParameters: new ecs.LinuxParameters(stack, 'LinuxParameters', { + initProcessEnabled: true, + sharedMemorySize: 1024, + }), + logging: new ecs.AwsLogDriver({ streamPrefix: 'prefix' }), + memoryReservationMiB: 1024, + secrets: { + SECRET: ecs.Secret.fromSecretsManager(secret), + PARAMETER: ecs.Secret.fromSsmParameter(parameter), + }, + user: 'amazon', + workingDirectory: 'app/', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [ + { + Command: [ + 'CMD env', + ], + Cpu: 256, + DisableNetworking: true, + DnsSearchDomains: [ + '0.0.0.0', + ], + DnsServers: [ + '1.1.1.1', + ], + DockerLabels: { + LABEL: 'label', + }, + DockerSecurityOptions: [ + 'ECS_SELINUX_CAPABLE=true', + ], + EntryPoint: [ + '/app/node_modules/.bin/cdk', + ], + Environment: [ + { + Name: 'TEST_ENVIRONMENT_VARIABLE', + Value: 'test environment variable value', + }, + ], + EnvironmentFiles: [{ + Type: 's3', + Value: { + 'Fn::Join': [ + '', + [ + 'arn:aws:s3:::', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7', + }, + '/', + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3VersionKey40E12C15', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3VersionKey40E12C15', + }, + ], + }, + ], + }, + ], + ], + }, + }], + Essential: true, + ExtraHosts: [ + { + Hostname: 'EXTRAHOST', + IpAddress: 'extra host', + }, + ], + HealthCheck: { + Command: [ + 'CMD-SHELL', + 'curl localhost:8000', + ], + Interval: 20, + Retries: 5, + StartPeriod: 10, + Timeout: 5, + }, + Hostname: 'webHost', + Image: 'amazon/amazon-ecs-sample', + LinuxParameters: { + Capabilities: {}, + InitProcessEnabled: true, + SharedMemorySize: 1024, + }, + LogConfiguration: { + LogDriver: 'awslogs', + Options: { + 'awslogs-group': { + Ref: 'ExternalTaskDefwebLogGroup827719D6', + }, + 'awslogs-stream-prefix': 'prefix', + 'awslogs-region': { + Ref: 'AWS::Region', + }, + }, + }, + Memory: 2048, + MemoryReservation: 1024, + Name: 'web', + Secrets: [ + { + Name: 'SECRET', + ValueFrom: { + Ref: 'SecretA720EF05', + }, + }, + { + Name: 'PARAMETER', + ValueFrom: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter/name', + ], + ], + }, + }, + ], + User: 'amazon', + WorkingDirectory: 'app/', + }, + ], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using all props'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage', { + lifecycleRegistryId: '123456789101', + lifecycleRules: [{ + rulePriority: 10, + tagPrefixList: ['abc'], + maxImageCount: 1, + }], + removalPolicy: cdk.RemovalPolicy.DESTROY, + repositoryName: 'project-a/amazon-ecs-sample', + })), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECR::Repository', { + LifecyclePolicy: { + // eslint-disable-next-line max-len + LifecyclePolicyText: '{"rules":[{"rulePriority":10,"selection":{"tagStatus":"tagged","tagPrefixList":["abc"],"countType":"imageCountMoreThan","countNumber":1},"action":{"type":"expire"}}]}', + RegistryId: '123456789101', + }, + RepositoryName: 'project-a/amazon-ecs-sample', + }); + + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':latest', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + }, + + 'correctly sets containers from ECR repository using an image tag'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'myTag'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':myTag', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using an image digest'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + '@sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using default props'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + // WHEN + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage')), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECR::Repository', {}); + + test.done(); + }, + + 'warns when setting containers from ECR repository using fromRegistry method'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + // WHEN + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY'), + memoryLimitMiB: 512, + }); + + // THEN + expect(container.node.metadata[0].data).toEqual("Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); + + test.done(); + }, + + 'correctly sets volumes from'(test: Test) { + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); + + // THEN + expect(() => taskDefinition.addVolume({ + host: { + sourcePath: '/tmp/cache', + }, + name: 'scratch', + })).toThrow('External task definitions doesnt support volumes' ); + + test.done(); + }, + + 'error when interferenceAccelerators set'(test: Test) { + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); + + // THEN + expect(() => taskDefinition.addInferenceAccelerator({ + deviceName: 'device1', + deviceType: 'eia2.medium', + })).toThrow('Cannot use inference accelerators on tasks that run on External service'); + + test.done(); + }, +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-efs/package.json b/packages/@aws-cdk/aws-efs/package.json index 4efbcf3fec5d1..7a68d2200311a 100644 --- a/packages/@aws-cdk/aws-efs/package.json +++ b/packages/@aws-cdk/aws-efs/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks-legacy/package.json b/packages/@aws-cdk/aws-eks-legacy/package.json index cca6fa5402fde..862a69da0f289 100644 --- a/packages/@aws-cdk/aws-eks-legacy/package.json +++ b/packages/@aws-cdk/aws-eks-legacy/package.json @@ -70,7 +70,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 685674d96decb..bf23bff54c899 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -72,9 +72,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", + "@types/aws-lambda": "^8.10.78", "@types/sinon": "^9.0.11", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "@types/yaml": "1.9.6", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticache/package.json b/packages/@aws-cdk/aws-elasticache/package.json index 5b36458012dd2..c0b500a42f22d 100644 --- a/packages/@aws-cdk/aws-elasticache/package.json +++ b/packages/@aws-cdk/aws-elasticache/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/package.json b/packages/@aws-cdk/aws-elasticbeanstalk/package.json index bf69f8a117aa0..80d5033ecc931 100644 --- a/packages/@aws-cdk/aws-elasticbeanstalk/package.json +++ b/packages/@aws-cdk/aws-elasticbeanstalk/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/package.json b/packages/@aws-cdk/aws-elasticloadbalancing/package.json index 6c5fe1525c125..e20392648d2dd 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json index 6f667ad580497..053caf2ce49a5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json index 296b208c75600..e25ccb5ccd96f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json index afda57b575d74..cf411cbd32c88 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index 4e185fab1c424..37c35b69393b2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-emr/package.json b/packages/@aws-cdk/aws-emr/package.json index 357fc85567b2f..f216a69bdf839 100644 --- a/packages/@aws-cdk/aws-emr/package.json +++ b/packages/@aws-cdk/aws-emr/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-emrcontainers/package.json b/packages/@aws-cdk/aws-emrcontainers/package.json index d98724f001660..bf2a7fbe9a627 100644 --- a/packages/@aws-cdk/aws-emrcontainers/package.json +++ b/packages/@aws-cdk/aws-emrcontainers/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 558262fea08fd..3658b64701804 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index 10bcc9bea5d43..2d962235e0227 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "nodeunit": "^0.11.3", diff --git a/packages/@aws-cdk/aws-eventschemas/package.json b/packages/@aws-cdk/aws-eventschemas/package.json index f08e6d978c6e2..0e16baa12d4f4 100644 --- a/packages/@aws-cdk/aws-eventschemas/package.json +++ b/packages/@aws-cdk/aws-eventschemas/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-finspace/package.json b/packages/@aws-cdk/aws-finspace/package.json index 54c59e4e3c7ec..8b70bc5443455 100644 --- a/packages/@aws-cdk/aws-finspace/package.json +++ b/packages/@aws-cdk/aws-finspace/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-fis/package.json b/packages/@aws-cdk/aws-fis/package.json index 73af76ee270c0..7412ed44a10fc 100644 --- a/packages/@aws-cdk/aws-fis/package.json +++ b/packages/@aws-cdk/aws-fis/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-fms/package.json b/packages/@aws-cdk/aws-fms/package.json index a488662c1649a..5ed725a88b4cc 100644 --- a/packages/@aws-cdk/aws-fms/package.json +++ b/packages/@aws-cdk/aws-fms/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-frauddetector/package.json b/packages/@aws-cdk/aws-frauddetector/package.json index 40f62328af688..fbf05f78a7640 100644 --- a/packages/@aws-cdk/aws-frauddetector/package.json +++ b/packages/@aws-cdk/aws-frauddetector/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-fsx/package.json b/packages/@aws-cdk/aws-fsx/package.json index b86b3cb7497f9..1370bae1727de 100644 --- a/packages/@aws-cdk/aws-fsx/package.json +++ b/packages/@aws-cdk/aws-fsx/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-gamelift/package.json b/packages/@aws-cdk/aws-gamelift/package.json index 5f8977e82cde1..07774ebf11293 100644 --- a/packages/@aws-cdk/aws-gamelift/package.json +++ b/packages/@aws-cdk/aws-gamelift/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json b/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json index cf27864c3ab81..532b01de2fbe9 100644 --- a/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json +++ b/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json @@ -70,7 +70,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", diff --git a/packages/@aws-cdk/aws-globalaccelerator/package.json b/packages/@aws-cdk/aws-globalaccelerator/package.json index 3c62075d2b299..b4b0e90197370 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/package.json +++ b/packages/@aws-cdk/aws-globalaccelerator/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "cdk-integ-tools": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-glue/package.json b/packages/@aws-cdk/aws-glue/package.json index 15fb49b3bc56b..f489c1955478e 100644 --- a/packages/@aws-cdk/aws-glue/package.json +++ b/packages/@aws-cdk/aws-glue/package.json @@ -73,9 +73,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-greengrass/package.json b/packages/@aws-cdk/aws-greengrass/package.json index b6fe1c7e2e46a..9fa8de9238ee2 100644 --- a/packages/@aws-cdk/aws-greengrass/package.json +++ b/packages/@aws-cdk/aws-greengrass/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-greengrassv2/package.json b/packages/@aws-cdk/aws-greengrassv2/package.json index 02307786f2482..8096e3269ac30 100644 --- a/packages/@aws-cdk/aws-greengrassv2/package.json +++ b/packages/@aws-cdk/aws-greengrassv2/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-groundstation/package.json b/packages/@aws-cdk/aws-groundstation/package.json index d699b2aa0bb2f..f337bfd5dc79e 100644 --- a/packages/@aws-cdk/aws-groundstation/package.json +++ b/packages/@aws-cdk/aws-groundstation/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-guardduty/package.json b/packages/@aws-cdk/aws-guardduty/package.json index 1346c0f58b9e4..671cd3e85044c 100644 --- a/packages/@aws-cdk/aws-guardduty/package.json +++ b/packages/@aws-cdk/aws-guardduty/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index ed0bf8cbd9845..2cbf971197ef3 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", "@types/sinon": "^9.0.11", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-imagebuilder/package.json b/packages/@aws-cdk/aws-imagebuilder/package.json index d99105d35eb51..0f37220e9e269 100644 --- a/packages/@aws-cdk/aws-imagebuilder/package.json +++ b/packages/@aws-cdk/aws-imagebuilder/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-inspector/package.json b/packages/@aws-cdk/aws-inspector/package.json index 4111ea49c0f74..489f584c5b6e8 100644 --- a/packages/@aws-cdk/aws-inspector/package.json +++ b/packages/@aws-cdk/aws-inspector/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index 450c7917fe426..1e83190d4b532 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iot1click/package.json b/packages/@aws-cdk/aws-iot1click/package.json index 8350be2212a4e..b1821fe309430 100644 --- a/packages/@aws-cdk/aws-iot1click/package.json +++ b/packages/@aws-cdk/aws-iot1click/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotanalytics/package.json b/packages/@aws-cdk/aws-iotanalytics/package.json index 60a7a417efb74..97acc44632e9b 100644 --- a/packages/@aws-cdk/aws-iotanalytics/package.json +++ b/packages/@aws-cdk/aws-iotanalytics/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json b/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json index e896446f705a2..ca500daa21d26 100644 --- a/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json +++ b/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotevents/package.json b/packages/@aws-cdk/aws-iotevents/package.json index a2461f74bcd72..e6e819bba2aed 100644 --- a/packages/@aws-cdk/aws-iotevents/package.json +++ b/packages/@aws-cdk/aws-iotevents/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotfleethub/package.json b/packages/@aws-cdk/aws-iotfleethub/package.json index 8296a0853dad2..f873c33b08634 100644 --- a/packages/@aws-cdk/aws-iotfleethub/package.json +++ b/packages/@aws-cdk/aws-iotfleethub/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotsitewise/package.json b/packages/@aws-cdk/aws-iotsitewise/package.json index dbb8b922d178a..81750ea6c4221 100644 --- a/packages/@aws-cdk/aws-iotsitewise/package.json +++ b/packages/@aws-cdk/aws-iotsitewise/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotthingsgraph/package.json b/packages/@aws-cdk/aws-iotthingsgraph/package.json index 80c594cae7f4f..39dee778b632f 100644 --- a/packages/@aws-cdk/aws-iotthingsgraph/package.json +++ b/packages/@aws-cdk/aws-iotthingsgraph/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotwireless/package.json b/packages/@aws-cdk/aws-iotwireless/package.json index 43fdd6db8741a..176941dd97bb4 100644 --- a/packages/@aws-cdk/aws-iotwireless/package.json +++ b/packages/@aws-cdk/aws-iotwireless/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-ivs/package.json b/packages/@aws-cdk/aws-ivs/package.json index 2c37af7ef1515..31a2a63490f4d 100644 --- a/packages/@aws-cdk/aws-ivs/package.json +++ b/packages/@aws-cdk/aws-ivs/package.json @@ -85,7 +85,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-kendra/package.json b/packages/@aws-cdk/aws-kendra/package.json index 43af58a860116..c78e48ea705e7 100644 --- a/packages/@aws-cdk/aws-kendra/package.json +++ b/packages/@aws-cdk/aws-kendra/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesis/package.json b/packages/@aws-cdk/aws-kinesis/package.json index b2684052ff3c9..51697ec9931df 100644 --- a/packages/@aws-cdk/aws-kinesis/package.json +++ b/packages/@aws-cdk/aws-kinesis/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json index 9adae1521da0e..02ffcb62439e7 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json @@ -66,7 +66,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-kinesisanalytics/package.json b/packages/@aws-cdk/aws-kinesisanalytics/package.json index 1d5f1fdf248be..3a3933d62d655 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics/package.json @@ -76,7 +76,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisfirehose/package.json b/packages/@aws-cdk/aws-kinesisfirehose/package.json index e3aaff7954822..fc949c6290e88 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-kms/package.json b/packages/@aws-cdk/aws-kms/package.json index b61313f395a88..d157f85c4e67d 100644 --- a/packages/@aws-cdk/aws-kms/package.json +++ b/packages/@aws-cdk/aws-kms/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lakeformation/package.json b/packages/@aws-cdk/aws-lakeformation/package.json index 8434842b8e653..a374c0d9627da 100644 --- a/packages/@aws-cdk/aws-lakeformation/package.json +++ b/packages/@aws-cdk/aws-lakeformation/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-destinations/package.json b/packages/@aws-cdk/aws-lambda-destinations/package.json index 9c266b3909ec2..4156149551b5b 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/package.json +++ b/packages/@aws-cdk/aws-lambda-destinations/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index 178e9a4ce5bc7..8a1897a9fc7a5 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", diff --git a/packages/@aws-cdk/aws-lambda-go/package.json b/packages/@aws-cdk/aws-lambda-go/package.json index 2cc20f8b38d92..9f83f91c15963 100644 --- a/packages/@aws-cdk/aws-lambda-go/package.json +++ b/packages/@aws-cdk/aws-lambda-go/package.json @@ -67,7 +67,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert-internal": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index 04f9eca6babf7..ce16b825a12fc 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -64,12 +64,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "delay": "5.0.0", - "esbuild": "^0.12.9", + "esbuild": "^0.12.15", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-lambda-python/package.json b/packages/@aws-cdk/aws-lambda-python/package.json index fb320c9dcb5c0..814ec44de6c69 100644 --- a/packages/@aws-cdk/aws-lambda-python/package.json +++ b/packages/@aws-cdk/aws-lambda-python/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index e83aaf00c6e10..da9f2ff159fd6 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -77,9 +77,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/aws-lambda": "^8.10.77", - "@types/lodash": "^4.14.170", + "@types/jest": "^26.0.24", + "@types/aws-lambda": "^8.10.78", + "@types/lodash": "^4.14.171", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-licensemanager/package.json b/packages/@aws-cdk/aws-licensemanager/package.json index 0847cc63de9f8..eb66089eae974 100644 --- a/packages/@aws-cdk/aws-licensemanager/package.json +++ b/packages/@aws-cdk/aws-licensemanager/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-location/package.json b/packages/@aws-cdk/aws-location/package.json index da398d6e16269..dedff67266046 100644 --- a/packages/@aws-cdk/aws-location/package.json +++ b/packages/@aws-cdk/aws-location/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-logs-destinations/package.json b/packages/@aws-cdk/aws-logs-destinations/package.json index 31b963bcaebe9..5d13936533fcc 100644 --- a/packages/@aws-cdk/aws-logs-destinations/package.json +++ b/packages/@aws-cdk/aws-logs-destinations/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index 226ed0a05dd1b..a7838115e50db 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -72,15 +72,15 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", - "@types/aws-lambda": "^8.10.77", + "@types/nodeunit": "^0.0.32", + "@types/aws-lambda": "^8.10.78", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nock": "^13.1.0", + "nock": "^13.1.1", "nodeunit": "^0.11.3", "pkglint": "0.0.0", "sinon": "^9.2.4", diff --git a/packages/@aws-cdk/aws-lookoutmetrics/package.json b/packages/@aws-cdk/aws-lookoutmetrics/package.json index 29ed950656e67..93c04870c0f5d 100644 --- a/packages/@aws-cdk/aws-lookoutmetrics/package.json +++ b/packages/@aws-cdk/aws-lookoutmetrics/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lookoutvision/package.json b/packages/@aws-cdk/aws-lookoutvision/package.json index c8e46589f578e..d55a122cb7624 100644 --- a/packages/@aws-cdk/aws-lookoutvision/package.json +++ b/packages/@aws-cdk/aws-lookoutvision/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-macie/package.json b/packages/@aws-cdk/aws-macie/package.json index f891fda3ecd68..1cbdb49f09399 100644 --- a/packages/@aws-cdk/aws-macie/package.json +++ b/packages/@aws-cdk/aws-macie/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-managedblockchain/package.json b/packages/@aws-cdk/aws-managedblockchain/package.json index 6590e8af3da6e..675de2f297c20 100644 --- a/packages/@aws-cdk/aws-managedblockchain/package.json +++ b/packages/@aws-cdk/aws-managedblockchain/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediaconnect/package.json b/packages/@aws-cdk/aws-mediaconnect/package.json index e035ebdeb643d..b67436e9224aa 100644 --- a/packages/@aws-cdk/aws-mediaconnect/package.json +++ b/packages/@aws-cdk/aws-mediaconnect/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediaconvert/package.json b/packages/@aws-cdk/aws-mediaconvert/package.json index 1dda1afe88d23..4808bc6dda2ff 100644 --- a/packages/@aws-cdk/aws-mediaconvert/package.json +++ b/packages/@aws-cdk/aws-mediaconvert/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-medialive/package.json b/packages/@aws-cdk/aws-medialive/package.json index b82ef361febb4..3e1e7088f1f0c 100644 --- a/packages/@aws-cdk/aws-medialive/package.json +++ b/packages/@aws-cdk/aws-medialive/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediapackage/package.json b/packages/@aws-cdk/aws-mediapackage/package.json index fbc58672183b7..84077872af4d5 100644 --- a/packages/@aws-cdk/aws-mediapackage/package.json +++ b/packages/@aws-cdk/aws-mediapackage/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediastore/package.json b/packages/@aws-cdk/aws-mediastore/package.json index 440c6c934bccf..20e9b4057dbe5 100644 --- a/packages/@aws-cdk/aws-mediastore/package.json +++ b/packages/@aws-cdk/aws-mediastore/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-msk/package.json b/packages/@aws-cdk/aws-msk/package.json index 14d07f5bb7ebb..7309920e959ac 100644 --- a/packages/@aws-cdk/aws-msk/package.json +++ b/packages/@aws-cdk/aws-msk/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-mwaa/package.json b/packages/@aws-cdk/aws-mwaa/package.json index 7cd2aa2a15c27..07fbbab350909 100644 --- a/packages/@aws-cdk/aws-mwaa/package.json +++ b/packages/@aws-cdk/aws-mwaa/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index 93e43835652ca..dfc50d27ac70d 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-networkfirewall/package.json b/packages/@aws-cdk/aws-networkfirewall/package.json index 6a29d30433858..27b3aa04a7136 100644 --- a/packages/@aws-cdk/aws-networkfirewall/package.json +++ b/packages/@aws-cdk/aws-networkfirewall/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-networkmanager/package.json b/packages/@aws-cdk/aws-networkmanager/package.json index 29071ca7dae1b..f0666a1e676cc 100644 --- a/packages/@aws-cdk/aws-networkmanager/package.json +++ b/packages/@aws-cdk/aws-networkmanager/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-nimblestudio/package.json b/packages/@aws-cdk/aws-nimblestudio/package.json index 83fe8f795b276..5f9f77d2f58b2 100644 --- a/packages/@aws-cdk/aws-nimblestudio/package.json +++ b/packages/@aws-cdk/aws-nimblestudio/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-opsworks/package.json b/packages/@aws-cdk/aws-opsworks/package.json index 9c35474f64721..978dd2b2b07c5 100644 --- a/packages/@aws-cdk/aws-opsworks/package.json +++ b/packages/@aws-cdk/aws-opsworks/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-opsworkscm/package.json b/packages/@aws-cdk/aws-opsworkscm/package.json index 2dfddff960cf0..42c0e014301cb 100644 --- a/packages/@aws-cdk/aws-opsworkscm/package.json +++ b/packages/@aws-cdk/aws-opsworkscm/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-pinpoint/package.json b/packages/@aws-cdk/aws-pinpoint/package.json index 48fa3dbeac296..0f5b7b48a84cf 100644 --- a/packages/@aws-cdk/aws-pinpoint/package.json +++ b/packages/@aws-cdk/aws-pinpoint/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-pinpointemail/package.json b/packages/@aws-cdk/aws-pinpointemail/package.json index ac26c0fbcccc0..7659ce4025b4a 100644 --- a/packages/@aws-cdk/aws-pinpointemail/package.json +++ b/packages/@aws-cdk/aws-pinpointemail/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-qldb/package.json b/packages/@aws-cdk/aws-qldb/package.json index 6b902b32ece98..ac50a35b7c1aa 100644 --- a/packages/@aws-cdk/aws-qldb/package.json +++ b/packages/@aws-cdk/aws-qldb/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-quicksight/package.json b/packages/@aws-cdk/aws-quicksight/package.json index 5b1b626100d37..3d55f0052512b 100644 --- a/packages/@aws-cdk/aws-quicksight/package.json +++ b/packages/@aws-cdk/aws-quicksight/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-ram/package.json b/packages/@aws-cdk/aws-ram/package.json index f5d96286a4def..6383a8a7ce1a1 100644 --- a/packages/@aws-cdk/aws-ram/package.json +++ b/packages/@aws-cdk/aws-ram/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 4dfa680fe5abe..6144a8f13493b 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/cx-api": "0.0.0", diff --git a/packages/@aws-cdk/aws-redshift/package.json b/packages/@aws-cdk/aws-redshift/package.json index 4663e68c63c86..5ccccb2f4f2df 100644 --- a/packages/@aws-cdk/aws-redshift/package.json +++ b/packages/@aws-cdk/aws-redshift/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-resourcegroups/package.json b/packages/@aws-cdk/aws-resourcegroups/package.json index a8fb1b9692a1b..611c01ef93ae8 100644 --- a/packages/@aws-cdk/aws-resourcegroups/package.json +++ b/packages/@aws-cdk/aws-resourcegroups/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-robomaker/package.json b/packages/@aws-cdk/aws-robomaker/package.json index c6c5e5116fd17..8f4f5c4350cc7 100644 --- a/packages/@aws-cdk/aws-robomaker/package.json +++ b/packages/@aws-cdk/aws-robomaker/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-patterns/package.json b/packages/@aws-cdk/aws-route53-patterns/package.json index e4e4b00ea0679..4692f1c25903d 100644 --- a/packages/@aws-cdk/aws-route53-patterns/package.json +++ b/packages/@aws-cdk/aws-route53-patterns/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-targets/package.json b/packages/@aws-cdk/aws-route53-targets/package.json index 780d3eaa5ae5e..0d42c4d83462f 100644 --- a/packages/@aws-cdk/aws-route53-targets/package.json +++ b/packages/@aws-cdk/aws-route53-targets/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 564fce910f60e..4d72b2ed05616 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -73,9 +73,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", - "@types/nodeunit": "^0.0.31", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", + "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index 0adddf59c4e03..ab96545ce6345 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-assets/package.json b/packages/@aws-cdk/aws-s3-assets/package.json index 84586ad299090..987039fd3ab4b 100644 --- a/packages/@aws-cdk/aws-s3-assets/package.json +++ b/packages/@aws-cdk/aws-s3-assets/package.json @@ -71,7 +71,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index b3e459bd47261..c5ae455fc2928 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -80,7 +80,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/cx-api": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-s3-notifications/package.json b/packages/@aws-cdk/aws-s3-notifications/package.json index 9ccc7aeef64c4..62602ff1b6ce4 100644 --- a/packages/@aws-cdk/aws-s3-notifications/package.json +++ b/packages/@aws-cdk/aws-s3-notifications/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 866f2bfb6c7df..06e4248c943e7 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3objectlambda/package.json b/packages/@aws-cdk/aws-s3objectlambda/package.json index e9d370457a7ee..7818c261a21bc 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/package.json +++ b/packages/@aws-cdk/aws-s3objectlambda/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3outposts/package.json b/packages/@aws-cdk/aws-s3outposts/package.json index efbb11baea3e7..4645c65189333 100644 --- a/packages/@aws-cdk/aws-s3outposts/package.json +++ b/packages/@aws-cdk/aws-s3outposts/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-sagemaker/package.json b/packages/@aws-cdk/aws-sagemaker/package.json index aebaaba2813c2..3f2fa9d8ab857 100644 --- a/packages/@aws-cdk/aws-sagemaker/package.json +++ b/packages/@aws-cdk/aws-sagemaker/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index b7452bfb3ba08..0fa097ffba069 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-sdb/package.json b/packages/@aws-cdk/aws-sdb/package.json index 7786f7b50037b..b1773cf5a3907 100644 --- a/packages/@aws-cdk/aws-sdb/package.json +++ b/packages/@aws-cdk/aws-sdb/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index 1a51fdba46872..8e465f8d54c77 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-securityhub/package.json b/packages/@aws-cdk/aws-securityhub/package.json index b49e9244b5fe7..ad2e1abf1e401 100644 --- a/packages/@aws-cdk/aws-securityhub/package.json +++ b/packages/@aws-cdk/aws-securityhub/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index 74073c55b37ce..63b461e1460f3 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -30,6 +30,9 @@ enables organizations to create and manage catalogs of products for their end us - [Granting access to a portfolio](#granting-access-to-a-portfolio) - [Sharing a portfolio with another AWS account](#sharing-a-portfolio-with-another-aws-account) - [Product](#product) + - [Adding a product to a portfolio](#adding-a-product-to-a-portfolio) +- [Constraints](#constraints) + - [Tag update constraint](#tag-update-constraint) The `@aws-cdk/aws-servicecatalog` package contains resources that enable users to automate governance and management of their AWS resources at scale. @@ -57,7 +60,7 @@ new servicecatalog.Portfolio(this, 'MyFirstPortfolio', { displayName: 'MyFirstPortfolio', providerName: 'MyTeam', description: 'Portfolio for a project', - acceptLanguage: servicecatalog.AcceptLanguage.EN, + messageLanguage: servicecatalog.MessageLanguage.EN, }); ``` @@ -125,6 +128,8 @@ Assets are files that are uploaded to an S3 Bucket before deployment. `CloudFormationTemplate.fromAsset` can be utilized to create a Product by passing the path to a local template file on your disk: ```ts +import * as path from 'path'; + const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', { productName: "My Product", owner: "Product Owner", @@ -141,3 +146,46 @@ const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', ] }); ``` + +### Adding a product to a portfolio + +You add products to a portfolio to manage your resources at scale. After adding a product to a portfolio, +it creates a portfolio-product association, and will become visible from the portfolio side in both the console and service catalog CLI. +A product can be added to multiple portfolios depending on your resource and organizational needs. + +```ts fixture=portfolio-product +portfolio.addProduct(product); +``` + +## Constraints + +Constraints define governance mechanisms that allow you to manage permissions, notifications, and options related to actions end users can perform on products, +Constraints are applied on a portfolio-product association. +Using the CDK, if you do not explicitly associate a product to a portfolio and add a constraint, it will automatically add an association for you. + +There are rules around plurariliites of constraints for a portfolio and product. +For example, you can only have a single "tag update" constraint applied to a portfolio-product association. +If a misconfigured constraint is added, `synth` will fail with an error message. + +Read more at [Service Catalog Constraints](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/constraints.html). + +### Tag update constraint + +Tag update constraints allow or disallow end users to update tags on resources associated with an AWS Service Catalog product upon provisioning. +By default, tag updating is not permitted. +If tag updating is allowed, then new tags associated with the product or portfolio will be applied to provisioned resources during a provisioned product update. + +```ts fixture=portfolio-product +portfolio.addProduct(product); + +portfolio.constrainTagUpdates(product); +``` + +If you want to disable this feature later on, you can update it by setting the "allow" parameter to `false`: + +```ts fixture=portfolio-product +// to disable tag updates: +portfolio.constrainTagUpdates(product, { + allow: false, +}); +``` diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/common.ts b/packages/@aws-cdk/aws-servicecatalog/lib/common.ts index f1382342626af..4f207be273867 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/common.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/common.ts @@ -1,7 +1,9 @@ /** * The language code. + * Used for error and logging messages for end users. + * The default behavior if not specified is English. */ -export enum AcceptLanguage { +export enum MessageLanguage { /** * English */ @@ -16,4 +18,4 @@ export enum AcceptLanguage { * Chinese */ ZH = 'zh' -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts b/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts new file mode 100644 index 0000000000000..54a6e40973c4f --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts @@ -0,0 +1,32 @@ +import { MessageLanguage } from './common'; + +/** + * Properties for governance mechanisms and constraints. + */ +export interface CommonConstraintOptions { + /** + * The language code. + * Configures the language for error messages from service catalog. + * + * @default - English + */ + readonly messageLanguage?: MessageLanguage; + + /** + * The description of the constraint. + * + * @default - No description provided + */ + readonly description?: string; +} + +/** + * Properties for ResourceUpdateConstraint. + */ +export interface TagUpdateConstraintOptions extends CommonConstraintOptions { + /** + * Toggle for if users should be allowed to change/update tags on provisioned products. + * @default true + */ + readonly allow?: boolean; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts index 330513de79bf6..7c621b4438ea4 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts @@ -1,4 +1,5 @@ export * from './common'; +export * from './constraints'; export * from './cloudformation-template'; export * from './portfolio'; export * from './product'; diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts index c11c980bb4520..ce1ac8edee493 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts @@ -1,8 +1,11 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { AcceptLanguage } from './common'; +import { MessageLanguage } from './common'; +import { TagUpdateConstraintOptions } from './constraints'; +import { AssociationManager } from './private/association-manager'; import { hashValues } from './private/util'; import { InputValidator } from './private/validation'; +import { IProduct } from './product'; import { CfnPortfolio, CfnPortfolioPrincipalAssociation, CfnPortfolioShare } from './servicecatalog.generated'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -15,15 +18,18 @@ import { Construct } from 'constructs'; export interface PortfolioShareOptions { /** * Whether to share tagOptions as a part of the portfolio share + * * @default - share not specified */ readonly shareTagOptions?: boolean; /** - * The accept language of the share - * @default - accept language not specified + * The message language of the share. + * Controls status and error message language for share. + * + * @default - English */ - readonly acceptLanguage?: AcceptLanguage; + readonly messageLanguage?: MessageLanguage; } /** @@ -66,6 +72,17 @@ export interface IPortfolio extends cdk.IResource { * @param options Options for the initiate share */ shareWithAccount(accountId: string, options?: PortfolioShareOptions): void; + + /** + * Associate portfolio with the given product. + * @param product A service catalog produt. + */ + addProduct(product: IProduct): void; + + /** + * Add a Resource Update Constraint. + */ + constrainTagUpdates(product: IProduct, options?: TagUpdateConstraintOptions): void; } abstract class PortfolioBase extends cdk.Resource implements IPortfolio { @@ -85,16 +102,24 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio { this.associatePrincipal(group.groupArn, group.node.addr); } + public addProduct(product: IProduct): void { + AssociationManager.associateProductWithPortfolio(this, product); + } + public shareWithAccount(accountId: string, options: PortfolioShareOptions = {}): void { const hashId = this.generateUniqueHash(accountId); new CfnPortfolioShare(this, `PortfolioShare${hashId}`, { portfolioId: this.portfolioId, accountId: accountId, shareTagOptions: options.shareTagOptions, - acceptLanguage: options.acceptLanguage, + acceptLanguage: options.messageLanguage, }); } + public constrainTagUpdates(product: IProduct, options: TagUpdateConstraintOptions = {}): void { + AssociationManager.constrainTagUpdates(this, product, options); + } + /** * Associate a principal with the portfolio. * If the principal is already associated, it will skip. @@ -132,13 +157,16 @@ export interface PortfolioProps { readonly providerName: string; /** - * The accept language. - * @default - No accept language provided + * The message language. Controls language for + * status logging and errors. + * + * @default - English */ - readonly acceptLanguage?: AcceptLanguage; + readonly messageLanguage?: MessageLanguage; /** * Description for portfolio. + * * @default - No description provided */ readonly description?: string; @@ -156,7 +184,7 @@ export class Portfolio extends PortfolioBase { * @param portfolioArn the Amazon Resource Name of the existing portfolio. */ public static fromPortfolioArn(scope: Construct, id: string, portfolioArn: string): IPortfolio { - const arn = cdk.Stack.of(scope).parseArn(portfolioArn); + const arn = cdk.Stack.of(scope).splitArn(portfolioArn, cdk.ArnFormat.SLASH_RESOURCE_NAME); const portfolioId = arn.resourceName; if (!portfolioId) { @@ -190,7 +218,7 @@ export class Portfolio extends PortfolioBase { displayName: props.displayName, providerName: props.providerName, description: props.description, - acceptLanguage: props.acceptLanguage, + acceptLanguage: props.messageLanguage, }); this.portfolioId = this.portfolio.ref; this.portfolioArn = cdk.Stack.of(this).formatArn({ diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts new file mode 100644 index 0000000000000..5a163073b29a1 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts @@ -0,0 +1,54 @@ +import * as cdk from '@aws-cdk/core'; +import { TagUpdateConstraintOptions } from '../constraints'; +import { IPortfolio } from '../portfolio'; +import { IProduct } from '../product'; +import { CfnPortfolioProductAssociation, CfnResourceUpdateConstraint } from '../servicecatalog.generated'; +import { hashValues } from './util'; +import { InputValidator } from './validation'; + +export class AssociationManager { + public static associateProductWithPortfolio( + portfolio: IPortfolio, product: IProduct, + ): { associationKey: string, cfnPortfolioProductAssociation: CfnPortfolioProductAssociation } { + const associationKey = hashValues(portfolio.node.addr, product.node.addr, product.stack.node.addr); + const constructId = `PortfolioProductAssociation${associationKey}`; + const existingAssociation = portfolio.node.tryFindChild(constructId); + const cfnAssociation = existingAssociation + ? existingAssociation as CfnPortfolioProductAssociation + : new CfnPortfolioProductAssociation(portfolio as unknown as cdk.Resource, constructId, { + portfolioId: portfolio.portfolioId, + productId: product.productId, + }); + + return { + associationKey: associationKey, + cfnPortfolioProductAssociation: cfnAssociation, + }; + } + + + public static constrainTagUpdates(portfolio: IPortfolio, product: IProduct, options: TagUpdateConstraintOptions): void { + InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options.description); + const association = this.associateProductWithPortfolio(portfolio, product); + const constructId = `ResourceUpdateConstraint${association.associationKey}`; + + if (!portfolio.node.tryFindChild(constructId)) { + const constraint = new CfnResourceUpdateConstraint(portfolio as unknown as cdk.Resource, constructId, { + acceptLanguage: options.messageLanguage, + description: options.description, + portfolioId: portfolio.portfolioId, + productId: product.productId, + tagUpdateOnProvisionedProduct: options.allow === false ? 'NOT_ALLOWED' : 'ALLOWED', + }); + + // Add dependsOn to force proper order in deployment. + constraint.addDependsOn(association.cfnPortfolioProductAssociation); + } else { + throw new Error(`Cannot have multiple tag update constraints for association ${this.prettyPrintAssociation(portfolio, product)}`); + } + } + + private static prettyPrintAssociation(portfolio: IPortfolio, product: IProduct): string { + return `- Portfolio: ${portfolio.node.path} | Product: ${product.node.path}`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts index 3de0ad8387460..466e1fa726e55 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts @@ -1,7 +1,7 @@ import { ArnFormat, IResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CloudFormationTemplate } from './cloudformation-template'; -import { AcceptLanguage } from './common'; +import { MessageLanguage } from './common'; import { InputValidator } from './private/validation'; import { CfnCloudFormationProduct } from './servicecatalog.generated'; @@ -77,9 +77,11 @@ export interface CloudFormationProductProps { /** * The language code. - * @default - No accept language provided + * Controls language for logging and errors. + * + * @default - English */ - readonly acceptLanguage?: AcceptLanguage; + readonly messageLanguage?: MessageLanguage; /** * The description of the product. @@ -156,7 +158,7 @@ export class CloudFormationProduct extends Product { this.validateProductProps(props); const product = new CfnCloudFormationProduct(this, 'Resource', { - acceptLanguage: props.acceptLanguage, + acceptLanguage: props.messageLanguage, description: props.description, distributor: props.distributor, name: props.productName, diff --git a/packages/@aws-cdk/aws-servicecatalog/package.json b/packages/@aws-cdk/aws-servicecatalog/package.json index 2c7ea4a279712..e448e5aa5b559 100644 --- a/packages/@aws-cdk/aws-servicecatalog/package.json +++ b/packages/@aws-cdk/aws-servicecatalog/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicecatalog/rosetta/portfolio-product.ts-fixture b/packages/@aws-cdk/aws-servicecatalog/rosetta/portfolio-product.ts-fixture new file mode 100644 index 0000000000000..20a1db30bf3ee --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/rosetta/portfolio-product.ts-fixture @@ -0,0 +1,28 @@ +// Fixture with packages imported, but nothing else +import { Construct, Stack } from '@aws-cdk/core'; +import * as servicecatalog from '@aws-cdk/aws-servicecatalog'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const portfolio = new servicecatalog.Portfolio(this, "MyFirstPortfolio", { + displayName: "MyFirstPortfolio", + providerName: "MyTeam", + }); + + const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', { + productName: "My Product", + owner: "Product Owner", + productVersions: [ + { + productVersionName: "v1", + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl( + 'https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ServiceCatalog/Product.yaml'), + }, + ] + }); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json index 0409382efba03..1fc70614de939 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json @@ -1,87 +1,128 @@ { - "Resources": { - "TestRole6C9272DF": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::", - { - "Ref": "AWS::AccountId" - }, - ":root" - ] + "Resources": { + "TestRole6C9272DF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" ] - } + ] } } - ], - "Version": "2012-10-17" - } - } - }, - "TestGroupAF88660E": { - "Type": "AWS::IAM::Group" - }, - "TestPortfolio4AC794EB": { - "Type": "AWS::ServiceCatalog::Portfolio", - "Properties": { - "DisplayName": "TestPortfolio", - "ProviderName": "TestProvider", - "Description": "This is our Service Catalog Portfolio" + } + ], + "Version": "2012-10-17" } - }, - "TestPortfolioPortolioPrincipalAssociation20e1afa20ac27E1A060D": { - "Type": "AWS::ServiceCatalog::PortfolioPrincipalAssociation", - "Properties": { - "PortfolioId": { - "Ref": "TestPortfolio4AC794EB" - }, - "PrincipalARN": { - "Fn::GetAtt": [ - "TestRole6C9272DF", - "Arn" - ] - }, - "PrincipalType": "IAM" + } + }, + "TestGroupAF88660E": { + "Type": "AWS::IAM::Group" + }, + "TestPortfolio4AC794EB": { + "Type": "AWS::ServiceCatalog::Portfolio", + "Properties": { + "DisplayName": "TestPortfolio", + "ProviderName": "TestProvider", + "AcceptLanguage": "en", + "Description": "This is our Service Catalog Portfolio" + } + }, + "TestPortfolioPortolioPrincipalAssociation20e1afa20ac27E1A060D": { + "Type": "AWS::ServiceCatalog::PortfolioPrincipalAssociation", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "PrincipalARN": { + "Fn::GetAtt": [ + "TestRole6C9272DF", + "Arn" + ] + }, + "PrincipalType": "IAM" + } + }, + "TestPortfolioPortolioPrincipalAssociation44a1ca1c23384D6E460B": { + "Type": "AWS::ServiceCatalog::PortfolioPrincipalAssociation", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "PrincipalARN": { + "Fn::GetAtt": [ + "TestGroupAF88660E", + "Arn" + ] + }, + "PrincipalType": "IAM" + } + }, + "TestPortfolioPortfolioSharebf5b82f042508F035880": { + "Type": "AWS::ServiceCatalog::PortfolioShare", + "Properties": { + "AccountId": "123456789012", + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" } - }, - "TestPortfolioPortolioPrincipalAssociation44a1ca1c23384D6E460B": { - "Type": "AWS::ServiceCatalog::PortfolioPrincipalAssociation", - "Properties": { - "PortfolioId": { - "Ref": "TestPortfolio4AC794EB" - }, - "PrincipalARN": { - "Fn::GetAtt": [ - "TestGroupAF88660E", - "Arn" - ] - }, - "PrincipalType": "IAM" + } + }, + "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7": { + "Type": "AWS::ServiceCatalog::PortfolioProductAssociation", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "ProductId": { + "Ref": "TestProduct7606930B" } + } + }, + "TestPortfolioResourceUpdateConstrainta0185761d231AB0EAAE0": { + "Type": "AWS::ServiceCatalog::ResourceUpdateConstraint", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "ProductId": { + "Ref": "TestProduct7606930B" + }, + "TagUpdateOnProvisionedProduct": "ALLOWED" }, - "TestPortfolioPortfolioSharebf5b82f042508F035880": { - "Type": "AWS::ServiceCatalog::PortfolioShare", - "Properties": { - "AccountId": "123456789012", - "PortfolioId": { - "Ref": "TestPortfolio4AC794EB" + "DependsOn": [ + "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" + ] + }, + "TestProduct7606930B": { + "Type": "AWS::ServiceCatalog::CloudFormationProduct", + "Properties": { + "Name": "testProduct", + "Owner": "testOwner", + "ProvisioningArtifactParameters": [ + { + "DisableTemplateValidation": true, + "Info": { + "LoadTemplateFromURL": "https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template" + } } - } + ] } } } - \ No newline at end of file +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts index b62523c59083f..d48bd9796286e 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts @@ -15,6 +15,7 @@ const portfolio = new servicecatalog.Portfolio(stack, 'TestPortfolio', { displayName: 'TestPortfolio', providerName: 'TestProvider', description: 'This is our Service Catalog Portfolio', + messageLanguage: servicecatalog.MessageLanguage.EN, }); portfolio.giveAccessToRole(role); @@ -22,4 +23,20 @@ portfolio.giveAccessToGroup(group); portfolio.shareWithAccount('123456789012'); +const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + validateTemplate: false, + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl( + 'https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template'), + }, + ], +}); + +portfolio.addProduct(product); + +portfolio.constrainTagUpdates(product); + app.synth(); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts index c85bf4b05955c..1df07c37ae356 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts @@ -30,17 +30,17 @@ describe('Portfolio', () => { }); }), - test('portfolio with explicit acceptLanguage and description', () => { + test('portfolio with explicit message language and description', () => { new servicecatalog.Portfolio(stack, 'MyPortfolio', { displayName: 'testPortfolio', providerName: 'testProvider', description: 'test portfolio description', - acceptLanguage: servicecatalog.AcceptLanguage.ZH, + messageLanguage: servicecatalog.MessageLanguage.ZH, }); expect(stack).toHaveResourceLike('AWS::ServiceCatalog::Portfolio', { Description: 'test portfolio description', - AcceptLanguage: servicecatalog.AcceptLanguage.ZH, + AcceptLanguage: servicecatalog.MessageLanguage.ZH, }); }), @@ -185,7 +185,7 @@ describe('Portfolio', () => { portfolio.shareWithAccount(shareAccountId, { shareTagOptions: true, - acceptLanguage: servicecatalog.AcceptLanguage.EN, + messageLanguage: servicecatalog.MessageLanguage.EN, }); expect(stack).toHaveResourceLike('AWS::ServiceCatalog::PortfolioShare', { @@ -259,3 +259,79 @@ describe('Portfolio', () => { }); }); }); + +describe('portfolio associations and product constraints', () => { + let stack: cdk.Stack; + let portfolio: servicecatalog.Portfolio; + let product: servicecatalog.CloudFormationProduct; + + beforeEach(() => { + stack = new cdk.Stack(); + + portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { + displayName: 'testPortfolio', + providerName: 'testProvider', + }); + + product = new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl('https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template'), + }, + ], + }); + }), + + test('basic portfolio product association', () => { + portfolio.addProduct(product); + + expect(stack).toHaveResource('AWS::ServiceCatalog::PortfolioProductAssociation'); + }); + + test('portfolio product associations are idempotent', () => { + portfolio.addProduct(product); + portfolio.addProduct(product); // If not idempotent these calls should fail + + expect(stack).toCountResources('AWS::ServiceCatalog::PortfolioProductAssociation', 1); //check anyway + }), + + test('add tag update constraint', () => { + portfolio.addProduct(product); + portfolio.constrainTagUpdates(product, { + allow: true, + }); + + expect(stack).toHaveResourceLike('AWS::ServiceCatalog::ResourceUpdateConstraint', { + TagUpdateOnProvisionedProduct: 'ALLOWED', + }); + }); + + test('tag update constraint still adds without explicit association', () => { + portfolio.constrainTagUpdates(product, { + messageLanguage: servicecatalog.MessageLanguage.EN, + description: 'test constraint description', + allow: false, + }); + + expect(stack).toHaveResourceLike('AWS::ServiceCatalog::ResourceUpdateConstraint', { + AcceptLanguage: servicecatalog.MessageLanguage.EN, + Description: 'test constraint description', + TagUpdateOnProvisionedProduct: 'NOT_ALLOWED', + }); + }), + + test('fails to add multiple tag update constraints', () => { + portfolio.constrainTagUpdates(product, { + description: 'test constraint description', + }); + + expect(() => { + portfolio.constrainTagUpdates(product, { + allow: false, + description: 'another test constraint description', + }); + }).toThrowError(/Cannot have multiple tag update constraints for association/); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json index e29e277575c45..ecf6b6990c3ad 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert-internal": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicediscovery/package.json b/packages/@aws-cdk/aws-servicediscovery/package.json index aecfddd3e5f08..6f336ff681706 100644 --- a/packages/@aws-cdk/aws-servicediscovery/package.json +++ b/packages/@aws-cdk/aws-servicediscovery/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ses-actions/package.json b/packages/@aws-cdk/aws-ses-actions/package.json index c27f63ff9e472..37ead111df047 100644 --- a/packages/@aws-cdk/aws-ses-actions/package.json +++ b/packages/@aws-cdk/aws-ses-actions/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ses/package.json b/packages/@aws-cdk/aws-ses/package.json index 7234e62501204..36c7fca9442e3 100644 --- a/packages/@aws-cdk/aws-ses/package.json +++ b/packages/@aws-cdk/aws-ses/package.json @@ -72,8 +72,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/nodeunit": "^0.0.31", + "@types/aws-lambda": "^8.10.78", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-signer/package.json b/packages/@aws-cdk/aws-signer/package.json index be95656d5aa57..e0318b1bcf011 100644 --- a/packages/@aws-cdk/aws-signer/package.json +++ b/packages/@aws-cdk/aws-signer/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-sns-subscriptions/package.json b/packages/@aws-cdk/aws-sns-subscriptions/package.json index 1b5da65d03a19..9d0b4db805c67 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/package.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index 09727cd171086..9ec8371fc3a6a 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -76,7 +76,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index 135d09b8ca4a1..7daec64f522f5 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -73,7 +73,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index cc2322ca7c5b9..dcf9ddfe31a17 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ssmcontacts/package.json b/packages/@aws-cdk/aws-ssmcontacts/package.json index 92baed5d4991f..fb7c8defd4291 100644 --- a/packages/@aws-cdk/aws-ssmcontacts/package.json +++ b/packages/@aws-cdk/aws-ssmcontacts/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ssmincidents/package.json b/packages/@aws-cdk/aws-ssmincidents/package.json index 88d9c8a1aad01..1ef3f6cc287f7 100644 --- a/packages/@aws-cdk/aws-ssmincidents/package.json +++ b/packages/@aws-cdk/aws-ssmincidents/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sso/package.json b/packages/@aws-cdk/aws-sso/package.json index b9e21e04cb25b..9eae6cfdc23a0 100644 --- a/packages/@aws-cdk/aws-sso/package.json +++ b/packages/@aws-cdk/aws-sso/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index bbf272be4873a..dc297c9d37bfc 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -71,7 +71,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-glue": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index f4d4079b6995d..485a879fee653 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-synthetics/package.json b/packages/@aws-cdk/aws-synthetics/package.json index 7c963a3d8dcd9..7febc7e87b686 100644 --- a/packages/@aws-cdk/aws-synthetics/package.json +++ b/packages/@aws-cdk/aws-synthetics/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-timestream/package.json b/packages/@aws-cdk/aws-timestream/package.json index b6d1d0921e26d..7a00e4fcb5c84 100644 --- a/packages/@aws-cdk/aws-timestream/package.json +++ b/packages/@aws-cdk/aws-timestream/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-transfer/package.json b/packages/@aws-cdk/aws-transfer/package.json index c761976d1404b..45fc2b0164fc1 100644 --- a/packages/@aws-cdk/aws-transfer/package.json +++ b/packages/@aws-cdk/aws-transfer/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-waf/package.json b/packages/@aws-cdk/aws-waf/package.json index 886bb0e3e35fd..ec2e3ec379265 100644 --- a/packages/@aws-cdk/aws-waf/package.json +++ b/packages/@aws-cdk/aws-waf/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-wafregional/package.json b/packages/@aws-cdk/aws-wafregional/package.json index a79a5b841661f..4003f4ff7b0f0 100644 --- a/packages/@aws-cdk/aws-wafregional/package.json +++ b/packages/@aws-cdk/aws-wafregional/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-wafv2/package.json b/packages/@aws-cdk/aws-wafv2/package.json index 1f8afbc0c51ae..f7c5ae3b14798 100644 --- a/packages/@aws-cdk/aws-wafv2/package.json +++ b/packages/@aws-cdk/aws-wafv2/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-workspaces/package.json b/packages/@aws-cdk/aws-workspaces/package.json index d50fb86e1c262..ab45c38ed0fbd 100644 --- a/packages/@aws-cdk/aws-workspaces/package.json +++ b/packages/@aws-cdk/aws-workspaces/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-xray/package.json b/packages/@aws-cdk/aws-xray/package.json index 22780d98fc754..1d63e8fa740b3 100644 --- a/packages/@aws-cdk/aws-xray/package.json +++ b/packages/@aws-cdk/aws-xray/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/cdk-assets-schema/package.json b/packages/@aws-cdk/cdk-assets-schema/package.json index ffc53ed507cd4..f4e5ef6cb1b5f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/package.json +++ b/packages/@aws-cdk/cdk-assets-schema/package.json @@ -52,7 +52,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index ea32ea3126781..794305b7dd323 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,56 @@ +# CloudFormation Resource Specification v39.5.0 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + +* AWS::AmazonMQ::Broker MaintenanceWindowStartTime.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::CodeDeploy::Application Tags (__added__) +* AWS::CodeDeploy::DeploymentConfig ComputePlatform (__added__) +* AWS::CodeDeploy::DeploymentConfig TrafficRoutingConfig (__added__) +* AWS::CodeDeploy::DeploymentGroup BlueGreenDeploymentConfiguration (__added__) +* AWS::CodeDeploy::DeploymentGroup ECSServices (__added__) +* AWS::DataBrew::Job DataCatalogOutputs (__added__) +* AWS::ServiceDiscovery::HttpNamespace Description.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ServiceDiscovery::PrivateDnsNamespace Properties (__added__) +* AWS::ServiceDiscovery::PrivateDnsNamespace Description.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ServiceDiscovery::PublicDnsNamespace Properties (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace Description.UpdateType (__changed__) + * Old: Immutable + * New: Mutable + +## Property Type Changes + +* AWS::CodeDeploy::DeploymentConfig.TimeBasedCanary (__added__) +* AWS::CodeDeploy::DeploymentConfig.TimeBasedLinear (__added__) +* AWS::CodeDeploy::DeploymentConfig.TrafficRoutingConfig (__added__) +* AWS::CodeDeploy::DeploymentGroup.BlueGreenDeploymentConfiguration (__added__) +* AWS::CodeDeploy::DeploymentGroup.BlueInstanceTerminationOption (__added__) +* AWS::CodeDeploy::DeploymentGroup.DeploymentReadyOption (__added__) +* AWS::CodeDeploy::DeploymentGroup.ECSService (__added__) +* AWS::CodeDeploy::DeploymentGroup.GreenFleetProvisioningOption (__added__) +* AWS::DataBrew::Job.DataCatalogOutput (__added__) +* AWS::DataBrew::Job.DatabaseTableOutputOptions (__added__) +* AWS::DataBrew::Job.S3TableOutputOptions (__added__) +* AWS::ServiceDiscovery::PrivateDnsNamespace.PrivateDnsPropertiesMutable (__added__) +* AWS::ServiceDiscovery::PrivateDnsNamespace.Properties (__added__) +* AWS::ServiceDiscovery::PrivateDnsNamespace.SOA (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace.Properties (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace.PublicDnsPropertiesMutable (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace.SOA (__added__) +* AWS::ApiGatewayV2::DomainName.DomainNameConfiguration OwnershipVerificationCertificateArn (__deleted__) + + # CloudFormation Resource Specification v39.3.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index c4c1d5c055c32..21aa1374d7809 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -39.3.0 +39.5.0 diff --git a/packages/@aws-cdk/cfnspec/package.json b/packages/@aws-cdk/cfnspec/package.json index cc391710f7112..f9c34feb21e7a 100644 --- a/packages/@aws-cdk/cfnspec/package.json +++ b/packages/@aws-cdk/cfnspec/package.json @@ -26,9 +26,9 @@ "main": "lib/index.js", "types": "lib/index.d.ts", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/md5": "^2.3.0", - "@types/nodeunit": "^0.0.31", + "@types/fs-extra": "^8.1.2", + "@types/md5": "^2.3.1", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "fast-json-patch": "^2.2.1", "fs-extra": "^9.1.0", diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 2ef6d5f76a30e..5986caf0ad273 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -2175,12 +2175,6 @@ "Required": false, "UpdateType": "Mutable" }, - "OwnershipVerificationCertificateArn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-domainname-domainnameconfiguration.html#cfn-apigatewayv2-domainname-domainnameconfiguration-ownershipverificationcertificatearn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "SecurityPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-domainname-domainnameconfiguration.html#cfn-apigatewayv2-domainname-domainnameconfiguration-securitypolicy", "PrimitiveType": "String", @@ -12810,6 +12804,63 @@ } } }, + "AWS::CodeDeploy::DeploymentConfig.TimeBasedCanary": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedcanary.html", + "Properties": { + "CanaryInterval": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedcanary.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedcanary-canaryinterval", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "CanaryPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedcanary.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedcanary-canarypercentage", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::CodeDeploy::DeploymentConfig.TimeBasedLinear": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedlinear.html", + "Properties": { + "LinearInterval": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedlinear.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedlinear-linearinterval", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "LinearPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedlinear.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedlinear-linearpercentage", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::CodeDeploy::DeploymentConfig.TrafficRoutingConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-trafficroutingconfig.html", + "Properties": { + "TimeBasedCanary": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-trafficroutingconfig.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedcanary", + "Required": false, + "Type": "TimeBasedCanary", + "UpdateType": "Mutable" + }, + "TimeBasedLinear": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-trafficroutingconfig.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedlinear", + "Required": false, + "Type": "TimeBasedLinear", + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-trafficroutingconfig.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.Alarm": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-alarm.html", "Properties": { @@ -12865,6 +12916,46 @@ } } }, + "AWS::CodeDeploy::DeploymentGroup.BlueGreenDeploymentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-bluegreendeploymentconfiguration.html", + "Properties": { + "DeploymentReadyOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-bluegreendeploymentconfiguration.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-deploymentreadyoption", + "Required": false, + "Type": "DeploymentReadyOption", + "UpdateType": "Mutable" + }, + "GreenFleetProvisioningOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-bluegreendeploymentconfiguration.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-greenfleetprovisioningoption", + "Required": false, + "Type": "GreenFleetProvisioningOption", + "UpdateType": "Mutable" + }, + "TerminateBlueInstancesOnDeploymentSuccess": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-bluegreendeploymentconfiguration.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-terminateblueinstancesondeploymentsuccess", + "Required": false, + "Type": "BlueInstanceTerminationOption", + "UpdateType": "Mutable" + } + } + }, + "AWS::CodeDeploy::DeploymentGroup.BlueInstanceTerminationOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-blueinstanceterminationoption.html", + "Properties": { + "Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-blueinstanceterminationoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-blueinstanceterminationoption-action", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "TerminationWaitTimeInMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-blueinstanceterminationoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-blueinstanceterminationoption-terminationwaittimeinminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.Deployment": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deployment.html", "Properties": { @@ -12888,6 +12979,23 @@ } } }, + "AWS::CodeDeploy::DeploymentGroup.DeploymentReadyOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deploymentreadyoption.html", + "Properties": { + "ActionOnTimeout": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deploymentreadyoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-deploymentreadyoption-actionontimeout", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "WaitTimeInMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deploymentreadyoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-deploymentreadyoption-waittimeinminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.DeploymentStyle": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deploymentstyle.html", "Properties": { @@ -12954,6 +13062,23 @@ } } }, + "AWS::CodeDeploy::DeploymentGroup.ECSService": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-ecsservice.html", + "Properties": { + "ClusterName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-ecsservice.html#cfn-codedeploy-deploymentgroup-ecsservice-clustername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ServiceName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-ecsservice.html#cfn-codedeploy-deploymentgroup-ecsservice-servicename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.ELBInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-elbinfo.html", "Properties": { @@ -12982,6 +13107,17 @@ } } }, + "AWS::CodeDeploy::DeploymentGroup.GreenFleetProvisioningOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-greenfleetprovisioningoption.html", + "Properties": { + "Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-greenfleetprovisioningoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-greenfleetprovisioningoption-action", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.LoadBalancerInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-loadbalancerinfo.html", "Properties": { @@ -16485,6 +16621,64 @@ } } }, + "AWS::DataBrew::Job.DataCatalogOutput": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html", + "Properties": { + "CatalogId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-catalogid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DatabaseName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-databasename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "DatabaseOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-databaseoptions", + "Required": false, + "Type": "DatabaseTableOutputOptions", + "UpdateType": "Mutable" + }, + "Overwrite": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-overwrite", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "S3Options": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-s3options", + "Required": false, + "Type": "S3TableOutputOptions", + "UpdateType": "Mutable" + }, + "TableName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-tablename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::DataBrew::Job.DatabaseTableOutputOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-databasetableoutputoptions.html", + "Properties": { + "TableName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-databasetableoutputoptions.html#cfn-databrew-job-databasetableoutputoptions-tablename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "TempDirectory": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-databasetableoutputoptions.html#cfn-databrew-job-databasetableoutputoptions-tempdirectory", + "Required": false, + "Type": "S3Location", + "UpdateType": "Mutable" + } + } + }, "AWS::DataBrew::Job.JobSample": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-jobsample.html", "Properties": { @@ -16607,6 +16801,17 @@ } } }, + "AWS::DataBrew::Job.S3TableOutputOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-s3tableoutputoptions.html", + "Properties": { + "Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-s3tableoutputoptions.html#cfn-databrew-job-s3tableoutputoptions-location", + "Required": true, + "Type": "S3Location", + "UpdateType": "Mutable" + } + } + }, "AWS::DataBrew::Project.Sample": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-project-sample.html", "Properties": { @@ -57973,6 +58178,72 @@ } } }, + "AWS::ServiceDiscovery::PrivateDnsNamespace.PrivateDnsPropertiesMutable": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-privatednspropertiesmutable.html", + "Properties": { + "SOA": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-privatednspropertiesmutable.html#cfn-servicediscovery-privatednsnamespace-privatednspropertiesmutable-soa", + "Required": false, + "Type": "SOA", + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PrivateDnsNamespace.Properties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-properties.html", + "Properties": { + "DnsProperties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-properties.html#cfn-servicediscovery-privatednsnamespace-properties-dnsproperties", + "Required": false, + "Type": "PrivateDnsPropertiesMutable", + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PrivateDnsNamespace.SOA": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-soa.html", + "Properties": { + "TTL": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-soa.html#cfn-servicediscovery-privatednsnamespace-soa-ttl", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PublicDnsNamespace.Properties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-properties.html", + "Properties": { + "DnsProperties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-properties.html#cfn-servicediscovery-publicdnsnamespace-properties-dnsproperties", + "Required": false, + "Type": "PublicDnsPropertiesMutable", + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PublicDnsNamespace.PublicDnsPropertiesMutable": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-publicdnspropertiesmutable.html", + "Properties": { + "SOA": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-publicdnspropertiesmutable.html#cfn-servicediscovery-publicdnsnamespace-publicdnspropertiesmutable-soa", + "Required": false, + "Type": "SOA", + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PublicDnsNamespace.SOA": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-soa.html", + "Properties": { + "TTL": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-soa.html#cfn-servicediscovery-publicdnsnamespace-soa-ttl", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::ServiceDiscovery::Service.DnsConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-service-dnsconfig.html", "Properties": { @@ -60661,7 +60932,7 @@ } } }, - "ResourceSpecificationVersion": "39.3.0", + "ResourceSpecificationVersion": "39.5.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -60959,7 +61230,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amazonmq-broker.html#cfn-amazonmq-broker-maintenancewindowstarttime", "Required": false, "Type": "MaintenanceWindow", - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "PubliclyAccessible": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amazonmq-broker.html#cfn-amazonmq-broker-publiclyaccessible", @@ -67907,12 +68178,26 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-application.html#cfn-codedeploy-application-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" } } }, "AWS::CodeDeploy::DeploymentConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentconfig.html", "Properties": { + "ComputePlatform": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentconfig.html#cfn-codedeploy-deploymentconfig-computeplatform", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "DeploymentConfigName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentconfig.html#cfn-codedeploy-deploymentconfig-deploymentconfigname", "PrimitiveType": "String", @@ -67924,6 +68209,12 @@ "Required": false, "Type": "MinimumHealthyHosts", "UpdateType": "Immutable" + }, + "TrafficRoutingConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentconfig.html#cfn-codedeploy-deploymentconfig-trafficroutingconfig", + "Required": false, + "Type": "TrafficRoutingConfig", + "UpdateType": "Immutable" } } }, @@ -67956,6 +68247,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "BlueGreenDeploymentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentgroup.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration", + "Required": false, + "Type": "BlueGreenDeploymentConfiguration", + "UpdateType": "Mutable" + }, "Deployment": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentgroup.html#cfn-codedeploy-deploymentgroup-deployment", "Required": false, @@ -67980,6 +68277,14 @@ "Type": "DeploymentStyle", "UpdateType": "Mutable" }, + "ECSServices": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentgroup.html#cfn-codedeploy-deploymentgroup-ecsservices", + "DuplicatesAllowed": false, + "ItemType": "ECSService", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "Ec2TagFilters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentgroup.html#cfn-codedeploy-deploymentgroup-ec2tagfilters", "DuplicatesAllowed": false, @@ -70434,6 +70739,13 @@ "AWS::DataBrew::Job": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html", "Properties": { + "DataCatalogOutputs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html#cfn-databrew-job-datacatalogoutputs", + "ItemType": "DataCatalogOutput", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "DatasetName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html#cfn-databrew-job-datasetname", "PrimitiveType": "String", @@ -96768,7 +97080,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-httpnamespace.html#cfn-servicediscovery-httpnamespace-description", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-httpnamespace.html#cfn-servicediscovery-httpnamespace-name", @@ -96823,7 +97135,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-description", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-name", @@ -96831,6 +97143,12 @@ "Required": true, "UpdateType": "Immutable" }, + "Properties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-properties", + "Required": false, + "Type": "Properties", + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-tags", "ItemType": "Tag", @@ -96861,7 +97179,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-publicdnsnamespace.html#cfn-servicediscovery-publicdnsnamespace-description", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-publicdnsnamespace.html#cfn-servicediscovery-publicdnsnamespace-name", @@ -96869,6 +97187,12 @@ "Required": true, "UpdateType": "Immutable" }, + "Properties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-publicdnsnamespace.html#cfn-servicediscovery-publicdnsnamespace-properties", + "Required": false, + "Type": "Properties", + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-publicdnsnamespace.html#cfn-servicediscovery-publicdnsnamespace-tags", "ItemType": "Tag", diff --git a/packages/@aws-cdk/cloud-assembly-schema/package.json b/packages/@aws-cdk/cloud-assembly-schema/package.json index f4ea4f798a06b..98689e858a5f8 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/package.json +++ b/packages/@aws-cdk/cloud-assembly-schema/package.json @@ -60,9 +60,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/mock-fs": "^4.13.0", - "@types/semver": "^7.3.6", + "@types/jest": "^26.0.24", + "@types/mock-fs": "^4.13.1", + "@types/semver": "^7.3.7", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "mock-fs": "^4.14.0", diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index c34b91e1cff3b..1fe5f51e907de 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -32,7 +32,7 @@ "table": "^6.7.1" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/string-width": "^4.0.1", "cdk-build-tools": "0.0.0", "fast-check": "^2.17.0", diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 926dd3d13b5d0..94c805c24cafd 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -400,7 +400,7 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 9abf3365cc3d2..80033471fbdc5 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -169,11 +169,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/lodash": "^4.14.170", - "@types/minimatch": "^3.0.4", + "@types/aws-lambda": "^8.10.78", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/lodash": "^4.14.171", + "@types/minimatch": "^3.0.5", "@types/node": "^10.17.60", "@types/sinon": "^9.0.11", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 1eccaf9f810aa..67f181d8844f6 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,12 +73,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", - "@types/aws-lambda": "^8.10.77", - "@types/fs-extra": "^8.1.1", + "@types/aws-lambda": "^8.10.78", + "@types/fs-extra": "^8.1.2", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", @@ -86,7 +86,7 @@ "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "fs-extra": "^9.1.0", - "nock": "^13.1.0", + "nock": "^13.1.1", "pkglint": "0.0.0", "sinon": "^9.2.4", "@aws-cdk/assert-internal": "0.0.0" diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index 129d89f96e4a2..8dd8ed3b24ff5 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -66,9 +66,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/mock-fs": "^4.13.0", - "@types/semver": "^7.3.6", + "@types/jest": "^26.0.24", + "@types/mock-fs": "^4.13.1", + "@types/semver": "^7.3.7", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "mock-fs": "^4.14.0", diff --git a/packages/@aws-cdk/example-construct-library/package.json b/packages/@aws-cdk/example-construct-library/package.json index a0f66c55abeeb..9eb248eba276e 100644 --- a/packages/@aws-cdk/example-construct-library/package.json +++ b/packages/@aws-cdk/example-construct-library/package.json @@ -66,7 +66,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/lambda-layer-awscli/package.json b/packages/@aws-cdk/lambda-layer-awscli/package.json index 896310c5a8842..582ff94d18b4f 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/package.json +++ b/packages/@aws-cdk/lambda-layer-awscli/package.json @@ -66,7 +66,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/lambda-layer-kubectl/package.json b/packages/@aws-cdk/lambda-layer-kubectl/package.json index d02b93e717a39..514cc7071c299 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/package.json +++ b/packages/@aws-cdk/lambda-layer-kubectl/package.json @@ -66,7 +66,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/pipelines/ORIGINAL_API.md b/packages/@aws-cdk/pipelines/ORIGINAL_API.md new file mode 100644 index 0000000000000..3f1bd5920bcd2 --- /dev/null +++ b/packages/@aws-cdk/pipelines/ORIGINAL_API.md @@ -0,0 +1,498 @@ +# CDK Pipelines, original API + +This document describes the API the CDK Pipelines library originally went into +Developer Preview with. The API has since been reworked, but the original one +left in place because of popular uptake. The original API still works and is +still supported, but the revised one is preferred for future projects. + +## Definining the pipeline + +In the original API, you have to import the `aws-codepipeline` construct +library and create `Artifact` objects for the source and Cloud Assembly +artifacts: + +```ts +import { Construct, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; +import { CdkPipeline } from '@aws-cdk/pipelines'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; + +/** + * Stack to hold the pipeline + */ +class MyPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact(); + + const pipeline = new CdkPipeline(this, 'Pipeline', { + cloudAssemblyArtifact, + + sourceAction: new codepipeline_actions.GitHubSourceAction({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), + // Replace these with your actual GitHub project name + owner: 'OWNER', + repo: 'REPO', + branch: 'main', // default: 'master' + }), + + synthAction: SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + + // Use this if you need a build step (if you're not using ts-node + // or if you have TypeScript Lambdas that need to be compiled). + buildCommand: 'npm run build', + }), + }); + + // Do this as many times as necessary with any account and region + // Account and region may different from the pipeline's. + pipeline.addApplicationStage(new MyApplication(this, 'Prod', { + env: { + account: '123456789012', + region: 'eu-west-1', + } + })); + } +} +``` + +### A note on cost + +By default, the `CdkPipeline` construct creates an AWS Key Management Service +(AWS KMS) Customer Master Key (CMK) for you to encrypt the artifacts in the +artifact bucket, which incurs a cost of +**$1/month**. This default configuration is necessary to allow cross-account +deployments. + +If you do not intend to perform cross-account deployments, you can disable +the creation of the Customer Master Keys by passing `crossAccountKeys: false` +when defining the Pipeline: + +```ts +const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { + crossAccountKeys: false, + + // ... +}); +``` + +### Defining the Pipeline (Source and Synth) + +The pipeline is defined by instantiating `CdkPipeline` in a Stack. This defines the +source location for the pipeline as well as the build commands. For example, the following +defines a pipeline whose source is stored in a GitHub repository, and uses NPM +to build. The Pipeline will be provisioned in account `111111111111` and region +`eu-west-1`: + +```ts +class MyPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact(); + + const pipeline = new CdkPipeline(this, 'Pipeline', { + pipelineName: 'MyAppPipeline', + cloudAssemblyArtifact, + + sourceAction: new codepipeline_actions.GitHubSourceAction({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), + // Replace these with your actual GitHub project name + owner: 'OWNER', + repo: 'REPO', + branch: 'main', // default: 'master' + }), + + synthAction: SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + + // Optionally specify a VPC in which the action runs + vpc: new ec2.Vpc(this, 'NpmSynthVpc'), + + // Use this if you need a build step (if you're not using ts-node + // or if you have TypeScript Lambdas that need to be compiled). + buildCommand: 'npm run build', + }), + }); + } +} + +const app = new App(); +new MyPipelineStack(app, 'PipelineStack', { + env: { + account: '111111111111', + region: 'eu-west-1', + } +}); +``` + +If you prefer more control over the underlying CodePipeline object, you can +create one yourself, including custom Source and Build stages: + +```ts +const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [...], + }, + { + stageName: 'CustomBuild', + actions: [...], + }, + ], +}); + +const app = new App(); +const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { + codePipeline, + cloudAssemblyArtifact, +}); +``` + +If you use assets for files or Docker images, every asset will get its own upload action during the asset stage. +By setting the value `singlePublisherPerType` to `true`, only one action for files and one action for +Docker images is created that handles all assets of the respective type. + +If you need to run commands to setup proxies, mirrors, etc you can supply them using the `assetPreInstallCommands`. + +#### Sources + +Any of the regular sources from the [`@aws-cdk/aws-codepipeline-actions`](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-codepipeline-actions-readme.html#github) module can be used. + +#### Synths + +You define how to build and synth the project by specifying a `synthAction`. +This can be any CodePipeline action that produces an artifact with a CDK +Cloud Assembly in it (the contents of the `cdk.out` directory created when +`cdk synth` is called). Pass the output artifact of the synth in the +Pipeline's `cloudAssemblyArtifact` property. + +`SimpleSynthAction` is available for synths that can be performed by running a couple +of simple shell commands (install, build, and synth) using AWS CodeBuild. When +using these, the source repository does not need to have a `buildspec.yml`. An example +of using `SimpleSynthAction` to run a Maven build followed by a CDK synth: + +```ts +const pipeline = new CdkPipeline(this, 'Pipeline', { + // ... + synthAction: new SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + installCommands: ['npm install -g aws-cdk'], + buildCommands: ['mvn package'], + synthCommand: 'cdk synth', + }) +}); +``` + +Available as factory functions on `SimpleSynthAction` are some common +convention-based synth: + +* `SimpleSynthAction.standardNpmSynth()`: build using NPM conventions. Expects a `package-lock.json`, + a `cdk.json`, and expects the CLI to be a versioned dependency in `package.json`. Does + not perform a build step by default. +* `CdkSynth.standardYarnSynth()`: build using Yarn conventions. Expects a `yarn.lock` + a `cdk.json`, and expects the CLI to be a versioned dependency in `package.json`. Does + not perform a build step by default. + +If you need a custom build/synth step that is not covered by `SimpleSynthAction`, you can +always add a custom CodeBuild project and pass a corresponding `CodeBuildAction` to the +pipeline. + +#### Add Additional permissions to the CodeBuild Project Role for building and synthesizing + +You can customize the role permissions used by the CodeBuild project so it has access to +the needed resources. eg: Adding CodeArtifact repo permissions so we pull npm packages +from the CA repo instead of NPM. + +```ts +class MyPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + ... + const pipeline = new CdkPipeline(this, 'Pipeline', { + ... + synthAction: SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + + // Use this to customize and a permissions required for the build + // and synth + rolePolicyStatements: [ + new PolicyStatement({ + actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], + resources: ['arn:codeartifact:repo:arn'], + }), + ], + + // Then you can login to codeartifact repository + // and npm will now pull packages from your repository + // Note the codeartifact login command requires more params to work. + buildCommands: [ + 'aws codeartifact login --tool npm', + 'npm run build', + ], + }), + }); + } +} +``` + +### Adding Application Stages + +To define an application that can be added to the pipeline integrally, define a subclass +of `Stage`. The `Stage` can contain one or more stack which make up your application. If +there are dependencies between the stacks, the stacks will automatically be added to the +pipeline in the right order. Stacks that don't depend on each other will be deployed in +parallel. You can add a dependency relationship between stacks by calling +`stack1.addDependency(stack2)`. + +Stages take a default `env` argument which the Stacks inside the Stage will fall back to +if no `env` is defined for them. + +An application is added to the pipeline by calling `addApplicationStage()` with instances +of the Stage. The same class can be instantiated and added to the pipeline multiple times +to define different stages of your DTAP or multi-region application pipeline: + +```ts +// Testing stage +pipeline.addApplicationStage(new MyApplication(this, 'Testing', { + env: { account: '111111111111', region: 'eu-west-1' } +})); + +// Acceptance stage +pipeline.addApplicationStage(new MyApplication(this, 'Acceptance', { + env: { account: '222222222222', region: 'eu-west-1' } +})); + +// Production stage +pipeline.addApplicationStage(new MyApplication(this, 'Production', { + env: { account: '333333333333', region: 'eu-west-1' } +})); +``` + +> Be aware that adding new stages via `addApplicationStage()` will +> automatically add them to the pipeline and deploy the new stacks, but +> *removing* them from the pipeline or deleting the pipeline stack will not +> automatically delete deployed application stacks. You must delete those +> stacks by hand using the AWS CloudFormation console or the AWS CLI. + +### More Control + +Every *Application Stage* added by `addApplicationStage()` will lead to the addition of +an individual *Pipeline Stage*, which is subsequently returned. You can add more +actions to the stage by calling `addAction()` on it. For example: + +```ts +const testingStage = pipeline.addApplicationStage(new MyApplication(this, 'Testing', { + env: { account: '111111111111', region: 'eu-west-1' } +})); + +// Add a action -- in this case, a Manual Approval action +// (for illustration purposes: testingStage.addManualApprovalAction() is a +// convenience shorthand that does the same) +testingStage.addAction(new ManualApprovalAction({ + actionName: 'ManualApproval', + runOrder: testingStage.nextSequentialRunOrder(), +})); +``` + +You can also add more than one *Application Stage* to one *Pipeline Stage*. For example: + +```ts +// Create an empty pipeline stage +const testingStage = pipeline.addStage('Testing'); + +// Add two application stages to the same pipeline stage +testingStage.addApplication(new MyApplication1(this, 'MyApp1', { + env: { account: '111111111111', region: 'eu-west-1' } +})); +testingStage.addApplication(new MyApplication2(this, 'MyApp2', { + env: { account: '111111111111', region: 'eu-west-1' } +})); +``` + +Even more, adding a manual approval action or reserving space for some extra sequential actions +between 'Prepare' and 'Execute' ChangeSet actions is possible. + +```ts + pipeline.addApplicationStage(new MyApplication(this, 'Production'), { + manualApprovals: true, + extraRunOrderSpace: 1, + }); +``` + +### Adding validations to the pipeline + +You can add any type of CodePipeline Action to the pipeline in order to validate +the deployments you are performing. + +The CDK Pipelines construct library comes with a `ShellScriptAction` which uses AWS CodeBuild +to run a set of shell commands (potentially running a test set that comes with your application, +using stack outputs of the deployed stacks). + +In its simplest form, adding validation actions looks like this: + +```ts +const stage = pipeline.addApplicationStage(new MyApplication(/* ... */)); + +stage.addActions(new ShellScriptAction({ + actionName: 'MyValidation', + commands: ['curl -Ssf https://my.webservice.com/'], + // Optionally specify a VPC if, for example, the service is deployed with a private load balancer + vpc, + // Optionally specify SecurityGroups + securityGroups, + // Optionally specify a BuildEnvironment + environment, +})); +``` + +#### Using CloudFormation Stack Outputs in ShellScriptAction + +Because many CloudFormation deployments result in the generation of resources with unpredictable +names, validations have support for reading back CloudFormation Outputs after a deployment. This +makes it possible to pass (for example) the generated URL of a load balancer to the test set. + +To use Stack Outputs, expose the `CfnOutput` object you're interested in, and +call `pipeline.stackOutput()` on it: + +```ts +class MyLbApplication extends Stage { + public readonly loadBalancerAddress: CfnOutput; + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const lbStack = new LoadBalancerStack(this, 'Stack'); + + // Or create this in `LoadBalancerStack` directly + this.loadBalancerAddress = new CfnOutput(lbStack, 'LbAddress', { + value: `https://${lbStack.loadBalancer.loadBalancerDnsName}/` + }); + } +} + +const lbApp = new MyLbApplication(this, 'MyApp', { + env: { /* ... */ } +}); +const stage = pipeline.addApplicationStage(lbApp); +stage.addActions(new ShellScriptAction({ + // ... + useOutputs: { + // When the test is executed, this will make $URL contain the + // load balancer address. + URL: pipeline.stackOutput(lbApp.loadBalancerAddress), + } +}); +``` + +#### Using additional files in Shell Script Actions + +As part of a validation, you probably want to run a test suite that's more +elaborate than what can be expressed in a couple of lines of shell script. +You can bring additional files into the shell script validation by supplying +the `additionalArtifacts` property. + +Here are some typical examples for how you might want to bring in additional +files from several sources: + +* Directory from the source repository +* Additional compiled artifacts from the synth step + +#### Controlling IAM permissions + +IAM permissions can be added to the execution role of a `ShellScriptAction` in +two ways. + +Either pass additional policy statements in the `rolePolicyStatements` property: + +```ts +new ShellScriptAction({ + // ... + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['s3:GetObject'], + resources: ['*'], + }), + ], +})); +``` + +The Action can also be used as a Grantable after having been added to a Pipeline: + +```ts +const action = new ShellScriptAction({ /* ... */ }); +pipeline.addStage('Test').addActions(action); + +bucket.grantRead(action); +``` + +#### Additional files from the source repository + +Bringing in additional files from the source repository is appropriate if the +files in the source repository are directly usable in the test (for example, +if they are executable shell scripts themselves). Pass the `sourceArtifact`: + +```ts +const sourceArtifact = new codepipeline.Artifact(); + +const pipeline = new CdkPipeline(this, 'Pipeline', { + // ... +}); + +const validationAction = new ShellScriptAction({ + actionName: 'TestUsingSourceArtifact', + additionalArtifacts: [sourceArtifact], + + // 'test.sh' comes from the source repository + commands: ['./test.sh'], +}); +``` + +#### Additional files from the synth step + +Getting the additional files from the synth step is appropriate if your +tests need the compilation step that is done as part of synthesis. + +On the synthesis step, specify `additionalArtifacts` to package +additional subdirectories into artifacts, and use the same artifact +in the `ShellScriptAction`'s `additionalArtifacts`: + +```ts +// If you are using additional output artifacts from the synth step, +// they must be named. +const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); +const integTestsArtifact = new codepipeline.Artifact('IntegTests'); + +const pipeline = new CdkPipeline(this, 'Pipeline', { + synthAction: SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + buildCommands: ['npm run build'], + additionalArtifacts: [ + { + directory: 'test', + artifact: integTestsArtifact, + } + ], + }), + // ... +}); + +const validationAction = new ShellScriptAction({ + actionName: 'TestUsingBuildArtifact', + additionalArtifacts: [integTestsArtifact], + // 'test.js' was produced from 'test/test.ts' during the synth step + commands: ['node ./test.js'], +}); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 6e4f73c895b1f..76a8c3a84f8fb 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -17,19 +17,36 @@ A construct library for painless Continuous Delivery of CDK applications. -![Developer Preview](https://img.shields.io/badge/developer--preview-informational.svg?style=for-the-badge) - -> This module is in **developer preview**. We may make breaking changes to address unforeseen API issues. Therefore, these APIs are not subject to [Semantic Versioning](https://semver.org/), and breaking changes will be announced in release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. +> This module contains two sets of APIs: an **original** and a **modern** version of +CDK Pipelines. The *modern* API has been updated to be easier to work with and +customize, and will be the preferred API going forward. The *original* version +of the API is still available for backwards compatibility, but we recommend migrating +to the new version if possible. +> +> Compared to the original API, the modern API: has more sensible defaults; is +> more flexible; supports parallel deployments; supports multiple synth inputs; +> allows more control of CodeBuild project generation; supports deployment +> engines other than CodePipeline. +> +> The README for the original API can be found in [our GitHub repository](https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/pipelines/ORIGINAL_API.md). ## At a glance -Defining a pipeline for your application is as simple as defining a subclass -of `Stage`, and calling `pipeline.addApplicationStage()` with instances of -that class. Deploying to a different account or region looks exactly the -same, the *CDK Pipelines* library takes care of the details. +Deploying your application continuously starts by defining a +`MyApplicationStage`, a subclass of `Stage` that contains the stacks that make +up a single copy of your application. -(Note that have to *bootstrap* all environments before the following code -will work, see the section **CDK Environment Bootstrapping** below). +You then define a `Pipeline`, instantiate as many instances of +`MyApplicationStage` as you want for your test and production environments, with +different parameters for each, and calling `pipeline.addStage()` for each of +them. You can deploy to the same account and Region, or to a different one, +with the same amount of code. The *CDK Pipelines* library takes care of the +details. + +CDK Pipelines supports multiple *deployment engines* (see below), and comes with +a deployment engine that deployes CDK apps using AWS CodePipeline. To use the +CodePipeline engine, define a `CodePipeline` construct. The following +example creates a CodePipeline that deploys an application from GitHub: ```ts /** The stacks for our app are defined in my-stacks.ts. The internals of these @@ -38,10 +55,42 @@ will work, see the section **CDK Environment Bootstrapping** below). * to this table in its properties. */ import { DatabaseStack, ComputeStack } from '../lib/my-stacks'; - import { Construct, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; -import { CdkPipeline } from '@aws-cdk/pipelines'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import { CodePipeline, CodePipelineSource, ShellStep } from '@aws-cdk/pipelines'; + +/** + * Stack to hold the pipeline + */ +class MyPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new CodePipeline(this, 'Pipeline', { + synth: new ShellStep('Synth', { + // Use a connection created using the AWS console to authenticate to GitHub + // Other sources are available. + input: CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), + }); + + // 'MyApplication' is defined below. Call `addStage` as many times as + // necessary with any account and region (may be different from the + // pipeline's). + pipeline.addStage(new MyApplication(this, 'Prod', { + env: { + account: '123456789012', + region: 'eu-west-1', + } + })); + } +} /** * Your application @@ -62,30 +111,13 @@ class MyApplication extends Stage { } } -/** - * Stack to hold the pipeline - */ -class MyPipelineStack extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); - - const sourceArtifact = new codepipeline.Artifact(); - const cloudAssemblyArtifact = new codepipeline.Artifact(); - - const pipeline = new CdkPipeline(this, 'Pipeline', { - // ...source and build information here (see below) - }); - - // Do this as many times as necessary with any account and region - // Account and region may different from the pipeline's. - pipeline.addApplicationStage(new MyApplication(this, 'Prod', { - env: { - account: '123456789012', - region: 'eu-west-1', - } - })); +// In your main file +new MyPipelineStack(app, 'PipelineStack', { + env: { + account: '123456789012', + region: 'eu-west-1', } -} +}); ``` The pipeline is **self-mutating**, which means that if you add new @@ -93,10 +125,13 @@ application stages in the source code, or new stacks to `MyApplication`, the pipeline will automatically reconfigure itself to deploy those new stages and stacks. +(Note that have to *bootstrap* all environments before the above code +will work, see the section **CDK Environment Bootstrapping** below). + ## CDK Versioning -This library uses prerelease features of the CDK framework, which can be enabled by adding the -following to `cdk.json`: +This library uses prerelease features of the CDK framework, which can be enabled +by adding the following to `cdk.json`: ```js { @@ -107,484 +142,538 @@ following to `cdk.json`: } ``` -## A note on cost +## Provisioning the pipeline -By default, the `CdkPipeline` construct creates an AWS Key Management Service -(AWS KMS) Customer Master Key (CMK) for you to encrypt the artifacts in the -artifact bucket, which incurs a cost of -**$1/month**. This default configuration is necessary to allow cross-account -deployments. +To provision the pipeline you have defined, making sure the target environment +has been bootstrapped (see below), and then executing deploying the +`PipelineStack` *once*. Afterwards, the pipeline will keep itself up-to-date. -If you do not intend to perform cross-account deployments, you can disable -the creation of the Customer Master Keys by passing `crossAccountKeys: false` -when defining the Pipeline: +> **Important**: be sure to `git commit` and `git push` before deploying the +> Pipeline stack using `cdk deploy`! +> +> The reason is that the pipeline will start deploying and self-mutating +> right away based on the sources in the repository, so the sources it finds +> in there should be the ones you want it to find. -```ts -const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { - crossAccountKeys: false, +Run the following commands to get the pipeline going: - // ... -}); +```console +$ git commit -a +$ git push +$ cdk deploy PipelineStack ``` -## Defining the Pipeline (Source and Synth) +Administrative permissions to the account are only necessary up until +this point. We recommend you shed access to these credentials after doing this. + +### Working on the pipeline + +The self-mutation feature of the Pipeline might at times get in the way +of the pipeline development workflow. Each change to the pipeline must be pushed +to git, otherwise, after the pipeline was updated using `cdk deploy`, it will +automatically revert to the state found in git. -The pipeline is defined by instantiating `CdkPipeline` in a Stack. This defines the -source location for the pipeline as well as the build commands. For example, the following -defines a pipeline whose source is stored in a GitHub repository, and uses NPM -to build. The Pipeline will be provisioned in account `111111111111` and region -`eu-west-1`: +To make the development more convenient, the self-mutation feature can be turned +off temporarily, by passing `selfMutation: false` property, example: ```ts -class MyPipelineStack extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); +// Modern API +const pipeline = new CodePipeline(this, 'Pipeline', { + selfMutation: false, + ... +}); - const sourceArtifact = new codepipeline.Artifact(); - const cloudAssemblyArtifact = new codepipeline.Artifact(); - - const pipeline = new CdkPipeline(this, 'Pipeline', { - pipelineName: 'MyAppPipeline', - cloudAssemblyArtifact, - - sourceAction: new codepipeline_actions.GitHubSourceAction({ - actionName: 'GitHub', - output: sourceArtifact, - oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), - // Replace these with your actual GitHub project name - owner: 'OWNER', - repo: 'REPO', - branch: 'main', // default: 'master' - }), +// Original API +const pipeline = new CdkPipeline(this, 'Pipeline', { + selfMutating: false, + ... +}); +``` - synthAction: SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, +## Definining the pipeline - // Optionally specify a VPC in which the action runs - vpc: new ec2.Vpc(this, 'NpmSynthVpc'), +This section of the documentation describes the AWS CodePipeline engine, which +comes with this library. If you want to use a different deployment engine, read +the section *Using a different deployment engine* below. - // Use this if you need a build step (if you're not using ts-node - // or if you have TypeScript Lambdas that need to be compiled). - buildCommand: 'npm run build', - }), - }); - } -} +### Synth and sources -const app = new App(); -new MyPipelineStack(app, 'PipelineStack', { - env: { - account: '111111111111', - region: 'eu-west-1', - } -}); -``` +To define a pipeline, instantiate a `CodePipeline` construct from the +`@aws-cdk/pipelines` module. It takes one argument, a `synth` step, which is +expected to produce the CDK Cloud Assembly as its single output (the contents of +the `cdk.out` directory after running `cdk synth`). "Steps" are arbitrary +actions in the pipeline, typically used to run scripts or commands. -If you prefer more control over the underlying CodePipeline object, you can -create one yourself, including custom Source and Build stages: +For the synth, use a `ShellStep` and specify the commands necessary to build +your project and run `cdk synth`; the specific commands required will depend on +the programming language you are using. For a typical NPM-based project, the synth +will look like this: ```ts -const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { - stages: [ - { - stageName: 'CustomSource', - actions: [...], - }, - { - stageName: 'CustomBuild', - actions: [...], - }, - ], +const source = /* the repository source */; + +const pipeline = new CodePipeline(this, 'Pipeline', { + synth: new ShellStep('Synth', { + input: source, + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), }); +``` -const app = new App(); -const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { - codePipeline, - cloudAssemblyArtifact, +The pipeline assumes that your `ShellStep` will produce a `cdk.out` +directory in the root, containing the CDK cloud assembly. If your +CDK project lives in a subdirectory, be sure to adjust the +`primaryOutputDirectory` to match: + +```ts +const pipeline = new CodePipeline(this, 'Pipeline', { + synth: new ShellStep('Synth', { + input: source, + commands: [ + 'cd mysubdir', + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + primaryOutputDirectory: 'mysubdir/cdk.out', + }), }); ``` -If you use assets for files or Docker images, every asset will get its own upload action during the asset stage. -By setting the value `singlePublisherPerType` to `true`, only one action for files and one action for -Docker images is created that handles all assets of the respective type. +The underlying `@aws-cdk/aws-codepipeline.Pipeline` construct will be produced +when `app.synth()` is called. You can also force it to be produced +earlier by calling `pipeline.buildPipeline()`. After you've called +that method, you can inspect the constructs that were produced by +accessing the properties of the `pipeline` object. -If you need to run commands to setup proxies, mirrors, etc you can supply them using the `assetPreInstallCommands`. +#### CodePipeline Sources -## Initial pipeline deployment +In CodePipeline, *Sources* define where the source of your application lives. +When a change to the source is detected, the pipeline will start executing. +Source objects can be created by factory methods on the `CodePipelineSource` class: -You provision this pipeline by making sure the target environment has been -bootstrapped (see below), and then executing deploying the `PipelineStack` -*once*. Afterwards, the pipeline will keep itself up-to-date. - -> **Important**: be sure to `git commit` and `git push` before deploying the -> Pipeline stack using `cdk deploy`! -> -> The reason is that the pipeline will start deploying and self-mutating -> right away based on the sources in the repository, so the sources it finds -> in there should be the ones you want it to find. +##### GitHub, GitHub Enterprise, BitBucket using a connection -Run the following commands to get the pipeline going: +The recommended way of connecting to GitHub or BitBucket is by using a *connection*. +You will first use the AWS Console to authenticate to the source control +provider, and then use the connection ARN in your pipeline definition: -```console -$ git commit -a -$ git push -$ cdk deploy PipelineStack +```ts +CodePipelineSource.connection('org/repo', 'branch', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', +}); ``` -Administrative permissions to the account are only necessary up until -this point. We recommend you shed access to these credentials after doing this. +##### GitHub using OAuth -### Sources +You can also authenticate to GitHub using a personal access token. This expects +that you've created a personal access token and stored it in Secrets Manager. +By default, the source object will look for a secret named **github-token**, but +you can change the name. The token should have the **repo** and **admin:repo_hook** +scopes. -Any of the regular sources from the [`@aws-cdk/aws-codepipeline-actions`](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-codepipeline-actions-readme.html#github) module can be used. - -### Synths +```ts +CodePipelineSource.gitHub('org/repo', 'branch', { + // This is optional + authentication: SecretValue.secretsManager('my-token'), +}); +``` -You define how to build and synth the project by specifying a `synthAction`. -This can be any CodePipeline action that produces an artifact with a CDK -Cloud Assembly in it (the contents of the `cdk.out` directory created when -`cdk synth` is called). Pass the output artifact of the synth in the -Pipeline's `cloudAssemblyArtifact` property. +##### CodeCommit -`SimpleSynthAction` is available for synths that can be performed by running a couple -of simple shell commands (install, build, and synth) using AWS CodeBuild. When -using these, the source repository does not need to have a `buildspec.yml`. An example -of using `SimpleSynthAction` to run a Maven build followed by a CDK synth: +You can use a CodeCommit repository as the source. Either create or import +that the CodeCommit repository and then use `CodePipelineSource.codeCommit` +to reference it: ```ts -const pipeline = new CdkPipeline(this, 'Pipeline', { - // ... - synthAction: new SimpleSynthAction({ - sourceArtifact, - cloudAssemblyArtifact, - installCommands: ['npm install -g aws-cdk'], - buildCommands: ['mvn package'], - synthCommand: 'cdk synth', - }) -}); +const repository = codecommit.fromRepositoryName(this, 'Repository', 'my-repository'); +CodePipelineSource.codeCommit(repository); ``` -Available as factory functions on `SimpleSynthAction` are some common -convention-based synth: +##### S3 -* `SimpleSynthAction.standardNpmSynth()`: build using NPM conventions. Expects a `package-lock.json`, - a `cdk.json`, and expects the CLI to be a versioned dependency in `package.json`. Does - not perform a build step by default. -* `CdkSynth.standardYarnSynth()`: build using Yarn conventions. Expects a `yarn.lock` - a `cdk.json`, and expects the CLI to be a versioned dependency in `package.json`. Does - not perform a build step by default. +You can use a zip file in S3 as the source of the pipeline. The pipeline will be +triggered every time the file in S3 is changed: -If you need a custom build/synth step that is not covered by `SimpleSynthAction`, you can -always add a custom CodeBuild project and pass a corresponding `CodeBuildAction` to the -pipeline. +```ts +const bucket = s3.Bucket.fromBucketName(this, 'Bucket', 'my-bucket'); +CodePipelineSource.s3(bucket, 'my/source.zip'); +``` -## Adding Application Stages +#### Additional inputs -To define an application that can be added to the pipeline integrally, define a subclass -of `Stage`. The `Stage` can contain one or more stack which make up your application. If -there are dependencies between the stacks, the stacks will automatically be added to the -pipeline in the right order. Stacks that don't depend on each other will be deployed in -parallel. You can add a dependency relationship between stacks by calling -`stack1.addDependency(stack2)`. +`ShellStep` allows passing in more than one input: additional +inputs will be placed in the directories you specify. Any step that produces an +output file set can be used as an input, such as a `CodePipelineSource`, but +also other `ShellStep`: -Stages take a default `env` argument which the Stacks inside the Stage will fall back to -if no `env` is defined for them. +```ts +const prebuild = new ShellStep('Prebuild', { + input: CodePipelineSource.gitHub('myorg/repo1'), + primaryOutputDirectory: './build', + commands: ['./build.sh'], +}); -An application is added to the pipeline by calling `addApplicationStage()` with instances -of the Stage. The same class can be instantiated and added to the pipeline multiple times -to define different stages of your DTAP or multi-region application pipeline: +const pipeline = new CodePipeline(this, 'Pipeline', { + synth: new ShellStep('Synth', { + input: CodePipelineSource.gitHub('myorg/repo2'), + additionalInputs: { + 'subdir': CodePipelineSource.gitHub('myorg/repo3'), + '../siblingdir': prebuild, + }, -```ts -// Testing stage -pipeline.addApplicationStage(new MyApplication(this, 'Testing', { - env: { account: '111111111111', region: 'eu-west-1' } -})); + commands: ['./build.sh'], + }) +}); +``` -// Acceptance stage -pipeline.addApplicationStage(new MyApplication(this, 'Acceptance', { - env: { account: '222222222222', region: 'eu-west-1' } -})); +### CDK application deployments -// Production stage -pipeline.addApplicationStage(new MyApplication(this, 'Production', { - env: { account: '333333333333', region: 'eu-west-1' } +After you have defined the pipeline and the `synth` step, you can add one or +more CDK `Stages` which will be deployed to their target environments. To do +so, call `pipeline.addStage()` on the Stage object: + +```ts +// Do this as many times as necessary with any account and region +// Account and region may different from the pipeline's. +pipeline.addStage(new MyApplicationStage(this, 'Prod', { + env: { + account: '123456789012', + region: 'eu-west-1', + } })); ``` -> Be aware that adding new stages via `addApplicationStage()` will -> automatically add them to the pipeline and deploy the new stacks, but -> *removing* them from the pipeline or deleting the pipeline stack will not -> automatically delete deployed application stacks. You must delete those -> stacks by hand using the AWS CloudFormation console or the AWS CLI. +CDK Pipelines will automatically discover all `Stacks` in the given `Stage` +object, determine their dependency order, and add appropriate actions to the +pipeline to publish the assets referenced in those stacks and deploy the stacks +in the right order. + +If the `Stacks` are targeted at an environment in a different AWS account or +Region and that environment has been +[bootstrapped](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html) +, CDK Pipelines will transparently make sure the IAM roles are set up +correctly and any requisite replication Buckets are created. -### More Control +#### Deploying in parallel -Every *Application Stage* added by `addApplicationStage()` will lead to the addition of -an individual *Pipeline Stage*, which is subsequently returned. You can add more -actions to the stage by calling `addAction()` on it. For example: +By default, all applications added to CDK Pipelines by calling `addStage()` will +be deployed in sequence, one after the other. If you have a lot of stages, you can +speed up the pipeline by choosing to deploy some stages in parallel. You do this +by calling `addWave()` instead of `addStage()`: a *wave* is a set of stages that +are all deployed in parallel instead of sequentially. Waves themselves are still +deployed in sequence. For example, the following will deploy two copies of your +application to `eu-west-1` and `eu-central-1` in parallel: ```ts -const testingStage = pipeline.addApplicationStage(new MyApplication(this, 'Testing', { - env: { account: '111111111111', region: 'eu-west-1' } +const europeWave = pipeline.addWave('Europe'); +europeWave.addStage(new MyApplicationStage(this, 'Ireland', { + env: { region: 'eu-west-1' } })); - -// Add a action -- in this case, a Manual Approval action -// (for illustration purposes: testingStage.addManualApprovalAction() is a -// convenience shorthand that does the same) -testingStage.addAction(new ManualApprovalAction({ - actionName: 'ManualApproval', - runOrder: testingStage.nextSequentialRunOrder(), +europeWave.addStage(new MyApplicationStage(this, 'Germany', { + env: { region: 'eu-central-1' } })); ``` -You can also add more than one *Application Stage* to one *Pipeline Stage*. For example: +#### Deploying to other accounts / encrypting the Artifact Bucket -```ts -// Create an empty pipeline stage -const testingStage = pipeline.addStage('Testing'); +CDK Pipelines can transparently deploy to other Regions and other accounts +(provided those target environments have been +[*bootstrapped*](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html)). +However, deploying to another account requires one additional piece of +configuration: you need to enable `crossAccountKeys: true` when creating the +pipeline. -// Add two application stages to the same pipeline stage -testingStage.addApplication(new MyApplication1(this, 'MyApp1', { - env: { account: '111111111111', region: 'eu-west-1' } -})); -testingStage.addApplication(new MyApplication2(this, 'MyApp2', { - env: { account: '111111111111', region: 'eu-west-1' } -})); -``` +This will encrypt the artifact bucket(s), but incurs a cost for maintaining the +KMS key. -Even more, adding a manual approval action or reserving space for some extra sequential actions -between 'Prepare' and 'Execute' ChangeSet actions is possible. +Example: ```ts - pipeline.addApplicationStage(new MyApplication(this, 'Production'), { - manualApprovals: true, - extraRunOrderSpace: 1, - }); +const pipeline = new CodePipeline(this, 'Pipeline', { + // Encrypt artifacts, required for cross-account deployments + crossAccountKeys: true, +}); ``` -## Adding validations to the pipeline - -You can add any type of CodePipeline Action to the pipeline in order to validate -the deployments you are performing. +### Validation -The CDK Pipelines construct library comes with a `ShellScriptAction` which uses AWS CodeBuild -to run a set of shell commands (potentially running a test set that comes with your application, -using stack outputs of the deployed stacks). +Every `addStage()` and `addWave()` command takes additional options. As part of these options, +you can specify `pre` and `post` steps, which are arbitrary steps that run before or after +the contents of the stage or wave, respectively. You can use these to add validations like +manual or automated gates to your pipeline. -In its simplest form, adding validation actions looks like this: +The following example shows both an automated approval in the form of a `ShellStep`, and +a manual approvel in the form of a `ManualApprovalStep` added to the pipeline. Both must +pass in order to promote from the `PreProd` to the `Prod` environment: ```ts -const stage = pipeline.addApplicationStage(new MyApplication(/* ... */)); - -stage.addActions(new ShellScriptAction({ - actionName: 'MyValidation', - commands: ['curl -Ssf https://my.webservice.com/'], - // Optionally specify a VPC if, for example, the service is deployed with a private load balancer - vpc, - // Optionally specify SecurityGroups - securityGroups, - // Optionally specify a BuildEnvironment - environment, -})); +const preprod = new MyApplicationStage(this, 'PreProd', { ... }); +const prod = new MyApplicationStage(this, 'Prod', { ... }); + +pipeline.addStage(preprod, { + post: [ + new ShellStep('Validate Endpoint', { + commands: ['curl -Ssf https://my.webservice.com/'], + }), + ], +}); +pipeline.addStage(prod, { + pre: [ + new ManualApprovalStep('PromoteToProd'), + ], +}); ``` -### Using CloudFormation Stack Outputs in ShellScriptAction +#### Using CloudFormation Stack Outputs in approvals Because many CloudFormation deployments result in the generation of resources with unpredictable names, validations have support for reading back CloudFormation Outputs after a deployment. This makes it possible to pass (for example) the generated URL of a load balancer to the test set. To use Stack Outputs, expose the `CfnOutput` object you're interested in, and -call `pipeline.stackOutput()` on it: +pass it to `envFromCfnOutputs` of the `ShellStep`: ```ts -class MyLbApplication extends Stage { +class MyApplicationStage extends Stage { public readonly loadBalancerAddress: CfnOutput; - - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - - const lbStack = new LoadBalancerStack(this, 'Stack'); - - // Or create this in `LoadBalancerStack` directly - this.loadBalancerAddress = new CfnOutput(lbStack, 'LbAddress', { - value: `https://${lbStack.loadBalancer.loadBalancerDnsName}/` - }); - } + // ... } -const lbApp = new MyLbApplication(this, 'MyApp', { - env: { /* ... */ } -}); -const stage = pipeline.addApplicationStage(lbApp); -stage.addActions(new ShellScriptAction({ - // ... - useOutputs: { - // When the test is executed, this will make $URL contain the - // load balancer address. - URL: pipeline.stackOutput(lbApp.loadBalancerAddress), - } +const lbApp = new MyApplicationStage(this, 'MyApp', { /* ... */ }); +pipeline.addStage(lbApp, { + post: [ + new ShellStep('HitEndpoint', { + envFromCfnOutputs: { + // Make the load balancer address available as $URL inside the commands + URL: lbApp.loadBalancerAddress, + }, + commands: ['curl -Ssf $URL'], + }); + ], }); ``` -### Using additional files in Shell Script Actions +#### Running scripts compiled during the synth step As part of a validation, you probably want to run a test suite that's more elaborate than what can be expressed in a couple of lines of shell script. You can bring additional files into the shell script validation by supplying -the `additionalArtifacts` property. +the `input` or `additionalInputs` property of `ShellStep`. The input can +be produced by the `Synth` step, or come from a source or any other build +step. -Here are some typical examples for how you might want to bring in additional -files from several sources: +Here's an example that captures an additional output directory in the synth +step and runs tests from there: -* Directory from the source repository -* Additional compiled artifacts from the synth step +```ts +const synth = new ShellStep('Synth', { /* ... */ }); +const pipeline = new CodePipeline(this, 'Pipeline', { synth }); -### Controlling IAM permissions +new ShellStep('Approve', { + // Use the contents of the 'integ' directory from the synth step as the input + input: synth.addOutputDirectory('integ'), + commands: ['cd integ && ./run.sh'], +}); +``` -IAM permissions can be added to the execution role of a `ShellScriptAction` in -two ways. +### Customizing CodeBuild Projects -Either pass additional policy statements in the `rolePolicyStatements` property: +CDK pipelines will generate CodeBuild projects for each `ShellStep` you use, and it +will also generate CodeBuild projects to publish assets and perform the self-mutation +of the pipeline. To control the various aspects of the CodeBuild projects that get +generated, use a `CodeBuildStep` instead of a `ShellStep`. This class has a number +of properties that allow you to customize various aspects of the projects: ```ts -new ShellScriptAction({ - // ... - rolePolicyStatements: [ - new iam.PolicyStatement({ - actions: ['s3:GetObject'], - resources: ['*'], - }), - ], -})); -``` +new CodeBuildStep('Synth', { + // ...standard RunScript props... + commands: [/* ... */], + env: { /* ... */ }, + + // If you are using a CodeBuildStep explicitly, set the 'cdk.out' directory + // to be the synth step's output. + primaryOutputDirectory: 'cdk.out', + + // Control the name of the project + projectName: 'MyProject', + + // Control parts of the BuildSpec other than the regular 'build' and 'install' commands + partialBuildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + // ... + }), -The Action can also be used as a Grantable after having been added to a Pipeline: + // Control the build environment + buildEnvironment: { + computeType: codebuild.ComputeType.LARGE, + }, -```ts -const action = new ShellScriptAction({ /* ... */ }); -pipeline.addStage('Test').addActions(action); + // Control Elastic Network Interface creation + vpc: vpc, + subnetSelection: { subnetType: ec2.SubnetType.PRIVATE }, + securityGroups: [mySecurityGroup], -bucket.grantRead(action); + // Additional policy statements for the execution role + rolePolicy: [ + new iam.PolicyStatement({ /* ... */ }), + ], +}); ``` -#### Additional files from the source repository - -Bringing in additional files from the source repository is appropriate if the -files in the source repository are directly usable in the test (for example, -if they are executable shell scripts themselves). Pass the `sourceArtifact`: +You can also configure defaults for *all* CodeBuild projects by passing `codeBuildDefaults`, +or just for the asset publishing and self-mutation projects by passing `assetPublishingCodeBuildDefaults` +or `selfMutationCodeBuildDefaults`: ```ts -const sourceArtifact = new codepipeline.Artifact(); - -const pipeline = new CdkPipeline(this, 'Pipeline', { +new CodePipeline(this, 'Pipeline', { // ... -}); -const validationAction = new ShellScriptAction({ - actionName: 'TestUsingSourceArtifact', - additionalArtifacts: [sourceArtifact], + // Defaults for all CodeBuild projects + codeBuildDefaults: { + // Prepend commands and configuration to all projects + partialBuildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + // ... + }), + + // Control the build environment + buildEnvironment: { + computeType: codebuild.ComputeType.LARGE, + }, - // 'test.sh' comes from the source repository - commands: ['./test.sh'], + // Control Elastic Network Interface creation + vpc: vpc, + subnetSelection: { subnetType: ec2.SubnetType.PRIVATE }, + securityGroups: [mySecurityGroup], + + // Additional policy statements for the execution role + rolePolicy: [ + new iam.PolicyStatement({ /* ... */ }), + ], + }, + + assetPublishingCodeBuildDefaults: { /* ... */ }, + selfMutationCodeBuildDefaults: { /* ... */ }, }); ``` -#### Additional files from the synth step +### Arbitrary CodePipeline actions -Getting the additional files from the synth step is appropriate if your -tests need the compilation step that is done as part of synthesis. +If you want to add a type of CodePipeline action to the CDK Pipeline that +doesn't have a matching class yet, you can define your own step class that extends +`Step` and implements `ICodePipelineActionFactory`. -On the synthesis step, specify `additionalArtifacts` to package -additional subdirectories into artifacts, and use the same artifact -in the `ShellScriptAction`'s `additionalArtifacts`: +Here's a simple example that adds a Jenkins step: ```ts -// If you are using additional output artifacts from the synth step, -// they must be named. -const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); -const integTestsArtifact = new codepipeline.Artifact('IntegTests'); +class MyJenkinsStep extends Step implements ICodePipelineActionFactory { + constructor(private readonly provider: codepipeline_actions.JenkinsProvider, private readonly input: FileSet) { + } -const pipeline = new CdkPipeline(this, 'Pipeline', { - synthAction: SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - buildCommands: ['npm run build'], - additionalArtifacts: [ - { - directory: 'test', - artifact: integTestsArtifact, - } - ], - }), - // ... -}); + public produceAction(stage: codepipeline.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { -const validationAction = new ShellScriptAction({ - actionName: 'TestUsingBuildArtifact', - additionalArtifacts: [integTestsArtifact], - // 'test.js' was produced from 'test/test.ts' during the synth step - commands: ['node ./test.js'], -}); + // This is where you control what type of Action gets added to the + // CodePipeline + stage.addAction(new codepipeline_actions.JenkinsAction({ + // Copy 'actionName' and 'runOrder' from the options + actionName: options.actionName, + runOrder: options.runOrder, + + // Jenkins-specific configuration + type: cpactions.JenkinsActionType.TEST, + jenkinsProvider: this.provider, + projectName: 'MyJenkinsProject', + + // Translate the FileSet into a codepipeline.Artifact + inputs: [options.artifacts.toCodePipeline(this.input)], + })); + + return { runOrdersConsumed: 1 }; + } +} ``` -#### Add Additional permissions to the CodeBuild Project Role for building and synthesizing +## Using Docker in the pipeline + +Docker can be used in 3 different places in the pipeline: + +* If you are using Docker image assets in your application stages: Docker will + run in the asset publishing projects. +* If you are using Docker image assets in your stack (for example as + images for your CodeBuild projects): Docker will run in the self-mutate project. +* If you are using Docker to bundle file assets anywhere in your project (for + example, if you are using such construct libraries as + `@aws-cdk/aws-lambda-nodejs`): Docker will run in the + *synth* project. -You can customize the role permissions used by the CodeBuild project so it has access to -the needed resources. eg: Adding CodeArtifact repo permissions so we pull npm packages -from the CA repo instead of NPM. +For the first case, you don't need to do anything special. For the other two cases, +you need to make sure that **privileged mode** is enabled on the correct CodeBuild +projects, so that Docker can run correctly. The follow sections describe how to do +that. + +You may also need to authenticate to Docker registries to avoid being throttled. +See the section **Authenticating to Docker registries** below for information on how to do +that. + +### Using Docker image assets in the pipeline + +If your `PipelineStack` is using Docker image assets (as opposed to the application +stacks the pipeline is deploying), for example by the use of `LinuxBuildImage.fromAsset()`, +you need to pass `dockerEnabledForSelfMutation: true` to the pipeline. For example: ```ts -class MyPipelineStack extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - ... - const pipeline = new CdkPipeline(this, 'Pipeline', { - ... - synthAction: SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - - // Use this to customize and a permissions required for the build - // and synth - rolePolicyStatements: [ - new PolicyStatement({ - actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], - resources: ['arn:codeartifact:repo:arn'], - }), - ], +const pipeline = new CodePipeline(this, 'Pipeline', { + // ... - // Then you can login to codeartifact repository - // and npm will now pull packages from your repository - // Note the codeartifact login command requires more params to work. - buildCommands: [ - 'aws codeartifact login --tool npm', - 'npm run build', - ], - }), - }); - } -} + // Turn this on because the pipeline uses Docker image assets + dockerEnabledForSelfMutation: true, +}); + +pipeline.addWave('MyWave', { + post: [ + new CodeBuildStep('RunApproval', { + commands: ['command-from-image'], + buildEnvironment: { + // The user of a Docker image asset in the pipeline requires turning on + // 'dockerEnabledForSelfMutation'. + buildImage: LinuxBuildImage.fromAsset(this, 'Image', { + directory: './docker-image', + }) + }, + }) + ], +}); ``` -### Developing the pipeline +> **Important**: You must turn on the `dockerEnabledForSelfMutation` flag, +> commit and allow the pipeline to self-update *before* adding the actual +> Docker asset. -The self-mutation feature of the `CdkPipeline` might at times get in the way -of the pipeline development workflow. Each change to the pipeline must be pushed -to git, otherwise, after the pipeline was updated using `cdk deploy`, it will -automatically revert to the state found in git. +### Using bundled file assets -To make the development more convenient, the self-mutation feature can be turned -off temporarily, by passing `selfMutating: false` property, example: +If you are using asset bundling anywhere (such as automatically done for you +if you add a construct like `@aws-cdk/aws-lambda-nodejs`), you need to pass +`dockerEnabledForSynth: true` to the pipeline. For example: ```ts -const pipeline = new CdkPipeline(this, 'Pipeline', { - selfMutating: false, - ... +const pipeline = new CodePipeline(this, 'Pipeline', { + // ... + + // Turn this on because the application uses bundled file assets + dockerEnabledForSynth: true, }); ``` -## Docker Registry Credentials +> **Important**: You must turn on the `dockerEnabledForSynth` flag, +> commit and allow the pipeline to self-update *before* adding the actual +> Docker asset. + +### Authenticating to Docker registries You can specify credentials to use for authenticating to Docker registries as part of the pipeline definition. This can be useful if any Docker image assets — in the pipeline or @@ -597,26 +686,27 @@ const customRegSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'CRSec const repo1 = ecr.Repository.fromRepositoryArn(stack, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo1'); const repo2 = ecr.Repository.fromRepositoryArn(stack, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo2'); -const pipeline = new CdkPipeline(this, 'Pipeline', { +const pipeline = new CodePipeline(this, 'Pipeline', { dockerCredentials: [ DockerCredential.dockerHub(dockerHubSecret), DockerCredential.customRegistry('dockerregistry.example.com', customRegSecret), DockerCredential.ecr([repo1, repo2]); ], - ... + // ... }); ``` -You can authenticate to DockerHub, or any other Docker registry, by specifying a secret -with the username and secret/password to pass to `docker login`. The names of the fields -within the secret to use for the username and password can be customized. Authentication -to ECR repostories is done using the execution role of the relevant CodeBuild job. Both -types of credentials can be provided with an optional role to assume before requesting -the credentials. +For authenticating to Docker registries that require a username and password combination +(like DockerHub), create a Secrets Manager Secret with fields named `username` +and `secret`, and import it (the field names change be customized). -By default, the Docker credentials provided to the pipeline will be available to the -Synth/Build, Self-Update, and Asset Publishing actions within the pipeline. The scope of -the credentials can be limited via the `DockerCredentialUsage` option. +Authentication to ECR repostories is done using the execution role of the +relevant CodeBuild job. Both types of credentials can be provided with an +optional role to assume before requesting the credentials. + +By default, the Docker credentials provided to the pipeline will be available to +the **Synth**, **Self-Update**, and **Asset Publishing** actions within the +*pipeline. The scope of the credentials can be limited via the `DockerCredentialUsage` option. ```ts const dockerHubSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'DHSecret', 'arn:aws:...'); @@ -640,6 +730,11 @@ Before you can provision the pipeline, you have to *bootstrap* the environment y to create it in. If you are deploying your application to different environments, you also have to bootstrap those and be sure to add a *trust* relationship. +After you have bootstrapped an environment and created a pipeline that deploys +to it, it's important that you don't delete the stack or change its *Qualifier*, +or future deployments to this environment will fail. If you want to upgrade +the bootstrap stack to a newer version, do that by updating it in-place. + > This library requires a newer version of the bootstrapping stack which has > been updated specifically to support cross-account continuous delivery. In the future, > this new bootstrapping stack will become the default, but for now it is still @@ -821,6 +916,16 @@ leading NPM 6 reading that same file to not install all required packages anymor Make sure you are using the same NPM version everywhere, either downgrade your workstation's version or upgrade the CodeBuild version. +### Cannot find module '.../check-node-version.js' (MODULE_NOT_FOUND) + +The above error may be produced by `npx` when executing the CDK CLI, or any +project that uses the AWS SDK for JavaScript, without the target application +having been installed yet. For example, it can be triggered by `npx cdk synth` +if `aws-cdk` is not in your `package.json`. + +Work around this by either installing the target application using NPM *before* +running `npx`, or set the environment variable `NPM_CONFIG_UNSAFE_PERM=true`. + ### Cannot connect to the Docker daemon at unix:///var/run/docker.sock If, in the 'Synth' action (inside the 'Build' stage) of your pipeline, you get an error like this: @@ -858,6 +963,13 @@ update to the right state). ### S3 error: Access Denied +An "S3 Access Denied" error can have two causes: + +* Asset hashes have changed, but self-mutation has been disabled in the pipeline. +* You have deleted and recreated the bootstrap stack, or changed its qualifier. + +#### Self-mutation step has been removed + Some constructs, such as EKS clusters, generate nested stacks. When CloudFormation tries to deploy those stacks, it may fail with this error: @@ -876,7 +988,7 @@ const pipeline = new CdkPipeline(this, 'MyPipeline', { }); ``` -### Action Execution Denied +#### Bootstrap roles have been renamed or recreated While attempting to deploy an application stage, the "Prepare" or "Deploy" stage may fail with a cryptic error like: @@ -922,7 +1034,7 @@ new MyStack(this, 'MyStack', { * Re-deploy the pipeline to use the original qualifier. * Delete the temporary bootstrap stack(s) -#### Manual Alternative +##### Manual Alternative Alternatively, the errors can be resolved by finding each impacted resource and policy, and correcting the policies by replacing the canonical IDs (e.g., `AROAYBRETNYCYV6ZF2R93`) with the appropriate ARNs. As an example, the KMS @@ -942,13 +1054,6 @@ encryption key policy for the artifacts bucket may have a statement that looks l Any resource or policy that references the qualifier (`hnb659fds` by default) will need to be updated. -## Current Limitations - -Limitations that we are aware of and will address: - -* **No context queries**: context queries are not supported. That means that - Vpc.fromLookup() and other functions like it will not work [#8905](https://github.com/aws/aws-cdk/issues/8905). - ## Known Issues There are some usability issues that are caused by underlying technology, and diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/asset-type.ts b/packages/@aws-cdk/pipelines/lib/blueprint/asset-type.ts new file mode 100644 index 0000000000000..3fe015586a7c9 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/asset-type.ts @@ -0,0 +1,15 @@ +/** + * Type of the asset that is being published + */ +export enum AssetType { + /** + * A file + */ + FILE = 'file', + + /** + * A Docker image + */ + DOCKER_IMAGE = 'docker-image', +} + diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/file-set.ts b/packages/@aws-cdk/pipelines/lib/blueprint/file-set.ts new file mode 100644 index 0000000000000..8070bf30de2be --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/file-set.ts @@ -0,0 +1,66 @@ +import { Step } from './step'; + +/** + * A set of files traveling through the deployment pipeline + * + * Individual steps in the pipeline produce or consume + * `FileSet`s. + */ +export class FileSet implements IFileSetProducer { + /** + * The primary output of a file set producer + * + * The primary output of a FileSet is itself. + */ + public readonly primaryOutput?: FileSet = this; + private _producer?: Step; + + constructor( + /** Human-readable descriptor for this file set (does not need to be unique) */ + public readonly id: string, producer?: Step) { + this._producer = producer; + } + + /** + * The Step that produces this FileSet + */ + public get producer() { + if (!this._producer) { + throw new Error(`FileSet '${this.id}' doesn\'t have a producer; call 'fileSet.producedBy()'`); + } + return this._producer; + } + + /** + * Mark the given Step as the producer for this FileSet + * + * This method can only be called once. + */ + public producedBy(producer?: Step) { + if (this._producer) { + throw new Error(`FileSet '${this.id}' already has a producer (${this._producer}) while setting producer: ${producer}`); + } + this._producer = producer; + } + + /** + * Return a string representation of this FileSet + */ + public toString() { + return `FileSet(${this.id})`; + } +} + +/** + * Any class that produces, or is itself, a `FileSet` + * + * Steps implicitly produce a primary FileSet as an output. + */ +export interface IFileSetProducer { + /** + * The `FileSet` produced by this file set producer + * + * @default - This producer doesn't produce any file set + */ + readonly primaryOutput?: FileSet; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/index.ts b/packages/@aws-cdk/pipelines/lib/blueprint/index.ts new file mode 100644 index 0000000000000..d842ca1c7cd67 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/index.ts @@ -0,0 +1,8 @@ +export * from './asset-type'; +export * from './file-set'; +export * from './script-step'; +export * from './stack-deployment'; +export * from './stage-deployment'; +export * from './step'; +export * from './wave'; +export * from './manual-approval'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts b/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts new file mode 100644 index 0000000000000..859c279533fa3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts @@ -0,0 +1,37 @@ +import { Step } from './step'; + +/** + * Construction properties for a `ManualApprovalStep` + */ +export interface ManualApprovalStepProps { + /** + * The comment to display with this manual approval + * + * @default - No comment + */ + readonly comment?: string; +} + +/** + * A manual approval step + * + * If this step is added to a Pipeline, the Pipeline will + * be paused waiting for a human to resume it + * + * Only engines that support pausing the deployment will + * support this step type. + */ +export class ManualApprovalStep extends Step { + /** + * The comment associated with this manual approval + * + * @default - No comment + */ + public readonly comment?: string; + + constructor(id: string, props: ManualApprovalStepProps = {}) { + super(id); + + this.comment = props.comment; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts new file mode 100644 index 0000000000000..75c1883d92419 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts @@ -0,0 +1,275 @@ +import { CfnOutput, Stack } from '@aws-cdk/core'; +import { mapValues } from '../private/javascript'; +import { FileSet, IFileSetProducer } from './file-set'; +import { StackDeployment } from './stack-deployment'; +import { Step } from './step'; + +/** + * Construction properties for a `ShellStep`. + */ +export interface ShellStepProps { + /** + * Commands to run + */ + readonly commands: string[]; + + /** + * Installation commands to run before the regular commands + * + * For deployment engines that support it, install commands will be classified + * differently in the job history from the regular `commands`. + * + * @default - No installation commands + */ + readonly installCommands?: string[]; + + /** + * Environment variables to set + * + * @default - No environment variables + */ + readonly env?: Record; + + /** + * Set environment variables based on Stack Outputs + * + * `ShellStep`s following stack or stage deployments may + * access the `CfnOutput`s of those stacks to get access to + * --for example--automatically generated resource names or + * endpoint URLs. + * + * @default - No environment variables created from stack outputs + */ + readonly envFromCfnOutputs?: Record; + + /** + * FileSet to run these scripts on + * + * The files in the FileSet will be placed in the working directory when + * the script is executed. Use `additionalInputs` to download file sets + * to other directories as well. + * + * @default - No input specified + */ + readonly input?: IFileSetProducer; + + /** + * Additional FileSets to put in other directories + * + * Specifies a mapping from directory name to FileSets. During the + * script execution, the FileSets will be available in the directories + * indicated. + * + * The directory names may be relative. For example, you can put + * the main input and an additional input side-by-side with the + * following configuration: + * + * ```ts + * const script = new ShellStep('MainScript', { + * // ... + * input: MyEngineSource.gitHub('org/source1'), + * additionalInputs: { + * '../siblingdir': MyEngineSource.gitHub('org/source2'), + * } + * }); + * ``` + * + * @default - No additional inputs + */ + readonly additionalInputs?: Record; + + /** + * The directory that will contain the primary output fileset + * + * After running the script, the contents of the given directory + * will be treated as the primary output of this Step. + * + * @default - No primary output + */ + readonly primaryOutputDirectory?: string; + +} + +/** + * Run shell script commands in the pipeline + */ +export class ShellStep extends Step { + /** + * Commands to run + */ + public readonly commands: string[]; + + /** + * Installation commands to run before the regular commands + * + * For deployment engines that support it, install commands will be classified + * differently in the job history from the regular `commands`. + * + * @default - No installation commands + */ + public readonly installCommands: string[]; + + /** + * Environment variables to set + * + * @default - No environment variables + */ + public readonly env: Record; + + /** + * Set environment variables based on Stack Outputs + * + * @default - No environment variables created from stack outputs + */ + public readonly envFromCfnOutputs: Record; + + /** + * Input FileSets + * + * A list of `(FileSet, directory)` pairs, which are a copy of the + * input properties. This list should not be modified directly. + */ + public readonly inputs: FileSetLocation[] = []; + + /** + * Output FileSets + * + * A list of `(FileSet, directory)` pairs, which are a copy of the + * input properties. This list should not be modified directly. + */ + public readonly outputs: FileSetLocation[] = []; + + private readonly _additionalOutputs: Record = {}; + + private _primaryOutputDirectory?: string; + + constructor(id: string, props: ShellStepProps) { + super(id); + + this.commands = props.commands; + this.installCommands = props.installCommands ?? []; + this.env = props.env ?? {}; + this.envFromCfnOutputs = mapValues(props.envFromCfnOutputs ?? {}, StackOutputReference.fromCfnOutput); + + // Inputs + if (props.input) { + const fileSet = props.input.primaryOutput; + if (!fileSet) { + throw new Error(`'${id}': primary input should be a step that produces a file set, got ${props.input}`); + } + this.addDependencyFileSet(fileSet); + this.inputs.push({ directory: '.', fileSet }); + } + + for (const [directory, step] of Object.entries(props.additionalInputs ?? {})) { + if (directory === '.') { + throw new Error(`'${id}': input for directory '.' should be passed via 'input' property`); + } + + const fileSet = step.primaryOutput; + if (!fileSet) { + throw new Error(`'${id}': additionalInput for directory '${directory}' should be a step that produces a file set, got ${step}`); + } + this.addDependencyFileSet(fileSet); + this.inputs.push({ directory, fileSet }); + } + + // Outputs + + if (props.primaryOutputDirectory) { + this._primaryOutputDirectory = props.primaryOutputDirectory; + const fileSet = new FileSet('Output', this); + this.configurePrimaryOutput(fileSet); + this.outputs.push({ directory: props.primaryOutputDirectory, fileSet }); + } + } + + /** + * Configure the given output directory as primary output + * + * If no primary output has been configured yet, this directory + * will become the primary output of this ShellStep, otherwise this + * method will throw if the given directory is different than the + * currently configured primary output directory. + */ + public primaryOutputDirectory(directory: string): FileSet { + if (this._primaryOutputDirectory !== undefined) { + if (this._primaryOutputDirectory !== directory) { + throw new Error(`${this}: primaryOutputDirectory is '${this._primaryOutputDirectory}', cannot be changed to '${directory}'`); + } + + return this.primaryOutput!; + } + + this._primaryOutputDirectory = directory; + const fileSet = new FileSet('Output', this); + this.configurePrimaryOutput(fileSet); + this.outputs.push({ directory: directory, fileSet }); + return fileSet; + } + + /** + * Add an additional output FileSet based on a directory. + * + * + * After running the script, the contents of the given directory + * will be exported as a `FileSet`. Use the `FileSet` as the + * input to another step. + * + * Multiple calls with the exact same directory name string (not normalized) + * will return the same FileSet. + */ + public addOutputDirectory(directory: string): FileSet { + let fileSet = this._additionalOutputs[directory]; + if (!fileSet) { + fileSet = new FileSet(directory, this); + this._additionalOutputs[directory] = fileSet; + this.outputs.push({ directory, fileSet }); + } + return fileSet; + } +} + +/** + * Location of a FileSet consumed or produced by a ShellStep + */ +export interface FileSetLocation { + /** + * The (relative) directory where the FileSet is found + */ + readonly directory: string; + + /** + * The FileSet object + */ + readonly fileSet: FileSet; +} + +/** + * A Reference to a Stack Output + */ +export class StackOutputReference { + /** + * Create a StackOutputReference that references the given CfnOutput + */ + public static fromCfnOutput(output: CfnOutput) { + const stack = Stack.of(output); + return new StackOutputReference(stack.node.path, stack.artifactId, stack.resolve(output.logicalId)); + } + + private constructor( + /** A human-readable description of the producing stack */ + public readonly stackDescription: string, + /** Artifact id of the producing stack */ + private readonly stackArtifactId: string, + /** Output name of the producing stack */ + public readonly outputName: string) { + } + + /** + * Whether or not this stack output is being produced by the given Stack deployment + */ + public isProducedBy(stack: StackDeployment) { + return stack.stackArtifactId === this.stackArtifactId; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/stack-deployment.ts b/packages/@aws-cdk/pipelines/lib/blueprint/stack-deployment.ts new file mode 100644 index 0000000000000..2fe74ef15ccd3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/stack-deployment.ts @@ -0,0 +1,311 @@ +import * as path from 'path'; +import { parse as parseUrl } from 'url'; +import * as cxapi from '@aws-cdk/cx-api'; +import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; +import { isAssetManifest } from '../private/cloud-assembly-internals'; +import { AssetType } from './asset-type'; + +/** + * Properties for a `StackDeployment` + */ +export interface StackDeploymentProps { + /** + * Artifact ID for this stack + */ + readonly stackArtifactId: string; + + /** + * Construct path for this stack + */ + readonly constructPath: string; + + /** + * Name for this stack + */ + readonly stackName: string; + + /** + * Region where the stack should be deployed + * + * @default - Pipeline region + */ + readonly region?: string; + + /** + * Account where the stack should be deployed + * + * @default - Pipeline account + */ + readonly account?: string; + + /** + * Role to assume before deploying this stack + * + * @default - Don't assume any role + */ + readonly assumeRoleArn?: string; + + /** + * Execution role to pass to CloudFormation + * + * @default - No execution role + */ + readonly executionRoleArn?: string; + + /** + * Tags to apply to the stack + * + * @default - No tags + */ + readonly tags?: Record; + + /** + * Template path on disk to cloud assembly (cdk.out) + */ + readonly absoluteTemplatePath: string; + + /** + * Assets referenced by this stack + * + * @default - No assets + */ + readonly assets?: StackAsset[]; + + /** + * The S3 URL which points to the template asset location in the publishing + * bucket. + * + * @default - Stack template is not published + */ + readonly templateS3Uri?: string; +} + +/** + * Deployment of a single Stack + * + * You don't need to instantiate this class -- it will + * be automatically instantiated as necessary when you + * add a `Stage` to a pipeline. + */ +export class StackDeployment { + /** + * Build a `StackDeployment` from a Stack Artifact in a Cloud Assembly. + */ + public static fromArtifact(stackArtifact: cxapi.CloudFormationStackArtifact): StackDeployment { + const artRegion = stackArtifact.environment.region; + const region = artRegion === cxapi.UNKNOWN_REGION ? undefined : artRegion; + const artAccount = stackArtifact.environment.account; + const account = artAccount === cxapi.UNKNOWN_ACCOUNT ? undefined : artAccount; + + return new StackDeployment({ + account, + region, + tags: stackArtifact.tags, + stackArtifactId: stackArtifact.id, + constructPath: stackArtifact.hierarchicalId, + stackName: stackArtifact.stackName, + absoluteTemplatePath: path.join(stackArtifact.assembly.directory, stackArtifact.templateFile), + assumeRoleArn: stackArtifact.assumeRoleArn, + executionRoleArn: stackArtifact.cloudFormationExecutionRoleArn, + assets: extractStackAssets(stackArtifact), + templateS3Uri: stackArtifact.stackTemplateAssetObjectUrl, + }); + } + + /** + * Artifact ID for this stack + */ + public readonly stackArtifactId: string; + + /** + * Construct path for this stack + */ + public readonly constructPath: string; + + /** + * Name for this stack + */ + public readonly stackName: string; + + /** + * Region where the stack should be deployed + * + * @default - Pipeline region + */ + public readonly region?: string; + + /** + * Account where the stack should be deployed + * + * @default - Pipeline account + */ + public readonly account?: string; + + /** + * Role to assume before deploying this stack + * + * @default - Don't assume any role + */ + public readonly assumeRoleArn?: string; + + /** + * Execution role to pass to CloudFormation + * + * @default - No execution role + */ + public readonly executionRoleArn?: string; + + /** + * Tags to apply to the stack + */ + public readonly tags: Record; + + /** + * Assets referenced by this stack + */ + public readonly assets: StackAsset[]; + + /** + * Other stacks this stack depends on + */ + public readonly stackDependencies: StackDeployment[] = []; + + /** + * The asset that represents the CloudFormation template for this stack. + */ + public readonly templateAsset?: StackAsset; + + /** + * The S3 URL which points to the template asset location in the publishing + * bucket. + * + * This is `undefined` if the stack template is not published. Use the + * `DefaultStackSynthesizer` to ensure it is. + * + * @example https://bucket.s3.amazonaws.com/object/key + */ + public readonly templateUrl?: string; + + /** + * Template path on disk to CloudAssembly + */ + public readonly absoluteTemplatePath: string; + + private constructor(props: StackDeploymentProps) { + this.stackArtifactId = props.stackArtifactId; + this.constructPath = props.constructPath; + this.account = props.account; + this.region = props.region; + this.tags = props.tags ?? {}; + this.assumeRoleArn = props.assumeRoleArn; + this.executionRoleArn = props.executionRoleArn; + this.stackName = props.stackName; + this.absoluteTemplatePath = props.absoluteTemplatePath; + this.templateUrl = props.templateS3Uri ? s3UrlFromUri(props.templateS3Uri, props.region) : undefined; + + this.assets = new Array(); + + for (const asset of props.assets ?? []) { + if (asset.isTemplate) { + this.templateAsset = asset; + } else { + this.assets.push(asset); + } + } + } + + /** + * Add a dependency on another stack + */ + public addStackDependency(stackDeployment: StackDeployment) { + this.stackDependencies.push(stackDeployment); + } +} + +/** + * An asset used by a Stack + */ +export interface StackAsset { + /** + * Absolute asset manifest path + * + * This needs to be made relative at a later point in time, but when this + * information is parsed we don't know about the root cloud assembly yet. + */ + readonly assetManifestPath: string; + + /** + * Asset identifier + */ + readonly assetId: string; + + /** + * Asset selector to pass to `cdk-assets`. + */ + readonly assetSelector: string; + + /** + * Type of asset to publish + */ + readonly assetType: AssetType; + + /** + * Role ARN to assume to publish + * + * @default - No need to assume any role + */ + readonly assetPublishingRoleArn?: string; + + /** + * Does this asset represent the CloudFormation template for the stack + * + * @default false + */ + readonly isTemplate: boolean; +} + +function extractStackAssets(stackArtifact: cxapi.CloudFormationStackArtifact): StackAsset[] { + const ret = new Array(); + + const assetManifests = stackArtifact.dependencies.filter(isAssetManifest); + for (const manifestArtifact of assetManifests) { + const manifest = AssetManifestReader.fromFile(manifestArtifact.file); + + for (const entry of manifest.entries) { + let assetType: AssetType; + let isTemplate = false; + + if (entry instanceof DockerImageManifestEntry) { + assetType = AssetType.DOCKER_IMAGE; + } else if (entry instanceof FileManifestEntry) { + isTemplate = entry.source.packaging === 'file' && entry.source.path === stackArtifact.templateFile; + assetType = AssetType.FILE; + } else { + throw new Error(`Unrecognized asset type: ${entry.type}`); + } + + ret.push({ + assetManifestPath: manifestArtifact.file, + assetId: entry.id.assetId, + assetSelector: entry.id.toString(), + assetType, + assetPublishingRoleArn: entry.destination.assumeRoleArn, + isTemplate, + }); + } + } + + return ret; +} + +/** + * Takes an s3://bucket/object-key uri and returns a region-aware https:// url for it + * + * @param uri The s3 URI + * @param region The region (if undefined, we will return the global endpoint) + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access + */ +function s3UrlFromUri(uri: string, region: string | undefined) { + const url = parseUrl(uri); + return `https://${url.hostname}.s3.${region ? `${region}.` : ''}amazonaws.com${url.path}`; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts b/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts new file mode 100644 index 0000000000000..499d324dfb25f --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts @@ -0,0 +1,118 @@ +import * as cdk from '@aws-cdk/core'; +import { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; +import { isStackArtifact } from '../private/cloud-assembly-internals'; +import { StackDeployment } from './stack-deployment'; +import { Step } from './step'; + +/** + * Properties for a `StageDeployment` + */ +export interface StageDeploymentProps { + /** + * Stage name to use in the pipeline + * + * @default - Use Stage's construct ID + */ + readonly stageName?: string; + + /** + * Additional steps to run before any of the stacks in the stage + * + * @default - No additional steps + */ + readonly pre?: Step[]; + + /** + * Additional steps to run after all of the stacks in the stage + * + * @default - No additional steps + */ + readonly post?: Step[]; +} + +/** + * Deployment of a single `Stage` + * + * A `Stage` consists of one or more `Stacks`, which will be + * deployed in dependency order. + */ +export class StageDeployment { + /** + * Create a new `StageDeployment` from a `Stage` + * + * Synthesizes the target stage, and deployes the stacks found inside + * in dependency order. + */ + public static fromStage(stage: cdk.Stage, props: StageDeploymentProps = {}) { + const assembly = stage.synth(); + if (assembly.stacks.length === 0) { + // If we don't check here, a more puzzling "stage contains no actions" + // error will be thrown come deployment time. + throw new Error(`The given Stage construct ('${stage.node.path}') should contain at least one Stack`); + } + + const stepFromArtifact = new Map(); + for (const artifact of assembly.stacks) { + const step = StackDeployment.fromArtifact(artifact); + stepFromArtifact.set(artifact, step); + } + + for (const artifact of assembly.stacks) { + const thisStep = stepFromArtifact.get(artifact); + if (!thisStep) { + throw new Error('Logic error: we just added a step for this artifact but it disappeared.'); + } + + const stackDependencies = artifact.dependencies.filter(isStackArtifact); + for (const dep of stackDependencies) { + const depStep = stepFromArtifact.get(dep); + if (!depStep) { + throw new Error(`Stack '${artifact.id}' depends on stack not found in same Stage: '${dep.id}'`); + } + thisStep.addStackDependency(depStep); + } + } + + return new StageDeployment(Array.from(stepFromArtifact.values()), { + stageName: stage.stageName, + ...props, + }); + } + + /** + * The display name of this stage + */ + public readonly stageName: string; + + /** + * Additional steps that are run before any of the stacks in the stage + */ + public readonly pre: Step[]; + + /** + * Additional steps that are run after all of the stacks in the stage + */ + public readonly post: Step[]; + + private constructor( + /** The stacks deployed in this stage */ + public readonly stacks: StackDeployment[], props: StageDeploymentProps = {}) { + this.stageName = props.stageName ?? ''; + this.pre = props.pre ?? []; + this.post = props.post ?? []; + } + + /** + * Add an additional step to run before any of the stacks in this stage + */ + public addPre(...steps: Step[]) { + this.pre.push(...steps); + } + + /** + * Add an additional step to run after all of the stacks in this stage + */ + public addPost(...steps: Step[]) { + this.post.push(...steps); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts new file mode 100644 index 0000000000000..e252765efd04e --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts @@ -0,0 +1,72 @@ +import { FileSet, IFileSetProducer } from './file-set'; + +/** + * A generic Step which can be added to a Pipeline + * + * Steps can be used to add Sources, Build Actions and Validations + * to your pipeline. + * + * This class is abstract. See specific subclasses of Step for + * useful steps to add to your Pipeline + */ +export abstract class Step implements IFileSetProducer { + /** + * The list of FileSets consumed by this Step + */ + public readonly dependencyFileSets: FileSet[] = []; + + /** + * Whether or not this is a Source step + * + * What it means to be a Source step depends on the engine. + */ + public readonly isSource: boolean = false; + + private _primaryOutput?: FileSet; + + constructor( + /** Identifier for this step */ + public readonly id: string) { + } + + /** + * Return the steps this step depends on, based on the FileSets it requires + */ + public get dependencies(): Step[] { + return this.dependencyFileSets.map(f => f.producer); + } + + /** + * Return a string representation of this Step + */ + public toString() { + return `${this.constructor.name}(${this.id})`; + } + + /** + * The primary FileSet produced by this Step + * + * Not all steps produce an output FileSet--if they do + * you can substitute the `Step` object for the `FileSet` object. + */ + public get primaryOutput(): FileSet | undefined { + // Accessor so it can be mutable in children + return this._primaryOutput; + } + + /** + * Add an additional FileSet to the set of file sets required by this step + * + * This will lead to a dependency on the producer of that file set. + */ + protected addDependencyFileSet(fs: FileSet) { + this.dependencyFileSets.push(fs); + } + + /** + * Configure the given FileSet as the primary output of this step + */ + protected configurePrimaryOutput(fs: FileSet) { + this._primaryOutput = fs; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/wave.ts b/packages/@aws-cdk/pipelines/lib/blueprint/wave.ts new file mode 100644 index 0000000000000..709d43a1ed8bd --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/wave.ts @@ -0,0 +1,113 @@ +import * as cdk from '@aws-cdk/core'; +import { StageDeployment } from './stage-deployment'; +import { Step } from './step'; + +/** + * Construction properties for a `Wave` + */ +export interface WaveProps { + /** + * Additional steps to run before any of the stages in the wave + * + * @default - No additional steps + */ + readonly pre?: Step[]; + + /** + * Additional steps to run after all of the stages in the wave + * + * @default - No additional steps + */ + readonly post?: Step[]; +} + +/** + * Multiple stages that are deployed in parallel + */ +export class Wave { + /** + * Additional steps that are run before any of the stages in the wave + */ + public readonly pre: Step[]; + + /** + * Additional steps that are run after all of the stages in the wave + */ + public readonly post: Step[]; + + /** + * The stages that are deployed in this wave + */ + public readonly stages: StageDeployment[] = []; + + constructor( + /** Identifier for this Wave */ + public readonly id: string, props: WaveProps = {}) { + this.pre = props.pre ?? []; + this.post = props.post ?? []; + } + + /** + * Add a Stage to this wave + * + * It will be deployed in parallel with all other stages in this + * wave. + */ + public addStage(stage: cdk.Stage, options: AddStageOpts = {}) { + const ret = StageDeployment.fromStage(stage, options); + this.stages.push(ret); + return ret; + } + + /** + * Add an additional step to run before any of the stages in this wave + */ + public addPre(...steps: Step[]) { + this.pre.push(...steps); + } + + /** + * Add an additional step to run after all of the stages in this wave + */ + public addPost(...steps: Step[]) { + this.post.push(...steps); + } +} + +/** + * Options to pass to `addStage` + */ +export interface AddStageOpts { + /** + * Additional steps to run before any of the stacks in the stage + * + * @default - No additional steps + */ + readonly pre?: Step[]; + + /** + * Additional steps to run after all of the stacks in the stage + * + * @default - No additional steps + */ + readonly post?: Step[]; +} + +/** + * Options to pass to `addWave` + */ +export interface WaveOptions { + /** + * Additional steps to run before any of the stages in the wave + * + * @default - No additional steps + */ + readonly pre?: Step[]; + + /** + * Additional steps to run after all of the stages in the wave + * + * @default - No additional steps + */ + readonly post?: Step[]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts new file mode 100644 index 0000000000000..f814e8b8fe272 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts @@ -0,0 +1,502 @@ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { IDependable, Stack } from '@aws-cdk/core'; +import { Construct, Node } from 'constructs'; +import { FileSetLocation, ShellStep, StackDeployment, StackOutputReference } from '../blueprint'; +import { PipelineQueries } from '../helpers-internal/pipeline-queries'; +import { cloudAssemblyBuildSpecDir, obtainScope } from '../private/construct-internals'; +import { mapValues, mkdict, noEmptyObject, noUndefined, partition } from '../private/javascript'; +import { ArtifactMap } from './artifact-map'; +import { CodeBuildStep } from './codebuild-step'; +import { CodeBuildOptions } from './codepipeline'; +import { ICodePipelineActionFactory, ProduceActionOptions, CodePipelineActionFactoryResult } from './codepipeline-action-factory'; + +export interface CodeBuildFactoryProps { + /** + * Name for the generated CodeBuild project + * + * @default - Automatically generated + */ + readonly projectName?: string; + + /** + * Customization options for the project + * + * Will at CodeBuild production time be combined with the option + * defaults configured on the pipeline. + * + * @default - No special values + */ + readonly projectOptions?: CodeBuildOptions; + + /** + * Custom execution role to be used for the CodeBuild project + * + * @default - A role is automatically created + */ + readonly role?: iam.IRole; + + /** + * If true, the build spec will be passed via the Cloud Assembly instead of rendered onto the Project + * + * Doing this has two advantages: + * + * - Bypass size restrictions: the buildspec on the project is restricted + * in size, while buildspecs coming from an input artifact are not restricted + * in such a way. + * - Bypass pipeline update: if the SelfUpdate step has to change the buildspec, + * that just takes time. On the other hand, if the buildspec comes from the + * pipeline artifact, no such update has to take place. + * + * @default false + */ + readonly passBuildSpecViaCloudAssembly?: boolean; + + /** + * Override the construct tree where the CodeBuild project is created. + * + * Normally, the construct tree will look like this: + * + * ── Pipeline + * └── 'MyStage' <- options.scope + * └── 'MyAction' <- this is the CodeBuild project + * + * If this flag is set, the construct tree will look like this: + * + * ── Pipeline + * └── 'MyStage' <- options.scope + * └── 'MyAction' <- just a scope + * └── 'BackwardsCompatName' <- CodeBuild project + * + * This is to maintain logicalID compatibility with the previous iteration + * of pipelines (where the Action was a construct that would create the Project). + * + * @default true + */ + readonly additionalConstructLevel?: boolean; + + /** + * Additional dependency that the CodeBuild project should take + * + * @default - + */ + readonly additionalDependable?: IDependable; + + readonly inputs?: FileSetLocation[]; + readonly outputs?: FileSetLocation[]; + + readonly stepId?: string; + + readonly commands: string[]; + readonly installCommands?: string[]; + + readonly env?: Record; + readonly envFromCfnOutputs?: Record; + + /** + * If given, override the scope from the produce call with this scope. + */ + readonly scope?: Construct; + + /** + * Whether or not the given CodeBuild project is going to be the synth step + * + * @default false + */ + readonly isSynth?: boolean; +} + +/** + * Produce a CodeBuild project from a RunScript step and some CodeBuild-specific customizations + * + * The functionality here is shared between the `CodePipeline` translating a `ShellStep` into + * a CodeBuild project, as well as the `CodeBuildStep` straight up. + */ +export class CodeBuildFactory implements ICodePipelineActionFactory { + // eslint-disable-next-line max-len + public static fromShellStep(constructId: string, scriptStep: ShellStep, additional?: Partial): ICodePipelineActionFactory { + return new CodeBuildFactory(constructId, { + commands: scriptStep.commands, + env: scriptStep.env, + envFromCfnOutputs: scriptStep.envFromCfnOutputs, + inputs: scriptStep.inputs, + outputs: scriptStep.outputs, + stepId: scriptStep.id, + installCommands: scriptStep.installCommands, + ...additional, + }); + } + + public static fromCodeBuildStep(constructId: string, step: CodeBuildStep, additional?: Partial): ICodePipelineActionFactory { + const factory = CodeBuildFactory.fromShellStep(constructId, step, { + projectName: step.projectName, + role: step.role, + projectOptions: { + buildEnvironment: step.buildEnvironment, + rolePolicy: step.rolePolicyStatements, + securityGroups: step.securityGroups, + partialBuildSpec: step.partialBuildSpec, + vpc: step.vpc, + subnetSelection: step.subnetSelection, + ...additional?.projectOptions, + }, + ...additional, + }); + + return { + produceAction: (stage, options) => { + const result = factory.produceAction(stage, options); + if (result.project) { + step._setProject(result.project); + } + return result; + }, + }; + } + + private _project?: codebuild.IProject; + private stepId: string; + + private constructor( + private readonly constructId: string, + private readonly props: CodeBuildFactoryProps) { + + this.stepId = props.stepId ?? constructId; + } + + public get project(): codebuild.IProject { + if (!this._project) { + throw new Error('Project becomes available after produce() has been called'); + } + return this._project; + } + + public produceAction(stage: codepipeline.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { + const projectOptions = mergeCodeBuildOptions(options.codeBuildDefaults, this.props.projectOptions); + + const inputs = this.props.inputs ?? []; + const outputs = this.props.outputs ?? []; + + const mainInput = inputs.find(x => x.directory === '.'); + const extraInputs = inputs.filter(x => x.directory !== '.'); + + const inputArtifact = mainInput + ? options.artifacts.toCodePipeline(mainInput.fileSet) + : options.fallbackArtifact; + const extraInputArtifacts = extraInputs.map(x => options.artifacts.toCodePipeline(x.fileSet)); + const outputArtifacts = outputs.map(x => options.artifacts.toCodePipeline(x.fileSet)); + + if (!inputArtifact) { + // This should actually never happen because CodeBuild projects shouldn't be added before the + // Source, which always produces at least an artifact. + throw new Error(`CodeBuild action '${this.stepId}' requires an input (and the pipeline doesn't have a Source to fall back to). Add an input or a pipeline source.`); + } + + const installCommands = [ + ...generateInputArtifactLinkCommands(options.artifacts, extraInputs), + ...this.props.installCommands ?? [], + ]; + + const buildSpecHere = codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + install: (installCommands.length ?? 0) > 0 ? { commands: installCommands } : undefined, + build: this.props.commands.length > 0 ? { commands: this.props.commands } : undefined, + }, + artifacts: noEmptyObject(renderArtifactsBuildSpec(options.artifacts, this.props.outputs ?? [])), + }); + + // Partition environment variables into environment variables that can go on the project + // and environment variables that MUST go in the pipeline (those that reference CodePipeline variables) + const env = noUndefined(this.props.env ?? {}); + + const [actionEnvs, projectEnvs] = partition(Object.entries(env ?? {}), ([, v]) => containsPipelineVariable(v)); + + const environment = mergeBuildEnvironments( + projectOptions?.buildEnvironment ?? {}, + { + environmentVariables: noEmptyObject(mapValues(mkdict(projectEnvs), value => ({ value }))), + }); + + const fullBuildSpec = options.codeBuildDefaults?.partialBuildSpec + ? codebuild.mergeBuildSpecs(options.codeBuildDefaults?.partialBuildSpec, buildSpecHere) + : buildSpecHere; + + const osFromEnvironment = environment.buildImage && environment.buildImage instanceof codebuild.WindowsBuildImage + ? ec2.OperatingSystemType.WINDOWS + : ec2.OperatingSystemType.LINUX; + + const actualBuildSpec = filterBuildSpecCommands(fullBuildSpec, osFromEnvironment); + + const scope = this.props.scope ?? options.scope; + + let projectBuildSpec; + if (this.props.passBuildSpecViaCloudAssembly) { + // Write to disk and replace with a reference + const relativeSpecFile = `buildspec-${Node.of(scope).addr}-${this.constructId}.yaml`; + const absSpecFile = path.join(cloudAssemblyBuildSpecDir(scope), relativeSpecFile); + fs.writeFileSync(absSpecFile, Stack.of(scope).resolve(actualBuildSpec.toBuildSpec()), { encoding: 'utf-8' }); + projectBuildSpec = codebuild.BuildSpec.fromSourceFilename(relativeSpecFile); + } else { + projectBuildSpec = actualBuildSpec; + } + + // A hash over the values that make the CodeBuild Project unique (and necessary + // to restart the pipeline if one of them changes). projectName is not necessary to include + // here because the pipeline will definitely restart if projectName changes. + // (Resolve tokens) + const projectConfigHash = hash(Stack.of(scope).resolve({ + environment: serializeBuildEnvironment(environment), + buildSpecString: actualBuildSpec.toBuildSpec(), + })); + + const actionName = options.actionName ?? this.stepId; + + let projectScope = scope; + if (this.props.additionalConstructLevel ?? true) { + projectScope = obtainScope(scope, actionName); + } + + const project = new codebuild.PipelineProject(projectScope, this.constructId, { + projectName: this.props.projectName, + environment, + vpc: projectOptions.vpc, + subnetSelection: projectOptions.subnetSelection, + securityGroups: projectOptions.securityGroups, + buildSpec: projectBuildSpec, + role: this.props.role, + }); + + if (this.props.additionalDependable) { + project.node.addDependency(this.props.additionalDependable); + } + + if (projectOptions.rolePolicy !== undefined) { + projectOptions.rolePolicy.forEach(policyStatement => { + project.addToRolePolicy(policyStatement); + }); + } + + const queries = new PipelineQueries(options.pipeline); + + const stackOutputEnv = mapValues(this.props.envFromCfnOutputs ?? {}, outputRef => + `#{${stackVariableNamespace(queries.producingStack(outputRef))}.${outputRef.outputName}}`, + ); + + const configHashEnv = options.beforeSelfMutation + ? { _PROJECT_CONFIG_HASH: projectConfigHash } + : {}; + + stage.addAction(new codepipeline_actions.CodeBuildAction({ + actionName: actionName, + input: inputArtifact, + extraInputs: extraInputArtifacts, + outputs: outputArtifacts, + project, + runOrder: options.runOrder, + + // Inclusion of the hash here will lead to the pipeline structure for any changes + // made the config of the underlying CodeBuild Project. + // Hence, the pipeline will be restarted. This is necessary if the users + // adds (for example) build or test commands to the buildspec. + environmentVariables: noEmptyObject(cbEnv({ + ...mkdict(actionEnvs), + ...configHashEnv, + ...stackOutputEnv, + })), + })); + + this._project = project; + + return { runOrdersConsumed: 1, project }; + } +} + +/** + * Generate commands to move additional input artifacts into the right place + */ +function generateInputArtifactLinkCommands(artifacts: ArtifactMap, inputs: FileSetLocation[]): string[] { + return inputs.map(input => { + const fragments = []; + + if (!['.', '..'].includes(path.dirname(input.directory))) { + fragments.push(`mkdir -p "${input.directory}"`); + } + + const artifact = artifacts.toCodePipeline(input.fileSet); + + fragments.push(`ln -s "$CODEBUILD_SRC_DIR_${artifact.artifactName}" "${input.directory}"`); + + return fragments.join(' && '); + }); +} + +function renderArtifactsBuildSpec(artifactMap: ArtifactMap, outputs: FileSetLocation[]) { + // save the generated files in the output artifact + // This part of the buildspec has to look completely different depending on whether we're + // using secondary artifacts or not. + if (outputs.length === 0) { return {}; } + + if (outputs.length === 1) { + return { + 'base-directory': outputs[0].directory, + 'files': '**/*', + }; + } + + const secondary: Record = {}; + for (const output of outputs) { + const art = artifactMap.toCodePipeline(output.fileSet); + + if (!art.artifactName) { + throw new Error('You must give the output artifact a name'); + } + secondary[art.artifactName] = { + 'base-directory': output.directory, + 'files': '**/*', + }; + } + + return { 'secondary-artifacts': secondary }; +} + +export function mergeCodeBuildOptions(...opts: Array) { + const xs = [{}, ...opts.filter(isDefined)]; + while (xs.length > 1) { + const [a, b] = xs.splice(xs.length - 2, 2); + xs.push(merge2(a, b)); + } + return xs[0]; + + function merge2(a: CodeBuildOptions, b: CodeBuildOptions): CodeBuildOptions { + return { + buildEnvironment: mergeBuildEnvironments(a.buildEnvironment, b.buildEnvironment), + rolePolicy: definedArray([...a.rolePolicy ?? [], ...b.rolePolicy ?? []]), + securityGroups: definedArray([...a.securityGroups ?? [], ...b.securityGroups ?? []]), + partialBuildSpec: mergeBuildSpecs(a.partialBuildSpec, b.partialBuildSpec), + vpc: b.vpc ?? a.vpc, + subnetSelection: b.subnetSelection ?? a.subnetSelection, + }; + } +} + +function mergeBuildEnvironments(a: codebuild.BuildEnvironment, b?: codebuild.BuildEnvironment): codebuild.BuildEnvironment; +function mergeBuildEnvironments(a: codebuild.BuildEnvironment | undefined, b: codebuild.BuildEnvironment): codebuild.BuildEnvironment; +function mergeBuildEnvironments(a?: codebuild.BuildEnvironment, b?: codebuild.BuildEnvironment): codebuild.BuildEnvironment | undefined; +function mergeBuildEnvironments(a?: codebuild.BuildEnvironment, b?: codebuild.BuildEnvironment) { + if (!a || !b) { return a ?? b; } + + return { + buildImage: b.buildImage ?? a.buildImage, + computeType: b.computeType ?? a.computeType, + environmentVariables: { + ...a.environmentVariables, + ...b.environmentVariables, + }, + privileged: b.privileged ?? a.privileged, + }; +} + +export function mergeBuildSpecs(a: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec; +export function mergeBuildSpecs(a: codebuild.BuildSpec | undefined, b: codebuild.BuildSpec): codebuild.BuildSpec; +export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec | undefined; +export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec) { + if (!a || !b) { return a ?? b; } + return codebuild.mergeBuildSpecs(a, b); +} + +function isDefined(x: A | undefined): x is NonNullable { + return x !== undefined; +} + +function hash(obj: A) { + const d = crypto.createHash('sha256'); + d.update(JSON.stringify(obj)); + return d.digest('hex'); +} + +/** + * Serialize a build environment to data (get rid of constructs & objects), so we can JSON.stringify it + */ +function serializeBuildEnvironment(env: codebuild.BuildEnvironment) { + return { + privileged: env.privileged, + environmentVariables: env.environmentVariables, + type: env.buildImage?.type, + imageId: env.buildImage?.imageId, + computeType: env.computeType, + imagePullPrincipalType: env.buildImage?.imagePullPrincipalType, + secretsManagerArn: env.buildImage?.secretsManagerCredentials?.secretArn, + }; +} + +export function stackVariableNamespace(stack: StackDeployment) { + return stack.stackArtifactId; +} + +/** + * Whether the given string contains a reference to a CodePipeline variable + */ +function containsPipelineVariable(s: string) { + return !!s.match(/#\{[^}]+\}/); +} + +/** + * Turn a collection into a collection of CodePipeline environment variables + */ +function cbEnv(xs: Record): Record { + return mkdict(Object.entries(xs) + .filter(([, v]) => v !== undefined) + .map(([k, v]) => [k, { value: v }] as const)); +} + +function definedArray(xs: A[]): A[] | undefined { + return xs.length > 0 ? xs : undefined; +} + +/** + * If lines in the buildspec start with '!WINDOWS!' or '!LINUX!', only render them on that platform. + * + * Very private protocol for now, but may come in handy in other libraries as well. + */ +function filterBuildSpecCommands(buildSpec: codebuild.BuildSpec, osType: ec2.OperatingSystemType) { + if (!buildSpec.isImmediate) { return buildSpec; } + const spec = (buildSpec as any).spec; + + const winTag = '!WINDOWS!'; + const linuxTag = '!LINUX!'; + const expectedTag = osType === ec2.OperatingSystemType.WINDOWS ? winTag : linuxTag; + + return codebuild.BuildSpec.fromObject(recurse(spec)); + + function recurse(x: any): any { + if (Array.isArray(x)) { + const ret: any[] = []; + for (const el of x) { + const [tag, payload] = extractTag(el); + if (tag === undefined || tag === expectedTag) { + ret.push(payload); + } + } + return ret; + } + if (x && typeof x === 'object') { + return mapValues(x, recurse); + } + return x; + } + + function extractTag(x: any): [string | undefined, any] { + if (typeof x !== 'string') { return [undefined, x]; } + for (const tag of [winTag, linuxTag]) { + if (x.startsWith(tag)) { return [tag, x.substr(tag.length)]; } + } + return [undefined, x]; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/artifact-map.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/artifact-map.ts new file mode 100644 index 0000000000000..4ec4b086815c7 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/artifact-map.ts @@ -0,0 +1,71 @@ +import * as cp from '@aws-cdk/aws-codepipeline'; +import { FileSet } from '../blueprint'; +import { PipelineGraph } from '../helpers-internal'; + +/** + * Translate FileSets to CodePipeline Artifacts + */ +export class ArtifactMap { + private artifacts = new Map(); + private usedNames = new Set(); + + /** + * Return the matching CodePipeline artifact for a FileSet + */ + public toCodePipeline(x: FileSet): cp.Artifact { + if (x instanceof CodePipelineFileSet) { + return x._artifact; + } + + let ret = this.artifacts.get(x); + if (!ret) { + // They all need a name + const artifactName = this.makeUniqueName(`${x.producer.id}.${x.id}`); + this.usedNames.add(artifactName); + this.artifacts.set(x, ret = new cp.Artifact(artifactName)); + } + return ret; + } + + private makeUniqueName(baseName: string) { + let i = 1; + baseName = sanitizeArtifactName(baseName); + let name = baseName; + while (this.usedNames.has(name)) { + name = `${baseName}${++i}`; + } + return name; + } +} + +function sanitizeArtifactName(x: string): string { + // FIXME: Does this REALLY not allow '.'? The docs don't mention it, but action names etc. do! + return x.replace(/[^A-Za-z0-9@\-_]/g, '_'); +} + +/** + * A FileSet created from a CodePipeline artifact + * + * You only need to use this if you want to add CDK Pipeline stages + * add the end of an existing CodePipeline, which should be very rare. + */ +export class CodePipelineFileSet extends FileSet { + /** + * Turn a CodePipeline Artifact into a FileSet + */ + public static fromArtifact(artifact: cp.Artifact) { + return new CodePipelineFileSet(artifact); + } + + /** + * The artifact this class is wrapping + * + * @internal + */ + public readonly _artifact: cp.Artifact; + + private constructor(artifact: cp.Artifact) { + super(artifact.artifactName ?? 'Imported', PipelineGraph.NO_STEP); + this._artifact = artifact; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts new file mode 100644 index 0000000000000..064c283ef2f12 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts @@ -0,0 +1,189 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { ShellStep, ShellStepProps } from '../blueprint'; + +/** + * Construction props for SimpleSynthAction + */ +export interface CodeBuildStepProps extends ShellStepProps { + /** + * Name for the generated CodeBuild project + * + * @default - Automatically generated + */ + readonly projectName?: string; + + /** + * Additional configuration that can only be configured via BuildSpec + * + * You should not use this to specify output artifacts; those + * should be supplied via the other properties of this class, otherwise + * CDK Pipelines won't be able to inspect the artifacts. + * + * Set the `commands` to an empty array if you want to fully specify + * the BuildSpec using this field. + * + * The BuildSpec must be available inline--it cannot reference a file + * on disk. + * + * @default - BuildSpec completely derived from other properties + */ + readonly partialBuildSpec?: codebuild.BuildSpec; + + /** + * The VPC where to execute the SimpleSynth. + * + * @default - No VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * Only used if 'vpc' is supplied. + * + * @default - All private subnets. + */ + readonly subnetSelection?: ec2.SubnetSelection; + + /** + * Policy statements to add to role used during the synth + * + * Can be used to add acces to a CodeArtifact repository etc. + * + * @default - No policy statements added to CodeBuild Project Role + */ + readonly rolePolicyStatements?: iam.PolicyStatement[]; + + /** + * Custom execution role to be used for the CodeBuild project + * + * @default - A role is automatically created + */ + readonly role?: iam.IRole; + + /** + * Changes to environment + * + * This environment will be combined with the pipeline's default + * environment. + * + * @default - Use the pipeline's default build environment + */ + readonly buildEnvironment?: codebuild.BuildEnvironment; + + /** + * Which security group to associate with the script's project network interfaces. + * If no security group is identified, one will be created automatically. + * + * Only used if 'vpc' is supplied. + * + * @default - Security group will be automatically created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; +} + +/** + * Run a script as a CodeBuild Project + */ +export class CodeBuildStep extends ShellStep { + /** + * Name for the generated CodeBuild project + * + * @default - No value specified at construction time, use defaults + */ + public readonly projectName?: string; + + /** + * Additional configuration that can only be configured via BuildSpec + * + * @default - No value specified at construction time, use defaults + */ + public readonly partialBuildSpec?: codebuild.BuildSpec; + + /** + * The VPC where to execute the SimpleSynth. + * + * @default - No value specified at construction time, use defaults + */ + public readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * @default - No value specified at construction time, use defaults + */ + public readonly subnetSelection?: ec2.SubnetSelection; + + /** + * Policy statements to add to role used during the synth + * + * @default - No value specified at construction time, use defaults + */ + public readonly rolePolicyStatements?: iam.PolicyStatement[]; + + /** + * Custom execution role to be used for the CodeBuild project + * + * @default - No value specified at construction time, use defaults + */ + public readonly role?: iam.IRole; + + /** + * Build environment + * + * @default - No value specified at construction time, use defaults + */ + readonly buildEnvironment?: codebuild.BuildEnvironment; + + /** + * Which security group to associate with the script's project network interfaces. + * + * @default - No value specified at construction time, use defaults + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + private _project?: codebuild.IProject; + + constructor(id: string, props: CodeBuildStepProps) { + super(id, props); + + this.projectName = props.projectName; + this.buildEnvironment = props.buildEnvironment; + this.partialBuildSpec = props.partialBuildSpec; + this.vpc = props.vpc; + this.subnetSelection = props.subnetSelection; + this.role = props.role; + this.rolePolicyStatements = props.rolePolicyStatements; + this.securityGroups = props.securityGroups; + } + + /** + * CodeBiuld Project generated for the pipeline + * + * Will only be available after the pipeline has been built. + */ + public get project(): codebuild.IProject { + if (!this._project) { + throw new Error('Project becomes available after the pipeline has been built'); + } + return this._project; + } + + /** + * The CodeBuild Project's principal + */ + public get grantPrincipal(): iam.IPrincipal { + return this.project.grantPrincipal; + } + + /** + * Set the internal project value + * + * @internal + */ + public _setProject(project: codebuild.IProject) { + this._project = project; + } +} diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts new file mode 100644 index 0000000000000..89d419b56223d --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts @@ -0,0 +1,100 @@ +import * as cb from '@aws-cdk/aws-codebuild'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import { Construct } from 'constructs'; +import { PipelineBase } from '../main'; +import { ArtifactMap } from './artifact-map'; +import { CodeBuildOptions } from './codepipeline'; + +/** + * Options for the `CodePipelineActionFactory.produce()` method. + */ +export interface ProduceActionOptions { + /** + * Scope in which to create constructs + */ + readonly scope: Construct; + + /** + * Name the action should get + */ + readonly actionName: string; + + /** + * RunOrder the action should get + */ + readonly runOrder: number; + + /** + * Helper object to translate FileSets to CodePipeline Artifacts + */ + readonly artifacts: ArtifactMap; + + /** + * An input artifact that CodeBuild projects that don't actually need an input artifact can use + * + * CodeBuild Projects MUST have an input artifact in order to be added to the Pipeline. If + * the Project doesn't actually care about its input (it can be anything), it can use the + * Artifact passed here. + * + * @default - A fallback artifact does not exist + */ + readonly fallbackArtifact?: cp.Artifact; + + /** + * The pipeline the action is being generated for + */ + readonly pipeline: PipelineBase; + + /** + * If this action factory creates a CodeBuild step, default options to inherit + * + * @default - No CodeBuild project defaults + */ + readonly codeBuildDefaults?: CodeBuildOptions; + + /** + * Whether or not this action is inserted before self mutation. + * + * If it is, the action should take care to reflect some part of + * its own definition in the pipeline action definition, to + * trigger a restart after self-mutation (if necessary). + * + * @default false + */ + readonly beforeSelfMutation?: boolean; +} + +/** + * Factory for explicit CodePipeline Actions + * + * If you have specific types of Actions you want to add to a + * CodePipeline, write a subclass of `Step` that implements this + * interface, and add the action or actions you want in the `produce` method. + * + * There needs to be a level of indirection here, because some aspects of the + * Action creation need to be controlled by the workflow engine (name and + * runOrder). All the rest of the properties are controlled by the factory. + */ +export interface ICodePipelineActionFactory { + /** + * Create the desired Action and add it to the pipeline + */ + produceAction(stage: cp.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult; +} + +/** + * The result of adding actions to the pipeline + */ +export interface CodePipelineActionFactoryResult { + /** + * How many RunOrders were consumed + */ + readonly runOrdersConsumed: number; + + /** + * If a CodeBuild project got created, the project + * + * @default - This factory did not create a CodeBuild project + */ + readonly project?: cb.IProject; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts new file mode 100644 index 0000000000000..d97b4c5f925de --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -0,0 +1,354 @@ +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import { Artifact } from '@aws-cdk/aws-codepipeline'; +import * as cp_actions from '@aws-cdk/aws-codepipeline-actions'; +import { Action, CodeCommitTrigger, GitHubTrigger, S3Trigger } from '@aws-cdk/aws-codepipeline-actions'; +import * as iam from '@aws-cdk/aws-iam'; +import { IBucket } from '@aws-cdk/aws-s3'; +import { SecretValue } from '@aws-cdk/core'; +import { FileSet, Step } from '../blueprint'; +import { CodePipelineActionFactoryResult, ProduceActionOptions, ICodePipelineActionFactory } from './codepipeline-action-factory'; + +/** + * CodePipeline source steps + * + * This class contains a number of factory methods for the different types + * of sources that CodePipeline supports. + */ +export abstract class CodePipelineSource extends Step implements ICodePipelineActionFactory { + /** + * Returns a GitHub source, using OAuth tokens to authenticate with + * GitHub and a separate webhook to detect changes. This is no longer + * the recommended method. Please consider using `connection()` + * instead. + * + * Pass in the owner and repository in a single string, like this: + * + * ```ts + * CodePipelineSource.gitHub('owner/repo', 'main'); + * ``` + * + * Authentication will be done by a secret called `github-token` in AWS + * Secrets Manager (unless specified otherwise). + * + * The token should have these permissions: + * + * * **repo** - to read the repository + * * **admin:repo_hook** - if you plan to use webhooks (true by default) + */ + public static gitHub(repoString: string, branch: string, props: GitHubSourceOptions = {}): CodePipelineSource { + return new GitHubSource(repoString, branch, props); + } + + /** + * Returns an S3 source. + * + * @param bucket The bucket where the source code is located. + * @param props The options, which include the key that identifies the source code file and + * and how the pipeline should be triggered. + * + * Example: + * + * ```ts + * const bucket: IBucket = ... + * CodePipelineSource.s3(bucket, { + * key: 'path/to/file.zip', + * }); + * ``` + */ + public static s3(bucket: IBucket, objectKey: string, props: S3SourceOptions = {}): CodePipelineSource { + return new S3Source(bucket, objectKey, props); + } + + /** + * Returns a CodeStar connection source. A CodeStar connection allows AWS CodePipeline to + * access external resources, such as repositories in GitHub, GitHub Enterprise or + * BitBucket. + * + * To use this method, you first need to create a CodeStar connection + * using the AWS console. In the process, you may have to sign in to the external provider + * -- GitHub, for example -- to authorize AWS to read and modify your repository. + * Once you have done this, copy the connection ARN and use it to create the source. + * + * Example: + * + * ```ts + * CodePipelineSource.connection('owner/repo', 'main', { + * connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console + * }); + * ``` + * + * @param repoString A string that encodes owner and repository separated by a slash (e.g. 'owner/repo'). + * @param branch The branch to use. + * @param props The source properties, including the connection ARN. + * + * @see https://docs.aws.amazon.com/dtconsole/latest/userguide/welcome-connections.html + */ + public static connection(repoString: string, branch: string, props: ConnectionSourceOptions): CodePipelineSource { + return new CodeStarConnectionSource(repoString, branch, props); + } + + /** + * Returns a CodeCommit source. + * + * @param repository The CodeCommit repository. + * @param branch The branch to use. + * @param props The source properties. + * + * Example: + * + * ```ts + * const repository: IRepository = ... + * CodePipelineSource.codeCommit(repository, 'main'); + * ``` + */ + public static codeCommit(repository: codecommit.IRepository, branch: string, props: CodeCommitSourceOptions = {}): CodePipelineSource { + return new CodeCommitSource(repository, branch, props); + } + + // tells `PipelineGraph` to hoist a "Source" step + public readonly isSource = true; + + public produceAction(stage: cp.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { + const output = options.artifacts.toCodePipeline(this.primaryOutput!); + const action = this.getAction(output, options.actionName, options.runOrder); + stage.addAction(action); + return { runOrdersConsumed: 1 }; + } + + protected abstract getAction(output: Artifact, actionName: string, runOrder: number): Action; +} + +/** + * Options for GitHub sources + */ +export interface GitHubSourceOptions { + /** + * A GitHub OAuth token to use for authentication. + * + * It is recommended to use a Secrets Manager `Secret` to obtain the token: + * + * ```ts + * const oauth = cdk.SecretValue.secretsManager('my-github-token'); + * new GitHubSource(this, 'GitHubSource', { oauthToken: oauth, ... }); + * ``` + * + * The GitHub Personal Access Token should have these scopes: + * + * * **repo** - to read the repository + * * **admin:repo_hook** - if you plan to use webhooks (true by default) + * + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/GitHub-create-personal-token-CLI.html + * + * @default - SecretValue.secretsManager('github-token') + */ + readonly authentication?: SecretValue; + + /** + * How AWS CodePipeline should be triggered + * + * With the default value "WEBHOOK", a webhook is created in GitHub that triggers the action. + * With "POLL", CodePipeline periodically checks the source for changes. + * With "None", the action is not triggered through changes in the source. + * + * To use `WEBHOOK`, your GitHub Personal Access Token should have + * **admin:repo_hook** scope (in addition to the regular **repo** scope). + * + * @default GitHubTrigger.WEBHOOK + */ + readonly trigger?: GitHubTrigger; + +} + +/** + * Extend CodePipelineSource so we can type-test in the CodePipelineEngine. + */ +class GitHubSource extends CodePipelineSource { + private readonly owner: string; + private readonly repo: string; + private readonly authentication: SecretValue; + + constructor(repoString: string, readonly branch: string, readonly props: GitHubSourceOptions) { + super(repoString); + + const parts = repoString.split('/'); + if (parts.length !== 2) { + throw new Error(`GitHub repository name should look like '/', got '${repoString}'`); + } + this.owner = parts[0]; + this.repo = parts[1]; + this.authentication = props.authentication ?? SecretValue.secretsManager('github-token'); + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, actionName: string, runOrder: number) { + return new cp_actions.GitHubSourceAction({ + output, + actionName, + runOrder, + oauthToken: this.authentication, + owner: this.owner, + repo: this.repo, + branch: this.branch, + trigger: this.props.trigger, + }); + } +} + +/** + * Options for S3 sources + */ +export interface S3SourceOptions { + /** + * How should CodePipeline detect source changes for this Action. + * Note that if this is S3Trigger.EVENTS, you need to make sure to include the source Bucket in a CloudTrail Trail, + * as otherwise the CloudWatch Events will not be emitted. + * + * @default S3Trigger.POLL + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/log-s3-data-events.html + */ + readonly trigger?: S3Trigger; +} + +class S3Source extends CodePipelineSource { + constructor(readonly bucket: IBucket, private readonly objectKey: string, readonly props: S3SourceOptions) { + super(bucket.bucketName); + + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, actionName: string, runOrder: number) { + return new cp_actions.S3SourceAction({ + output, + actionName, + runOrder, + bucketKey: this.objectKey, + trigger: this.props.trigger, + bucket: this.bucket, + }); + } +} + +/** + * Configuration options for CodeStar source + */ +export interface ConnectionSourceOptions { + /** + * The ARN of the CodeStar Connection created in the AWS console + * that has permissions to access this GitHub or BitBucket repository. + * + * @example 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh' + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/connections-create.html + */ + readonly connectionArn: string; + + + // long URL in @see + /** + * Whether the output should be the contents of the repository + * (which is the default), + * or a link that allows CodeBuild to clone the repository before building. + * + * **Note**: if this option is true, + * then only CodeBuild actions can use the resulting {@link output}. + * + * @default false + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodestarConnectionSource.html#action-reference-CodestarConnectionSource-config + */ + readonly codeBuildCloneOutput?: boolean; + + /** + * Controls automatically starting your pipeline when a new commit + * is made on the configured repository and branch. If unspecified, + * the default value is true, and the field does not display by default. + * + * @default true + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodestarConnectionSource.html + */ + readonly triggerOnPush?: boolean; +} + +class CodeStarConnectionSource extends CodePipelineSource { + private readonly owner: string; + private readonly repo: string; + + constructor(repoString: string, readonly branch: string, readonly props: ConnectionSourceOptions) { + super(repoString); + + const parts = repoString.split('/'); + if (parts.length !== 2) { + throw new Error(`CodeStar repository name should look like '/', got '${repoString}'`); + } + this.owner = parts[0]; + this.repo = parts[1]; + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, actionName: string, runOrder: number) { + return new cp_actions.CodeStarConnectionsSourceAction({ + output, + actionName, + runOrder, + connectionArn: this.props.connectionArn, + owner: this.owner, + repo: this.repo, + branch: this.branch, + codeBuildCloneOutput: this.props.codeBuildCloneOutput, + triggerOnPush: this.props.triggerOnPush, + }); + } +} + +/** + * Configuration options for a CodeCommit source + */ +export interface CodeCommitSourceOptions { + /** + * How should CodePipeline detect source changes for this Action. + * + * @default CodeCommitTrigger.EVENTS + */ + readonly trigger?: CodeCommitTrigger; + + /** + * Role to be used by on commit event rule. + * Used only when trigger value is CodeCommitTrigger.EVENTS. + * + * @default a new role will be created. + */ + readonly eventRole?: iam.IRole; + + /** + * Whether the output should be the contents of the repository + * (which is the default), + * or a link that allows CodeBuild to clone the repository before building. + * + * **Note**: if this option is true, + * then only CodeBuild actions can use the resulting {@link output}. + * + * @default false + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodeCommit.html + */ + readonly codeBuildCloneOutput?: boolean; +} + +class CodeCommitSource extends CodePipelineSource { + constructor(readonly repository: codecommit.IRepository, readonly branch: string, readonly props: CodeCommitSourceOptions) { + super(repository.repositoryName); + + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, actionName: string, runOrder: number) { + return new cp_actions.CodeCommitSourceAction({ + output, + actionName, + runOrder, + branch: this.branch, + trigger: this.props.trigger, + repository: this.repository, + eventRole: this.props.eventRole, + codeBuildCloneOutput: this.props.codeBuildCloneOutput, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts new file mode 100644 index 0000000000000..1c61a2c8cc42d --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts @@ -0,0 +1,961 @@ +import * as path from 'path'; +import * as cb from '@aws-cdk/aws-codebuild'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import * as cpa from '@aws-cdk/aws-codepipeline-actions'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { Aws, Fn, IDependable, Lazy, PhysicalName, Stack } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { Construct, Node } from 'constructs'; +import { AssetType, FileSet, IFileSetProducer, ManualApprovalStep, ShellStep, StackAsset, StackDeployment, Step } from '../blueprint'; +import { DockerCredential, dockerCredentialsInstallCommands, DockerCredentialUsage } from '../docker-credentials'; +import { GraphNode, GraphNodeCollection, isGraph, AGraphNode, PipelineGraph } from '../helpers-internal'; +import { PipelineBase } from '../main'; +import { appOf, assemblyBuilderOf, embeddedAsmPath, obtainScope } from '../private/construct-internals'; +import { toPosixPath } from '../private/fs'; +import { enumerate, flatten, maybeSuffix, noUndefined } from '../private/javascript'; +import { writeTemplateConfiguration } from '../private/template-configuration'; +import { CodeBuildFactory, mergeCodeBuildOptions, stackVariableNamespace } from './_codebuild-factory'; +import { ArtifactMap } from './artifact-map'; +import { CodeBuildStep } from './codebuild-step'; +import { CodePipelineActionFactoryResult, ICodePipelineActionFactory } from './codepipeline-action-factory'; + + +/** + * Properties for a `CodePipeline` + */ +export interface CodePipelineProps { + /** + * The build step that produces the CDK Cloud Assembly + * + * The primary output of this step needs to be the `cdk.out` directory + * generated by the `cdk synth` command. + * + * If you use a `ShellStep` here and you don't configure an output directory, + * the output directory will automatically be assumed to be `cdk.out`. + */ + readonly synth: IFileSetProducer; + + /** + * The name of the CodePipeline pipeline + * + * @default - Automatically generated + */ + readonly pipelineName?: string; + + /** + * Create KMS keys for the artifact buckets, allowing cross-account deployments + * + * The artifact buckets have to be encrypted to support deploying CDK apps to + * another account, so if you want to do that or want to have your artifact + * buckets encrypted, be sure to set this value to `true`. + * + * Be aware there is a cost associated with maintaining the KMS keys. + * + * @default false + */ + readonly crossAccountKeys?: boolean; + + /** + * CDK CLI version to use in self-mutation and asset publishing steps + * + * If you want to lock the CDK CLI version used in the pipeline, by steps + * that are automatically generated for you, specify the version here. + * + * You should not typically need to specify this value. + * + * @default - Latest version + */ + readonly cliVersion?: string; + + /** + * Whether the pipeline will update itself + * + * This needs to be set to `true` to allow the pipeline to reconfigure + * itself when assets or stages are being added to it, and `true` is the + * recommended setting. + * + * You can temporarily set this to `false` while you are iterating + * on the pipeline itself and prefer to deploy changes using `cdk deploy`. + * + * @default true + */ + readonly selfMutation?: boolean; + + /** + * Enable Docker for the self-mutate step + * + * Set this to true if the pipeline itself uses Docker container assets + * (for example, if you use `LinuxBuildImage.fromAsset()` as the build + * image of a CodeBuild step in the pipeline). + * + * You do not need to set it if you build Docker image assets in the + * application Stages and Stacks that are *deployed* by this pipeline. + * + * Configures privileged mode for the self-mutation CodeBuild action. + * + * If you are about to turn this on in an already-deployed Pipeline, + * set the value to `true` first, commit and allow the pipeline to + * self-update, and only then use the Docker asset in the pipeline. + * + * @default false + */ + readonly dockerEnabledForSelfMutation?: boolean; + + /** + * Enable Docker for the 'synth' step + * + * Set this to true if you are using file assets that require + * "bundling" anywhere in your application (meaning an asset + * compilation step will be run with the tools provided by + * a Docker image), both for the Pipeline stack as well as the + * application stacks. + * + * A common way to use bundling assets in your application is by + * using the `@aws-cdk/aws-lambda-nodejs` library. + * + * Configures privileged mode for the synth CodeBuild action. + * + * If you are about to turn this on in an already-deployed Pipeline, + * set the value to `true` first, commit and allow the pipeline to + * self-update, and only then use the bundled asset. + * + * @default false + */ + readonly dockerEnabledForSynth?: boolean; + + /** + * Customize the CodeBuild projects created for this pipeline + * + * @default - All projects run non-privileged build, SMALL instance, LinuxBuildImage.STANDARD_5_0 + */ + readonly codeBuildDefaults?: CodeBuildOptions; + + /** + * Additional customizations to apply to the asset publishing CodeBuild projects + * + * @default - Only `codeBuildProjectDefaults` are applied + */ + readonly assetPublishingCodeBuildDefaults?: CodeBuildOptions; + + /** + * Additional customizations to apply to the self mutation CodeBuild projects + * + * @default - Only `codeBuildProjectDefaults` are applied + */ + readonly selfMutationCodeBuildDefaults?: CodeBuildOptions; + + /** + * Publish assets in multiple CodeBuild projects + * + * If set to false, use one Project per type to publish all assets. + * + * Publishing in parallel improves concurrency and may reduce publishing + * latency, but may also increase overall provisioning time of the CodeBuild + * projects. + * + * Experiment and see what value works best for you. + * + * @default true + */ + readonly publishAssetsInParallel?: boolean; + + /** + * A list of credentials used to authenticate to Docker registries. + * + * Specify any credentials necessary within the pipeline to build, synth, update, or publish assets. + * + * @default [] + */ + readonly dockerCredentials?: DockerCredential[]; + + /** + * An existing Pipeline to be reused and built upon. + * + * [disable-awslint:ref-via-interface] + * + * @default - a new underlying pipeline is created. + */ + readonly codePipeline?: cp.Pipeline; +} + +/** + * Options for customizing a single CodeBuild project + */ +export interface CodeBuildOptions { + /** + * Partial build environment, will be combined with other build environments that apply + * + * @default - Non-privileged build, SMALL instance, LinuxBuildImage.STANDARD_5_0 + */ + readonly buildEnvironment?: cb.BuildEnvironment; + + /** + * Policy statements to add to role + * + * @default - No policy statements added to CodeBuild Project Role + */ + readonly rolePolicy?: iam.PolicyStatement[]; + + /** + * Partial buildspec, will be combined with other buildspecs that apply + * + * The BuildSpec must be available inline--it cannot reference a file + * on disk. + * + * @default - No initial BuildSpec + */ + readonly partialBuildSpec?: cb.BuildSpec; + + /** + * Which security group(s) to associate with the project network interfaces. + * + * Only used if 'vpc' is supplied. + * + * @default - Security group will be automatically created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * The VPC where to create the CodeBuild network interfaces in. + * + * @default - No VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * Only used if 'vpc' is supplied. + * + * @default - All private subnets. + */ + readonly subnetSelection?: ec2.SubnetSelection; +} + + +/** + * A CDK Pipeline that uses CodePipeline to deploy CDK apps + * + * This is a `Pipeline` with its `engine` property set to + * `CodePipelineEngine`, and exists for nicer ergonomics for + * users that don't need to switch out engines. + */ +export class CodePipeline extends PipelineBase { + private _pipeline?: cp.Pipeline; + private artifacts = new ArtifactMap(); + private _synthProject?: cb.IProject; + private readonly selfMutation: boolean; + private _myCxAsmRoot?: string; + private readonly dockerCredentials: DockerCredential[]; + + /** + * Asset roles shared for publishing + */ + private readonly assetCodeBuildRoles: Record = {}; + + /** + * Policies created for the build projects that they have to depend on + */ + private readonly assetAttachedPolicies: Record = {}; + + /** + * Per asset type, the target role ARNs that need to be assumed + */ + private readonly assetPublishingRoles: Record> = {}; + + /** + * This is set to the very first artifact produced in the pipeline + */ + private _fallbackArtifact?: cp.Artifact; + + private _cloudAssemblyFileSet?: FileSet; + + private readonly singlePublisherPerAssetType: boolean; + + constructor(scope: Construct, id: string, private readonly props: CodePipelineProps) { + super(scope, id, props); + + this.selfMutation = props.selfMutation ?? true; + this.dockerCredentials = props.dockerCredentials ?? []; + this.singlePublisherPerAssetType = !(props.publishAssetsInParallel ?? true); + } + + /** + * The CodeBuild project that performs the Synth + * + * Only available after the pipeline has been built. + */ + public get synthProject(): cb.IProject { + if (!this._synthProject) { + throw new Error('Call pipeline.buildPipeline() before reading this property'); + } + return this._synthProject; + } + + /** + * The CodePipeline pipeline that deploys the CDK app + * + * Only available after the pipeline has been built. + */ + public get pipeline(): cp.Pipeline { + if (!this._pipeline) { + throw new Error('Pipeline not created yet'); + } + return this._pipeline; + } + + + protected doBuildPipeline(): void { + if (this._pipeline) { + throw new Error('Pipeline already created'); + } + + this._myCxAsmRoot = path.resolve(assemblyBuilderOf(appOf(this)).outdir); + + if (this.props.codePipeline) { + if (this.props.pipelineName) { + throw new Error('Cannot set \'pipelineName\' if an existing CodePipeline is given using \'codePipeline\''); + } + if (this.props.crossAccountKeys !== undefined) { + throw new Error('Cannot set \'crossAccountKeys\' if an existing CodePipeline is given using \'codePipeline\''); + } + + this._pipeline = this.props.codePipeline; + } else { + this._pipeline = new cp.Pipeline(this, 'Pipeline', { + pipelineName: this.props.pipelineName, + crossAccountKeys: this.props.crossAccountKeys ?? false, + // This is necessary to make self-mutation work (deployments are guaranteed + // to happen only after the builds of the latest pipeline definition). + restartExecutionOnUpdate: true, + }); + } + + const graphFromBp = new PipelineGraph(this, { + selfMutation: this.selfMutation, + singlePublisherPerAssetType: this.singlePublisherPerAssetType, + }); + this._cloudAssemblyFileSet = graphFromBp.cloudAssemblyFileSet; + + this.pipelineStagesAndActionsFromGraph(graphFromBp); + } + + private get myCxAsmRoot(): string { + if (!this._myCxAsmRoot) { + throw new Error('Can\'t read \'myCxAsmRoot\' if build deployment not called yet'); + } + return this._myCxAsmRoot; + } + + /** + * Scope for Assets-related resources. + * + * Purely exists for construct tree backwards compatibility with legacy pipelines + */ + private get assetsScope(): Construct { + return obtainScope(this, 'Assets'); + } + + private pipelineStagesAndActionsFromGraph(structure: PipelineGraph) { + // Translate graph into Pipeline Stages and Actions + let beforeSelfMutation = this.selfMutation; + for (const stageNode of flatten(structure.graph.sortedChildren())) { + if (!isGraph(stageNode)) { + throw new Error(`Top-level children must be graphs, got '${stageNode}'`); + } + + // Group our ordered tranches into blocks of 50. + // We can map these onto stages without exceeding the capacity of a Stage. + const chunks = chunkTranches(50, stageNode.sortedLeaves()); + const actionsOverflowStage = chunks.length > 1; + for (const [i, tranches] of enumerate(chunks)) { + const stageName = actionsOverflowStage ? `${stageNode.id}.${i + 1}` : stageNode.id; + const pipelineStage = this.pipeline.addStage({ stageName }); + + const sharedParent = new GraphNodeCollection(flatten(tranches)).commonAncestor(); + + let runOrder = 1; + for (const tranche of tranches) { + const runOrdersConsumed = [0]; + + for (const node of tranche) { + const factory = this.actionFromNode(node); + + const nodeType = this.nodeTypeFromNode(node); + + const result = factory.produceAction(pipelineStage, { + actionName: actionName(node, sharedParent), + runOrder, + artifacts: this.artifacts, + scope: obtainScope(this.pipeline, stageName), + fallbackArtifact: this._fallbackArtifact, + pipeline: this, + // If this step happens to produce a CodeBuild job, set the default options + codeBuildDefaults: nodeType ? this.codeBuildDefaultsFor(nodeType) : undefined, + beforeSelfMutation, + }); + + if (node.data?.type === 'self-update') { + beforeSelfMutation = false; + } + + this.postProcessNode(node, result); + + runOrdersConsumed.push(result.runOrdersConsumed); + } + + runOrder += Math.max(...runOrdersConsumed); + } + } + } + } + + /** + * Do additional things after the action got added to the pipeline + * + * Some minor state manipulation of CodeBuild projects and pipeline + * artifacts. + */ + private postProcessNode(node: AGraphNode, result: CodePipelineActionFactoryResult) { + const nodeType = this.nodeTypeFromNode(node); + + if (result.project) { + const dockerUsage = dockerUsageFromCodeBuild(nodeType ?? CodeBuildProjectType.STEP); + if (dockerUsage) { + for (const c of this.dockerCredentials) { + c.grantRead(result.project, dockerUsage); + } + } + + if (nodeType === CodeBuildProjectType.SYNTH) { + this._synthProject = result.project; + } + } + + if (node.data?.type === 'step' && node.data.step.primaryOutput?.primaryOutput && !this._fallbackArtifact) { + this._fallbackArtifact = this.artifacts.toCodePipeline(node.data.step.primaryOutput?.primaryOutput); + } + } + + /** + * Make an action from the given node and/or step + */ + private actionFromNode(node: AGraphNode): ICodePipelineActionFactory { + switch (node.data?.type) { + // Nothing for these, they are groupings (shouldn't even have popped up here) + case 'group': + case 'stack-group': + case undefined: + throw new Error(`actionFromNode: did not expect to get group nodes: ${node.data?.type}`); + + case 'self-update': + return this.selfMutateAction(); + + case 'publish-assets': + return this.publishAssetsAction(node, node.data.assets); + + case 'prepare': + return this.createChangeSetAction(node.data.stack); + + case 'execute': + return this.executeChangeSetAction(node.data.stack, node.data.captureOutputs); + + case 'step': + return this.actionFromStep(node, node.data.step); + } + } + + /** + * Take a Step and turn it into a CodePipeline Action + * + * There are only 3 types of Steps we need to support: + * + * - RunScript (generic) + * - ManualApproval (generic) + * - CodePipelineActionFactory (CodePipeline-specific) + * + * The rest is expressed in terms of these 3, or in terms of graph nodes + * which are handled elsewhere. + */ + private actionFromStep(node: AGraphNode, step: Step): ICodePipelineActionFactory { + const nodeType = this.nodeTypeFromNode(node); + + // CodePipeline-specific steps first -- this includes Sources + if (isCodePipelineActionFactory(step)) { + return step; + } + + // Now built-in steps + if (step instanceof ShellStep || step instanceof CodeBuildStep) { + // The 'CdkBuildProject' will be the construct ID of the CodeBuild project, necessary for backwards compat + let constructId = nodeType === CodeBuildProjectType.SYNTH + ? 'CdkBuildProject' + : step.id; + + return step instanceof CodeBuildStep + ? CodeBuildFactory.fromCodeBuildStep(constructId, step) + : CodeBuildFactory.fromShellStep(constructId, step); + } + + if (step instanceof ManualApprovalStep) { + return { + produceAction: (stage, options) => { + stage.addAction(new cpa.ManualApprovalAction({ + actionName: options.actionName, + runOrder: options.runOrder, + additionalInformation: step.comment, + })); + return { runOrdersConsumed: 1 }; + }, + }; + } + + throw new Error(`Deployment step '${step}' is not supported for CodePipeline-backed pipelines`); + } + + private createChangeSetAction(stack: StackDeployment): ICodePipelineActionFactory { + const changeSetName = 'PipelineChange'; + + const templateArtifact = this.artifacts.toCodePipeline(this._cloudAssemblyFileSet!); + const templateConfigurationPath = this.writeTemplateConfiguration(stack); + + const region = stack.region !== Stack.of(this).region ? stack.region : undefined; + const account = stack.account !== Stack.of(this).account ? stack.account : undefined; + + const relativeTemplatePath = path.relative(this.myCxAsmRoot, stack.absoluteTemplatePath); + + return { + produceAction: (stage, options) => { + stage.addAction(new cpa.CloudFormationCreateReplaceChangeSetAction({ + actionName: options.actionName, + runOrder: options.runOrder, + changeSetName, + stackName: stack.stackName, + templatePath: templateArtifact.atPath(toPosixPath(relativeTemplatePath)), + adminPermissions: true, + role: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.assumeRoleArn), + deploymentRole: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.executionRoleArn), + region: region, + templateConfiguration: templateConfigurationPath + ? templateArtifact.atPath(toPosixPath(templateConfigurationPath)) + : undefined, + })); + return { runOrdersConsumed: 1 }; + }, + }; + } + + private executeChangeSetAction(stack: StackDeployment, captureOutputs: boolean): ICodePipelineActionFactory { + const changeSetName = 'PipelineChange'; + + const region = stack.region !== Stack.of(this).region ? stack.region : undefined; + const account = stack.account !== Stack.of(this).account ? stack.account : undefined; + + return { + produceAction: (stage, options) => { + stage.addAction(new cpa.CloudFormationExecuteChangeSetAction({ + actionName: options.actionName, + runOrder: options.runOrder, + changeSetName, + stackName: stack.stackName, + role: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.assumeRoleArn), + region: region, + variablesNamespace: captureOutputs ? stackVariableNamespace(stack) : undefined, + })); + + return { runOrdersConsumed: 1 }; + }, + }; + } + + private selfMutateAction(): ICodePipelineActionFactory { + const installSuffix = this.props.cliVersion ? `@${this.props.cliVersion}` : ''; + + const pipelineStack = Stack.of(this.pipeline); + const pipelineStackIdentifier = pipelineStack.node.path ?? pipelineStack.stackName; + + const step = new CodeBuildStep('SelfMutate', { + projectName: maybeSuffix(this.props.pipelineName, '-selfupdate'), + input: this._cloudAssemblyFileSet, + installCommands: [ + `npm install -g aws-cdk${installSuffix}`, + ], + commands: [ + `cdk -a ${toPosixPath(embeddedAsmPath(this.pipeline))} deploy ${pipelineStackIdentifier} --require-approval=never --verbose`, + ], + + rolePolicyStatements: [ + // allow the self-mutating project permissions to assume the bootstrap Action role + new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: [`arn:*:iam::${Stack.of(this.pipeline).account}:role/*`], + conditions: { + 'ForAnyValue:StringEquals': { + 'iam:ResourceTag/aws-cdk:bootstrap-role': ['image-publishing', 'file-publishing', 'deploy'], + }, + }, + }), + new iam.PolicyStatement({ + actions: ['cloudformation:DescribeStacks'], + resources: ['*'], // this is needed to check the status of the bootstrap stack when doing `cdk deploy` + }), + // S3 checks for the presence of the ListBucket permission + new iam.PolicyStatement({ + actions: ['s3:ListBucket'], + resources: ['*'], + }), + ], + }); + + // Different on purpose -- id needed for backwards compatible LogicalID + return CodeBuildFactory.fromCodeBuildStep('SelfMutation', step, { + additionalConstructLevel: false, + scope: obtainScope(this, 'UpdatePipeline'), + }); + } + + private publishAssetsAction(node: AGraphNode, assets: StackAsset[]): ICodePipelineActionFactory { + const installSuffix = this.props.cliVersion ? `@${this.props.cliVersion}` : ''; + + const commands = assets.map(asset => { + const relativeAssetManifestPath = path.relative(this.myCxAsmRoot, asset.assetManifestPath); + return `cdk-assets --path "${toPosixPath(relativeAssetManifestPath)}" --verbose publish "${asset.assetSelector}"`; + }); + + const assetType = assets[0].assetType; + if (assets.some(a => a.assetType !== assetType)) { + throw new Error('All assets in a single publishing step must be of the same type'); + } + + const publishingRoles = this.assetPublishingRoles[assetType] = (this.assetPublishingRoles[assetType] ?? new Set()); + for (const asset of assets) { + if (asset.assetPublishingRoleArn) { + publishingRoles.add(asset.assetPublishingRoleArn); + } + } + + const assetBuildConfig = this.obtainAssetCodeBuildRole(assets[0].assetType); + + // The base commands that need to be run + const script = new CodeBuildStep(node.id, { + commands, + installCommands: [ + `npm install -g cdk-assets${installSuffix}`, + ], + input: this._cloudAssemblyFileSet, + buildEnvironment: { + privileged: assets.some(asset => asset.assetType === AssetType.DOCKER_IMAGE), + }, + role: assetBuildConfig.role, + }); + + // Customizations that are not accessible to regular users + return CodeBuildFactory.fromCodeBuildStep(node.id, script, { + additionalConstructLevel: false, + additionalDependable: assetBuildConfig.dependable, + + // If we use a single publisher, pass buildspec via file otherwise it'll + // grow too big. + passBuildSpecViaCloudAssembly: this.singlePublisherPerAssetType, + scope: this.assetsScope, + }); + } + + private nodeTypeFromNode(node: AGraphNode) { + if (node.data?.type === 'step') { + return !!node.data?.isBuildStep ? CodeBuildProjectType.SYNTH : CodeBuildProjectType.STEP; + } + if (node.data?.type === 'publish-assets') { + return CodeBuildProjectType.ASSETS; + } + if (node.data?.type === 'self-update') { + return CodeBuildProjectType.SELF_MUTATE; + } + return undefined; + } + + private codeBuildDefaultsFor(nodeType: CodeBuildProjectType): CodeBuildOptions | undefined { + const defaultOptions: CodeBuildOptions = { + buildEnvironment: { + buildImage: cb.LinuxBuildImage.STANDARD_5_0, + computeType: cb.ComputeType.SMALL, + }, + }; + + const typeBasedCustomizations = { + [CodeBuildProjectType.SYNTH]: this.props.dockerEnabledForSynth + ? { buildEnvironment: { privileged: true } } + : {}, + + [CodeBuildProjectType.ASSETS]: this.props.assetPublishingCodeBuildDefaults, + + [CodeBuildProjectType.SELF_MUTATE]: this.props.dockerEnabledForSelfMutation + ? mergeCodeBuildOptions(this.props.selfMutationCodeBuildDefaults, { buildEnvironment: { privileged: true } }) + : this.props.selfMutationCodeBuildDefaults, + + [CodeBuildProjectType.STEP]: {}, + }; + + const dockerUsage = dockerUsageFromCodeBuild(nodeType); + const dockerCommands = dockerUsage !== undefined + ? dockerCredentialsInstallCommands(dockerUsage, this.dockerCredentials, 'both') + : []; + const typeBasedDockerCommands = dockerCommands.length > 0 ? { + partialBuildSpec: cb.BuildSpec.fromObject({ + version: '0.2', + phases: { + pre_build: { + commands: dockerCommands, + }, + }, + }), + } : {}; + + return mergeCodeBuildOptions( + defaultOptions, + this.props.codeBuildDefaults, + typeBasedCustomizations[nodeType], + typeBasedDockerCommands, + ); + } + + private roleFromPlaceholderArn(scope: Construct, region: string | undefined, + account: string | undefined, arn: string): iam.IRole; + private roleFromPlaceholderArn(scope: Construct, region: string | undefined, + account: string | undefined, arn: string | undefined): iam.IRole | undefined; + private roleFromPlaceholderArn(scope: Construct, region: string | undefined, + account: string | undefined, arn: string | undefined): iam.IRole | undefined { + + if (!arn) { return undefined; } + + // Use placeholdered arn as construct ID. + const id = arn; + + // https://github.com/aws/aws-cdk/issues/7255 + let existingRole = Node.of(scope).tryFindChild(`ImmutableRole${id}`) as iam.IRole; + if (existingRole) { return existingRole; } + // For when #7255 is fixed. + existingRole = Node.of(scope).tryFindChild(id) as iam.IRole; + if (existingRole) { return existingRole; } + + const arnToImport = cxapi.EnvironmentPlaceholders.replace(arn, { + region: region ?? Aws.REGION, + accountId: account ?? Aws.ACCOUNT_ID, + partition: Aws.PARTITION, + }); + return iam.Role.fromRoleArn(scope, id, arnToImport, { mutable: false, addGrantsToResources: true }); + } + + /** + * Non-template config files for CodePipeline actions + * + * Currently only supports tags. + */ + private writeTemplateConfiguration(stack: StackDeployment): string | undefined { + if (Object.keys(stack.tags).length === 0) { return undefined; } + + const absConfigPath = `${stack.absoluteTemplatePath}.config.json`; + const relativeConfigPath = path.relative(this.myCxAsmRoot, absConfigPath); + + // Write the template configuration file (for parameters into CreateChangeSet call that + // cannot be configured any other way). They must come from a file, and there's unfortunately + // no better hook to write this file (`construct.onSynthesize()` would have been the prime candidate + // but that is being deprecated--and DeployCdkStackAction isn't even a construct). + writeTemplateConfiguration(absConfigPath, { + Tags: noUndefined(stack.tags), + }); + + return relativeConfigPath; + } + + /** + * This role is used by both the CodePipeline build action and related CodeBuild project. Consolidating these two + * roles into one, and re-using across all assets, saves significant size of the final synthesized output. + * Modeled after the CodePipeline role and 'CodePipelineActionRole' roles. + * Generates one role per asset type to separate file and Docker/image-based permissions. + */ + private obtainAssetCodeBuildRole(assetType: AssetType): AssetCodeBuildRole { + if (this.assetCodeBuildRoles[assetType]) { + return { + role: this.assetCodeBuildRoles[assetType], + dependable: this.assetAttachedPolicies[assetType], + }; + } + + const stack = Stack.of(this); + + const rolePrefix = assetType === AssetType.DOCKER_IMAGE ? 'Docker' : 'File'; + const assetRole = new iam.Role(this.assetsScope, `${rolePrefix}Role`, { + roleName: PhysicalName.GENERATE_IF_NEEDED, + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('codebuild.amazonaws.com'), + new iam.AccountPrincipal(stack.account), + ), + }); + + // Logging permissions + const logGroupArn = stack.formatArn({ + service: 'logs', + resource: 'log-group', + sep: ':', + resourceName: '/aws/codebuild/*', + }); + assetRole.addToPolicy(new iam.PolicyStatement({ + resources: [logGroupArn], + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + })); + + // CodeBuild report groups + const codeBuildArn = stack.formatArn({ + service: 'codebuild', + resource: 'report-group', + resourceName: '*', + }); + assetRole.addToPolicy(new iam.PolicyStatement({ + actions: [ + 'codebuild:CreateReportGroup', + 'codebuild:CreateReport', + 'codebuild:UpdateReport', + 'codebuild:BatchPutTestCases', + 'codebuild:BatchPutCodeCoverages', + ], + resources: [codeBuildArn], + })); + + // CodeBuild start/stop + assetRole.addToPolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 'codebuild:BatchGetBuilds', + 'codebuild:StartBuild', + 'codebuild:StopBuild', + ], + })); + + // Publishing role access + // The ARNs include raw AWS pseudo parameters (e.g., ${AWS::Partition}), which need to be substituted. + // Lazy-evaluated so all asset publishing roles are included. + assetRole.addToPolicy(new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: Lazy.list({ produce: () => Array.from(this.assetPublishingRoles[assetType] ?? []).map(arn => Fn.sub(arn)) }), + })); + + // Grant pull access for any ECR registries and secrets that exist + if (assetType === AssetType.DOCKER_IMAGE) { + this.dockerCredentials.forEach(reg => reg.grantRead(assetRole, DockerCredentialUsage.ASSET_PUBLISHING)); + } + + // Artifact access + this.pipeline.artifactBucket.grantRead(assetRole); + + // VPC permissions required for CodeBuild + // Normally CodeBuild itself takes care of this but we're creating a singleton role so now + // we need to do this. + const assetCodeBuildOptions = this.codeBuildDefaultsFor(CodeBuildProjectType.ASSETS); + if (assetCodeBuildOptions?.vpc) { + const vpcPolicy = new iam.Policy(assetRole, 'VpcPolicy', { + statements: [ + new iam.PolicyStatement({ + resources: [`arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:network-interface/*`], + actions: ['ec2:CreateNetworkInterfacePermission'], + conditions: { + StringEquals: { + 'ec2:Subnet': assetCodeBuildOptions.vpc + .selectSubnets(assetCodeBuildOptions.subnetSelection).subnetIds + .map(si => `arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:subnet/${si}`), + 'ec2:AuthorizedService': 'codebuild.amazonaws.com', + }, + }, + }), + new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeVpcs', + ], + }), + ], + }); + assetRole.attachInlinePolicy(vpcPolicy); + this.assetAttachedPolicies[assetType] = vpcPolicy; + } + + this.assetCodeBuildRoles[assetType] = assetRole.withoutPolicyUpdates(); + return { + role: this.assetCodeBuildRoles[assetType], + dependable: this.assetAttachedPolicies[assetType], + }; + } +} + +function dockerUsageFromCodeBuild(cbt: CodeBuildProjectType): DockerCredentialUsage | undefined { + switch (cbt) { + case CodeBuildProjectType.ASSETS: return DockerCredentialUsage.ASSET_PUBLISHING; + case CodeBuildProjectType.SELF_MUTATE: return DockerCredentialUsage.SELF_UPDATE; + case CodeBuildProjectType.SYNTH: return DockerCredentialUsage.SYNTH; + case CodeBuildProjectType.STEP: return undefined; + } +} + +interface AssetCodeBuildRole { + readonly role: iam.IRole; + readonly dependable?: IDependable; +} + +enum CodeBuildProjectType { + SYNTH = 'SYNTH', + ASSETS = 'ASSETS', + SELF_MUTATE = 'SELF_MUTATE', + STEP = 'STEP', +} + +function actionName(node: GraphNode, parent: GraphNode) { + const names = node.ancestorPath(parent).map(n => n.id); + return names.map(sanitizeName).join('.'); +} + +function sanitizeName(x: string): string { + return x.replace(/[^A-Za-z0-9.@\-_]/g, '_'); +} + +/** + * Take a set of tranches and split them up into groups so + * that no set of tranches has more than n items total + */ +function chunkTranches(n: number, xss: A[][]): A[][][] { + const ret: A[][][] = []; + + while (xss.length > 0) { + const tranches: A[][] = []; + let count = 0; + + while (xss.length > 0) { + const xs = xss[0]; + const spaceRemaining = n - count; + if (xs.length <= spaceRemaining) { + tranches.push(xs); + count += xs.length; + xss.shift(); + } else { + tranches.push(xs.splice(0, spaceRemaining)); + count = n; + break; + } + } + + ret.push(tranches); + } + + + return ret; +} + +function isCodePipelineActionFactory(x: any): x is ICodePipelineActionFactory { + return !!(x as ICodePipelineActionFactory).produceAction; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts new file mode 100644 index 0000000000000..00e10509bb0df --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts @@ -0,0 +1,5 @@ +export * from './artifact-map'; +export * from './codebuild-step'; +export * from './codepipeline'; +export * from './codepipeline-action-factory'; +export * from './codepipeline-source'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/docker-credentials.ts b/packages/@aws-cdk/pipelines/lib/docker-credentials.ts index a2a5b2ca39d64..77b7d2c1b4381 100644 --- a/packages/@aws-cdk/pipelines/lib/docker-credentials.ts +++ b/packages/@aws-cdk/pipelines/lib/docker-credentials.ts @@ -104,11 +104,11 @@ export interface EcrDockerCredentialOptions { /** Defines which stages of a pipeline require the specified credentials */ export enum DockerCredentialUsage { /** Synth/Build */ - SYNTH, + SYNTH = 'SYNTH', /** Self-update */ - SELF_UPDATE, + SELF_UPDATE = 'SELF_UPDATE', /** Asset publishing */ - ASSET_PUBLISHING, + ASSET_PUBLISHING = 'ASSET_PUBLISHING', }; /** DockerCredential defined by registry domain and a secret */ @@ -202,7 +202,7 @@ interface DockerCredentialCredentialSource { export function dockerCredentialsInstallCommands( usage: DockerCredentialUsage, registries?: DockerCredential[], - osType?: ec2.OperatingSystemType): string[] { + osType?: ec2.OperatingSystemType | 'both'): string[] { const relevantRegistries = (registries ?? []).filter(reg => reg._applicableForUsage(usage)); if (!relevantRegistries || relevantRegistries.length === 0) { return []; } @@ -216,15 +216,25 @@ export function dockerCredentialsInstallCommands( domainCredentials, }; - if (osType === ec2.OperatingSystemType.WINDOWS) { + const windowsCommands = [ + 'mkdir %USERPROFILE%\\.cdk', + `echo '${JSON.stringify(cdkAssetsConfigFile)}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, + ]; + + const linuxCommands = [ + 'mkdir $HOME/.cdk', + `echo '${JSON.stringify(cdkAssetsConfigFile)}' > $HOME/.cdk/cdk-docker-creds.json`, + ]; + + if (osType === 'both') { return [ - 'mkdir %USERPROFILE%\\.cdk', - `echo '${JSON.stringify(cdkAssetsConfigFile)}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, + // These tags are magic and will be stripped when rendering the project + ...windowsCommands.map(c => `!WINDOWS!${c}`), + ...linuxCommands.map(c => `!LINUX!${c}`), ]; + } else if (osType === ec2.OperatingSystemType.WINDOWS) { + return windowsCommands; } else { - return [ - 'mkdir $HOME/.cdk', - `echo '${JSON.stringify(cdkAssetsConfigFile)}' > $HOME/.cdk/cdk-docker-creds.json`, - ]; + return linuxCommands; } } diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts new file mode 100644 index 0000000000000..6b1c2d85ee701 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts @@ -0,0 +1,385 @@ +/** + * A library for nested graphs + */ +import { addAll, extract, flatMap, isDefined } from '../private/javascript'; +import { topoSort } from './toposort'; + +export interface GraphNodeProps { + readonly data?: A; +} + +export class GraphNode { + public static of(id: string, data: A) { + return new GraphNode(id, { data }); + } + + public readonly dependencies: GraphNode[] = []; + public readonly data?: A; + private _parentGraph?: Graph; + + constructor(public readonly id: string, props: GraphNodeProps = {}) { + this.data = props.data; + } + + /** + * A graph-wide unique identifier for this node. Rendered by joining the IDs + * of all ancestors with hyphens. + */ + public get uniqueId(): string { + return this.ancestorPath(this.root).map(x => x.id).join('-'); + } + + /** + * The union of all dependencies of this node and the dependencies of all + * parent graphs. + */ + public get allDeps(): GraphNode[] { + const fromParent = this.parentGraph?.allDeps ?? []; + return [...this.dependencies, ...fromParent]; + } + + public dependOn(...dependencies: Array | undefined>) { + if (dependencies.includes(this)) { + throw new Error(`Cannot add dependency on self: ${this}`); + } + this.dependencies.push(...dependencies.filter(isDefined)); + } + + public ancestorPath(upTo: GraphNode): GraphNode[] { + let x: GraphNode = this; + const ret = [x]; + while (x.parentGraph && x.parentGraph !== upTo) { + x = x.parentGraph; + ret.unshift(x); + } + return ret; + } + + public rootPath(): GraphNode[] { + let x: GraphNode = this; + const ret = [x]; + while (x.parentGraph) { + x = x.parentGraph; + ret.unshift(x); + } + return ret; + } + + public get root() { + let x: GraphNode = this; + while (x.parentGraph) { + x = x.parentGraph; + } + return x; + } + + public get parentGraph() { + return this._parentGraph; + } + + /** + * @internal + */ + public _setParentGraph(parentGraph: Graph) { + if (this._parentGraph) { + throw new Error('Node already has a parent'); + } + this._parentGraph = parentGraph; + } + + public toString() { + return `${this.constructor.name}(${this.id})`; + } +} + +/** + * A dependency set that can be constructed partially and later finished + * + * It doesn't matter in what order sources and targets for the dependency + * relationship(s) get added. This class can serve as a synchronization + * point if the order in which graph nodes get added to the graph is not + * well-defined. + * + * Useful utility during graph building. + */ +export class DependencyBuilder { + private readonly targets: GraphNode[] = []; + private readonly sources: GraphNode[] = []; + + public dependOn(...targets: GraphNode[]) { + for (const target of targets) { + for (const source of this.sources) { + source.dependOn(target); + } + this.targets.push(target); + } + return this; + } + + public dependBy(...sources: GraphNode[]) { + for (const source of sources) { + for (const target of this.targets) { + source.dependOn(target); + } + this.sources.push(source); + } + return this; + } +} + +export class DependencyBuilders { + private readonly builders = new Map>(); + + public get(key: K) { + const b = this.builders.get(key); + if (b) { return b; } + const ret = new DependencyBuilder(); + this.builders.set(key, ret); + return ret; + } +} + +export interface GraphProps extends GraphNodeProps { + /** + * Initial nodes in the workflow + */ + readonly nodes?: GraphNode[]; +} + +export class Graph extends GraphNode { + public static of(id: string, data: A, nodes?: GraphNode[]) { + return new Graph(id, { data, nodes }); + } + + private readonly children = new Map>(); + + constructor(name: string, props: GraphProps={}) { + super(name, props); + + if (props.nodes) { + this.add(...props.nodes); + } + } + + public get nodes() { + return new Set(this.children.values()); + } + + public tryGetChild(name: string) { + return this.children.get(name); + } + + public contains(node: GraphNode) { + return this.nodes.has(node); + } + + public add(...nodes: Array>) { + for (const node of nodes) { + node._setParentGraph(this); + if (this.children.has(node.id)) { + throw new Error(`Node with duplicate id: ${node.id}`); + } + this.children.set(node.id, node); + } + } + + public absorb(other: Graph) { + this.add(...other.nodes); + } + + /** + * Return topologically sorted tranches of nodes at this graph level + */ + public sortedChildren(): GraphNode[][] { + // Project dependencies to current children + const nodes = this.nodes; + const projectedDependencies = projectDependencies(this.deepDependencies(), (node) => { + while (!nodes.has(node) && node.parentGraph) { + node = node.parentGraph; + } + return nodes.has(node) ? [node] : []; + }); + + return topoSort(nodes, projectedDependencies); + } + + /** + * Return a topologically sorted list of non-Graph nodes in the entire subgraph + */ + public sortedLeaves(): GraphNode[][] { + // Project dependencies to leaf nodes + const descendantsMap = new Map, GraphNode[]>(); + findDescendants(this); + + function findDescendants(node: GraphNode): GraphNode[] { + const ret: GraphNode[] = []; + + if (node instanceof Graph) { + for (const child of node.nodes) { + ret.push(...findDescendants(child)); + } + } else { + ret.push(node); + } + + descendantsMap.set(node, ret); + return ret; + } + + const projectedDependencies = projectDependencies(this.deepDependencies(), (node) => descendantsMap.get(node) ?? []); + return topoSort(new Set(projectedDependencies.keys()), projectedDependencies); + } + + public consoleLog(indent: number = 0) { + process.stdout.write(' '.repeat(indent) + this + depString(this) + '\n'); + for (const node of this.nodes) { + if (node instanceof Graph) { + node.consoleLog(indent + 2); + } else { + process.stdout.write(' '.repeat(indent + 2) + node + depString(node) + '\n'); + } + } + + function depString(node: GraphNode) { + if (node.dependencies.length > 0) { + return ` -> ${Array.from(node.dependencies).join(', ')}`; + } + return ''; + } + } + + /** + * Return the union of all dependencies of the descendants of this graph + */ + private deepDependencies() { + const ret = new Map, Set>>(); + for (const node of this.nodes) { + recurse(node); + } + return ret; + + function recurse(node: GraphNode) { + let deps = ret.get(node); + if (!deps) { + ret.set(node, deps = new Set()); + } + for (let dep of node.dependencies) { + deps.add(dep); + } + if (node instanceof Graph) { + for (const child of node.nodes) { + recurse(child); + } + } + } + } + + /** + * Return all non-Graph nodes + */ + public allLeaves(): GraphNodeCollection { + const ret: GraphNode[] = []; + recurse(this); + return new GraphNodeCollection(ret); + + function recurse(node: GraphNode) { + if (node instanceof Graph) { + for (const child of node.nodes) { + recurse(child); + } + } else { + ret.push(node); + } + } + } +} + +/** + * A collection of graph nodes + */ +export class GraphNodeCollection { + public readonly nodes: GraphNode[]; + + constructor(nodes: Iterable>) { + this.nodes = Array.from(nodes); + } + + public dependOn(...dependencies: Array | undefined>) { + for (const node of this.nodes) { + node.dependOn(...dependencies.filter(isDefined)); + } + } + + /** + * Returns the graph node that's shared between these nodes + */ + public commonAncestor() { + const paths = new Array[]>(); + for (const x of this.nodes) { + paths.push(x.rootPath()); + } + + if (paths.length === 0) { + throw new Error('Cannot find common ancestor between an empty set of nodes'); + } + if (paths.length === 1) { + const path = paths[0]; + + if (path.length < 2) { + throw new Error(`Cannot find ancestor of node without ancestor: ${path[0]}`); + } + return path[path.length - 2]; + } + + const originalPaths = [...paths]; + + // Remove the first element of every path as long as the 2nd elements are all + // the same -- this leaves the shared element in first place. + // + // A, B, C, 1, 2 }---> C + // A, B, C, 3 } + while (paths.every(path => paths[0].length >= 2 && path.length >= 2 && path[1] === paths[0][1])) { + for (const path of paths) { + path.shift(); + } + } + + // If any of the paths are left with 1 element, there's no shared parent. + if (paths.some(path => path.length < 2)) { + throw new Error(`Could not determine a shared parent between nodes: ${originalPaths.map(nodes => nodes.map(n => n.id).join('/'))}`); + } + + return paths[0][0]; + } +} + +/** + * Dependency map of nodes in this graph, taking into account dependencies between nodes in subgraphs + * + * Guaranteed to return an entry in the map for every node in the current graph. + */ +function projectDependencies(dependencies: Map, Set>>, project: (x: GraphNode) => GraphNode[]) { + // Project keys + for (const node of dependencies.keys()) { + const projectedNodes = project(node); + if (projectedNodes.length === 1 && projectedNodes[0] === node) { continue; } // Nothing to do, just for efficiency + + const deps = extract(dependencies, node)!; + for (const projectedNode of projectedNodes) { + addAll(dependencies.get(projectedNode)!, deps); + } + } + + // Project values. Ignore self-dependencies, they were just between nodes that were collapsed into the same node. + for (const [node, deps] of dependencies.entries()) { + const depset = new Set(flatMap(deps, project)); + depset.delete(node); + dependencies.set(node, depset); + } + + return dependencies; +} + +export function isGraph(x: GraphNode): x is Graph { + return x instanceof Graph; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts new file mode 100644 index 0000000000000..6709f7c84488f --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts @@ -0,0 +1,2 @@ +export * from './pipeline-graph'; +export * from './graph'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts new file mode 100644 index 0000000000000..0adcea551ff32 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts @@ -0,0 +1,319 @@ +import { AssetType, FileSet, ShellStep, StackAsset, StackDeployment, StageDeployment, Step, Wave } from '../blueprint'; +import { PipelineBase } from '../main/pipeline-base'; +import { DependencyBuilders, Graph, GraphNode, GraphNodeCollection } from './graph'; +import { PipelineQueries } from './pipeline-queries'; + +export interface PipelineGraphProps { + /** + * Add a self-mutation step. + * + * @default false + */ + readonly selfMutation?: boolean; + + /** + * Publishes the template asset to S3. + * + * @default false + */ + readonly publishTemplate?: boolean; + + /** + * Whether to combine asset publishers for the same type into one step + * + * @default false + */ + readonly singlePublisherPerAssetType?: boolean; + + /** + * Add a "prepare" step for each stack which can be used to create the change + * set. If this is disbled, only the "execute" step will be included. + * + * @default true + */ + readonly prepareStep?: boolean; +} + +/** + * Logic to turn the deployment blueprint into a graph + * + * This code makes all the decisions on how to lay out the CodePipeline + */ +export class PipelineGraph { + /** + * A Step object that may be used as the producer of FileSets that should not be represented in the graph + */ + public static readonly NO_STEP: Step = new class extends Step { } ('NO_STEP'); + + public readonly graph: AGraph = Graph.of('', { type: 'group' }); + public readonly cloudAssemblyFileSet: FileSet; + public readonly queries: PipelineQueries; + + private readonly added = new Map(); + private readonly assetNodes = new Map(); + private readonly assetNodesByType = new Map(); + private readonly synthNode?: AGraphNode; + private readonly selfMutateNode?: AGraphNode; + private readonly stackOutputDependencies = new DependencyBuilders(); + private readonly publishTemplate: boolean; + private readonly prepareStep: boolean; + private readonly singlePublisher: boolean; + + private lastPreparationNode?: AGraphNode; + private _fileAssetCtr = 0; + private _dockerAssetCtr = 0; + + constructor(public readonly pipeline: PipelineBase, props: PipelineGraphProps = {}) { + this.publishTemplate = props.publishTemplate ?? false; + this.prepareStep = props.prepareStep ?? true; + this.singlePublisher = props.singlePublisherPerAssetType ?? false; + + this.queries = new PipelineQueries(pipeline); + + if (pipeline.synth instanceof Step) { + this.synthNode = this.addBuildStep(pipeline.synth); + if (this.synthNode?.data?.type === 'step') { + this.synthNode.data.isBuildStep = true; + } + } + this.lastPreparationNode = this.synthNode; + + const cloudAssembly = pipeline.synth.primaryOutput?.primaryOutput; + if (!cloudAssembly) { + throw new Error(`The synth step must produce the cloud assembly artifact, but doesn't: ${pipeline.synth}`); + } + + this.cloudAssemblyFileSet = cloudAssembly; + + if (props.selfMutation) { + const stage: AGraph = Graph.of('UpdatePipeline', { type: 'group' }); + this.graph.add(stage); + this.selfMutateNode = GraphNode.of('SelfMutate', { type: 'self-update' }); + stage.add(this.selfMutateNode); + + this.selfMutateNode.dependOn(this.synthNode); + this.lastPreparationNode = this.selfMutateNode; + } + + const waves = pipeline.waves.map(w => this.addWave(w)); + + // Make sure the waves deploy sequentially + for (let i = 1; i < waves.length; i++) { + waves[i].dependOn(waves[i - 1]); + } + + // Add additional dependencies between steps that depend on stack outputs and the stacks + // that produce them. + } + + public isSynthNode(node: AGraphNode) { + return this.synthNode === node; + } + + private addBuildStep(step: Step) { + return this.addAndRecurse(step, this.topLevelGraph('Build')); + } + + private addWave(wave: Wave): AGraph { + // If the wave only has one Stage in it, don't add an additional Graph around it + const retGraph: AGraph = wave.stages.length === 1 + ? this.addStage(wave.stages[0]) + : Graph.of(wave.id, { type: 'group' }, wave.stages.map(s => this.addStage(s))); + + this.addPrePost(wave.pre, wave.post, retGraph); + retGraph.dependOn(this.lastPreparationNode); + this.graph.add(retGraph); + + return retGraph; + } + + private addStage(stage: StageDeployment): AGraph { + const retGraph: AGraph = Graph.of(stage.stageName, { type: 'group' }); + + const stackGraphs = new Map(); + + for (const stack of stage.stacks) { + const stackGraph: AGraph = Graph.of(this.simpleStackName(stack.stackName, stage.stageName), { type: 'stack-group', stack }); + const prepareNode: AGraphNode | undefined = this.prepareStep ? GraphNode.of('Prepare', { type: 'prepare', stack }) : undefined; + const deployNode: AGraphNode = GraphNode.of('Deploy', { + type: 'execute', + stack, + captureOutputs: this.queries.stackOutputsReferenced(stack).length > 0, + }); + + retGraph.add(stackGraph); + + stackGraph.add(deployNode); + let firstDeployNode; + if (prepareNode) { + stackGraph.add(prepareNode); + deployNode.dependOn(prepareNode); + firstDeployNode = prepareNode; + } else { + firstDeployNode = deployNode; + } + + stackGraphs.set(stack, stackGraph); + + const cloudAssembly = this.cloudAssemblyFileSet; + + firstDeployNode.dependOn(this.addAndRecurse(cloudAssembly.producer, retGraph)); + + // add the template asset + if (this.publishTemplate) { + if (!stack.templateAsset) { + throw new Error(`"publishTemplate" is enabled, but stack ${stack.stackArtifactId} does not have a template asset`); + } + + firstDeployNode.dependOn(this.publishAsset(stack.templateAsset)); + } + + // Depend on Assets + // FIXME: Custom Cloud Assembly currently doesn't actually help separating + // out templates from assets!!! + for (const asset of stack.assets) { + const assetNode = this.publishAsset(asset); + firstDeployNode.dependOn(assetNode); + } + + // Add stack output synchronization point + if (this.queries.stackOutputsReferenced(stack).length > 0) { + this.stackOutputDependencies.get(stack).dependOn(deployNode); + } + } + + for (const stack of stage.stacks) { + for (const dep of stack.stackDependencies) { + const stackNode = stackGraphs.get(stack); + const depNode = stackGraphs.get(dep); + if (!stackNode) { + throw new Error(`cannot find node for ${stack.stackName}`); + } + if (!depNode) { + throw new Error(`cannot find node for ${dep.stackName}`); + } + stackNode.dependOn(depNode); + } + } + + this.addPrePost(stage.pre, stage.post, retGraph); + + return retGraph; + } + + private addPrePost(pre: Step[], post: Step[], parent: AGraph) { + const currentNodes = new GraphNodeCollection(parent.nodes); + for (const p of pre) { + const preNode = this.addAndRecurse(p, parent); + currentNodes.dependOn(preNode); + } + for (const p of post) { + const postNode = this.addAndRecurse(p, parent); + postNode?.dependOn(...currentNodes.nodes); + } + } + + private topLevelGraph(name: string): AGraph { + let ret = this.graph.tryGetChild(name); + if (!ret) { + ret = new Graph(name); + this.graph.add(ret); + } + return ret as AGraph; + } + + private addAndRecurse(step: Step, parent: AGraph) { + if (step === PipelineGraph.NO_STEP) { return undefined; } + + const previous = this.added.get(step); + if (previous) { return previous; } + + const node: AGraphNode = GraphNode.of(step.id, { type: 'step', step }); + + // If the step is a source step, change the parent to a special "Source" stage + // (CodePipeline wants it that way) + if (step.isSource) { + parent = this.topLevelGraph('Source'); + } + + parent.add(node); + this.added.set(step, node); + + for (const dep of step.dependencies) { + const producerNode = this.addAndRecurse(dep, parent); + node.dependOn(producerNode); + } + + // Add stack dependencies (by use of the dependencybuilder this also works + // if we encounter the Step before the Stack has been properly added yet) + if (step instanceof ShellStep) { + for (const output of Object.values(step.envFromCfnOutputs)) { + const stack = this.queries.producingStack(output); + this.stackOutputDependencies.get(stack).dependBy(node); + } + } + + return node; + } + + private publishAsset(stackAsset: StackAsset): AGraphNode { + const assetsGraph = this.topLevelGraph('Assets'); + + let assetNode = this.assetNodes.get(stackAsset.assetId); + if (assetNode) { + // If there's already a node pubishing this asset, add as a new publishing + // destination to the same node. + } else if (this.singlePublisher && this.assetNodesByType.has(stackAsset.assetType)) { + // If we're doing a single node per type, lookup by that + assetNode = this.assetNodesByType.get(stackAsset.assetType)!; + } else { + // Otherwise add a new one + const id = stackAsset.assetType === AssetType.FILE + ? (this.singlePublisher ? 'FileAsset' : `FileAsset${++this._fileAssetCtr}`) + : (this.singlePublisher ? 'DockerAsset' : `DockerAsset${++this._dockerAssetCtr}`); + + assetNode = GraphNode.of(id, { type: 'publish-assets', assets: [] }); + assetsGraph.add(assetNode); + assetNode.dependOn(this.lastPreparationNode); + + this.assetNodesByType.set(stackAsset.assetType, assetNode); + this.assetNodes.set(stackAsset.assetId, assetNode); + } + + const data = assetNode.data; + if (data?.type !== 'publish-assets') { + throw new Error(`${assetNode} has the wrong data.type: ${data?.type}`); + } + if (!data.assets.some(a => a.assetSelector === stackAsset.assetSelector)) { + data.assets.push(stackAsset); + } + + return assetNode; + } + + /** + * Simplify the stack name by removing the `Stage-` prefix if it exists. + */ + private simpleStackName(stackName: string, stageName: string) { + return stripPrefix(stackName, `${stageName}-`); + } +} + +type GraphAnnotation = + { readonly type: 'group' } + | { readonly type: 'stack-group'; readonly stack: StackDeployment } + | { readonly type: 'publish-assets'; readonly assets: StackAsset[] } + | { readonly type: 'step'; readonly step: Step; isBuildStep?: boolean } + | { readonly type: 'self-update' } + | { readonly type: 'prepare'; readonly stack: StackDeployment } + | { readonly type: 'execute'; readonly stack: StackDeployment; readonly captureOutputs: boolean } + ; + +// Type aliases for the graph nodes tagged with our specific annotation type +// (to save on generics in the code above). +export type AGraphNode = GraphNode; +export type AGraph = Graph; + +function stripPrefix(s: string, prefix: string) { + return s.startsWith(prefix) ? s.substr(prefix.length) : s; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-queries.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-queries.ts new file mode 100644 index 0000000000000..d3306e4e0a934 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-queries.ts @@ -0,0 +1,67 @@ +import { Step, ShellStep, StackOutputReference, StackDeployment, StackAsset, StageDeployment } from '../blueprint'; +import { PipelineBase } from '../main/pipeline-base'; + +/** + * Answer some questions about a pipeline blueprint + */ +export class PipelineQueries { + constructor(private readonly pipeline: PipelineBase) { + } + + /** + * Return the names of all outputs for the given stack that are referenced in this blueprint + */ + public stackOutputsReferenced(stack: StackDeployment): string[] { + const steps = new Array(); + for (const wave of this.pipeline.waves) { + steps.push(...wave.pre, ...wave.post); + for (const stage of wave.stages) { + steps.push(...stage.pre, ...stage.post); + } + } + + const ret = new Array(); + for (const step of steps) { + if (!(step instanceof ShellStep)) { continue; } + + for (const outputRef of Object.values(step.envFromCfnOutputs)) { + if (outputRef.isProducedBy(stack)) { + ret.push(outputRef.outputName); + } + } + } + return ret; + } + + /** + * Find the stack deployment that is producing the given reference + */ + public producingStack(outputReference: StackOutputReference): StackDeployment { + for (const wave of this.pipeline.waves) { + for (const stage of wave.stages) { + for (const stack of stage.stacks) { + if (outputReference.isProducedBy(stack)) { + return stack; + } + } + } + } + + throw new Error(`Stack '${outputReference.stackDescription}' (producing output '${outputReference.outputName}') is not in the pipeline; call 'addStage()' to add the stack's Stage to the pipeline`); + } + + /** + * All assets referenced in all the Stacks of a StageDeployment + */ + public assetsInStage(stage: StageDeployment): StackAsset[] { + const assets = new Map(); + + for (const stack of stage.stacks) { + for (const asset of stack.assets) { + assets.set(asset.assetSelector, asset); + } + } + + return Array.from(assets.values()); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts new file mode 100644 index 0000000000000..eb5e0cc3483aa --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts @@ -0,0 +1,67 @@ +import { GraphNode } from './graph'; + +export function printDependencyMap(dependencies: Map, Set>>) { + const lines = ['---']; + for (const [k, vs] of dependencies.entries()) { + lines.push(`${k} -> ${Array.from(vs)}`); + } + // eslint-disable-next-line no-console + console.log(lines.join('\n')); +} + +export function topoSort(nodes: Set>, dependencies: Map, Set>>): GraphNode[][] { + const remaining = new Set>(nodes); + + const ret: GraphNode[][] = []; + while (remaining.size > 0) { + // All elements with no more deps in the set can be ordered + const selectable = Array.from(remaining.values()).filter(e => { + if (!dependencies.has(e)) { + throw new Error(`No key for ${e}`); + } + return dependencies.get(e)!.size === 0; + }); + selectable.sort((a, b) => a.id < b.id ? -1 : b.id < a.id ? 1 : 0); + + // If we didn't make any progress, we got stuck + if (selectable.length === 0) { + const cycle = findCycle(dependencies); + throw new Error(`Dependency cycle in graph: ${cycle.map(n => n.id).join(' => ')}`); + } + + ret.push(selectable); + + for (const selected of selectable) { + remaining.delete(selected); + for (const depSet of dependencies.values()) { + depSet.delete(selected); + } + } + } + + return ret; +} + +/** + * Find cycles in a graph + * + * Not the fastest, but effective and should be rare + */ +function findCycle(deps: Map, Set>>): GraphNode[] { + for (const node of deps.keys()) { + const cycle = recurse(node, [node]); + if (cycle) { return cycle; } + } + throw new Error('No cycle found. Assertion failure!'); + + function recurse(node: GraphNode, path: GraphNode[]): GraphNode[] | undefined { + for (const dep of deps.get(node) ?? []) { + if (dep === path[0]) { return [...path, dep]; } + + const cycle = recurse(dep, [...path, dep]); + if (cycle) { return cycle; } + } + + return undefined; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/index.ts b/packages/@aws-cdk/pipelines/lib/index.ts index 2e63ee1d083a9..5f469e9fd5ce6 100644 --- a/packages/@aws-cdk/pipelines/lib/index.ts +++ b/packages/@aws-cdk/pipelines/lib/index.ts @@ -1,6 +1,5 @@ -export * from './pipeline'; -export * from './stage'; -export * from './synths'; -export * from './actions'; -export * from './docker-credentials'; -export * from './validation'; +export * from './legacy'; +export * from './blueprint'; +export * from './codepipeline'; +export * from './main'; +export * from './docker-credentials'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts similarity index 98% rename from packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts index 095ed581302aa..af6b7821a308d 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts @@ -7,8 +7,8 @@ import * as iam from '@aws-cdk/aws-iam'; import { Aws, CfnCapabilities, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct, Node } from 'constructs'; -import { appOf, assemblyBuilderOf } from '../private/construct-internals'; -import { toPosixPath } from '../private/fs'; +import { appOf, assemblyBuilderOf } from '../../private/construct-internals'; +import { toPosixPath } from '../../private/fs'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line diff --git a/packages/@aws-cdk/pipelines/lib/actions/index.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/index.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/actions/index.ts rename to packages/@aws-cdk/pipelines/lib/legacy/actions/index.ts diff --git a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts similarity index 96% rename from packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts index a9661da20a9c2..72f2924e1690a 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts @@ -8,27 +8,13 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { IDependable, ISynthesisSession, Lazy, Stack, attachCustomSynthesis } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { toPosixPath } from '../private/fs'; +import { AssetType } from '../../blueprint/asset-type'; +import { toPosixPath } from '../../private/fs'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; -/** - * Type of the asset that is being published - */ -export enum AssetType { - /** - * A file - */ - FILE = 'file', - - /** - * A Docker image - */ - DOCKER_IMAGE = 'docker-image', -} - /** * Props for a PublishAssetsAction */ diff --git a/packages/@aws-cdk/pipelines/lib/actions/update-pipeline-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts similarity index 97% rename from packages/@aws-cdk/pipelines/lib/actions/update-pipeline-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts index 42b3c51c1da3a..cc866c20e51d8 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/update-pipeline-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts @@ -5,8 +5,8 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; -import { embeddedAsmPath } from '../private/construct-internals'; +import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../../docker-credentials'; +import { embeddedAsmPath } from '../../private/construct-internals'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line diff --git a/packages/@aws-cdk/pipelines/lib/legacy/index.ts b/packages/@aws-cdk/pipelines/lib/legacy/index.ts new file mode 100644 index 0000000000000..ca2b108fcb0d8 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/legacy/index.ts @@ -0,0 +1,5 @@ +export * from './pipeline'; +export * from './stage'; +export * from './synths'; +export * from './actions'; +export * from './validation'; diff --git a/packages/@aws-cdk/pipelines/lib/pipeline.ts b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts similarity index 98% rename from packages/@aws-cdk/pipelines/lib/pipeline.ts rename to packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts index a3084d9f489d7..95a828b981ea1 100644 --- a/packages/@aws-cdk/pipelines/lib/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts @@ -4,9 +4,10 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { Annotations, App, Aws, CfnOutput, Fn, Lazy, PhysicalName, Stack, Stage } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AssetType, DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; -import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from './docker-credentials'; -import { appOf, assemblyBuilderOf } from './private/construct-internals'; +import { AssetType } from '../blueprint/asset-type'; +import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; +import { appOf, assemblyBuilderOf } from '../private/construct-internals'; +import { DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from './stage'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. diff --git a/packages/@aws-cdk/pipelines/lib/stage.ts b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts similarity index 98% rename from packages/@aws-cdk/pipelines/lib/stage.ts rename to packages/@aws-cdk/pipelines/lib/legacy/stage.ts index 4d5eda62762d3..55c847d984a58 100644 --- a/packages/@aws-cdk/pipelines/lib/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts @@ -3,9 +3,10 @@ import * as cpactions from '@aws-cdk/aws-codepipeline-actions'; import { Stage, Aspects } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { AssetType, DeployCdkStackAction } from './actions'; -import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from './private/asset-manifest'; -import { topologicalSort } from './private/toposort'; +import { AssetType } from '../blueprint/asset-type'; +import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; +import { topologicalSort } from '../private/toposort'; +import { DeployCdkStackAction } from './actions'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line diff --git a/packages/@aws-cdk/pipelines/lib/synths/_util.ts b/packages/@aws-cdk/pipelines/lib/legacy/synths/_util.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/synths/_util.ts rename to packages/@aws-cdk/pipelines/lib/legacy/synths/_util.ts diff --git a/packages/@aws-cdk/pipelines/lib/synths/index.ts b/packages/@aws-cdk/pipelines/lib/legacy/synths/index.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/synths/index.ts rename to packages/@aws-cdk/pipelines/lib/legacy/synths/index.ts diff --git a/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts similarity index 99% rename from packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts index 8380a9c859698..b0fe2bcd466fb 100644 --- a/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts @@ -7,8 +7,8 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; -import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; -import { toPosixPath } from '../private/fs'; +import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../../docker-credentials'; +import { toPosixPath } from '../../private/fs'; import { copyEnvironmentVariables, filterEmpty } from './_util'; const DEFAULT_OUTPUT_DIR = 'cdk.out'; diff --git a/packages/@aws-cdk/pipelines/lib/validation/_files.ts b/packages/@aws-cdk/pipelines/lib/legacy/validation/_files.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/validation/_files.ts rename to packages/@aws-cdk/pipelines/lib/legacy/validation/_files.ts diff --git a/packages/@aws-cdk/pipelines/lib/validation/index.ts b/packages/@aws-cdk/pipelines/lib/legacy/validation/index.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/validation/index.ts rename to packages/@aws-cdk/pipelines/lib/legacy/validation/index.ts diff --git a/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/validation/shell-script-action.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/validation/shell-script-action.ts diff --git a/packages/@aws-cdk/pipelines/lib/main/index.ts b/packages/@aws-cdk/pipelines/lib/main/index.ts new file mode 100644 index 0000000000000..af40f3df33635 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/main/index.ts @@ -0,0 +1 @@ +export * from './pipeline-base'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts new file mode 100644 index 0000000000000..563697746a8cf --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts @@ -0,0 +1,132 @@ +import { Aspects, Stage } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { AddStageOpts as StageOptions, WaveOptions, Wave, IFileSetProducer, ShellStep } from '../blueprint'; + +// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. +// eslint-disable-next-line +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properties for a `Pipeline` + */ +export interface PipelineBaseProps { + /** + * The build step that produces the CDK Cloud Assembly + * + * The primary output of this step needs to be the `cdk.out` directory + * generated by the `cdk synth` command. + * + * If you use a `ShellStep` here and you don't configure an output directory, + * the output directory will automatically be assumed to be `cdk.out`. + */ + readonly synth: IFileSetProducer; +} + +/** + * A generic CDK Pipelines pipeline + * + * Different deployment systems will provide subclasses of `Pipeline` that generate + * the deployment infrastructure necessary to deploy CDK apps, specific to that system. + * + * This library comes with the `CodePipeline` class, which uses AWS CodePipeline + * to deploy CDK apps. + * + * The actual pipeline infrastructure is constructed (by invoking the engine) + * when `buildPipeline()` is called, or when `app.synth()` is called (whichever + * happens first). + */ +export abstract class PipelineBase extends CoreConstruct { + /** + * The build step that produces the CDK Cloud Assembly + */ + public readonly synth: IFileSetProducer; + + /** + * The waves in this pipeline + */ + public readonly waves: Wave[]; + + private built = false; + + constructor(scope: Construct, id: string, props: PipelineBaseProps) { + super(scope, id); + + if (props.synth instanceof ShellStep && !props.synth.primaryOutput) { + props.synth.primaryOutputDirectory('cdk.out'); + } + + this.synth = props.synth; + this.waves = []; + + if (!props.synth.primaryOutput) { + throw new Error(`synthStep ${props.synth} must produce a primary output, but is not producing anything. Configure the Step differently or use a different Step type.`); + } + + Aspects.of(this).add({ visit: () => this.buildJustInTime() }); + } + + /** + * Deploy a single Stage by itself + * + * Add a Stage to the pipeline, to be deployed in sequence with other + * Stages added to the pipeline. All Stacks in the stage will be deployed + * in an order automatically determined by their relative dependencies. + */ + public addStage(stage: Stage, options?: StageOptions) { + if (this.built) { + throw new Error('addStage: can\'t add Stages anymore after buildPipeline() has been called'); + } + + return this.addWave(stage.stageName).addStage(stage, options); + } + + /** + * Add a Wave to the pipeline, for deploying multiple Stages in parallel + * + * Use the return object of this method to deploy multiple stages in parallel. + * + * Example: + * + * ```ts + * const wave = pipeline.addWave('MyWave'); + * wave.addStage(new MyStage('Stage1', ...)); + * wave.addStage(new MyStage('Stage2', ...)); + * ``` + */ + public addWave(id: string, options?: WaveOptions) { + if (this.built) { + throw new Error('addWave: can\'t add Waves anymore after buildPipeline() has been called'); + } + + const wave = new Wave(id, options); + this.waves.push(wave); + return wave; + } + + /** + * Send the current pipeline definition to the engine, and construct the pipeline + * + * It is not possible to modify the pipeline after calling this method. + */ + public buildPipeline() { + if (this.built) { + throw new Error('build() has already been called: can only call it once'); + } + this.doBuildPipeline(); + this.built = true; + } + + /** + * Implemented by subclasses to do the actual pipeline construction + */ + protected abstract doBuildPipeline(): void; + + /** + * Automatically call 'build()' just before synthesis if the user hasn't explicitly called it yet + */ + private buildJustInTime() { + if (!this.built) { + this.buildPipeline(); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/cloud-assembly-internals.ts b/packages/@aws-cdk/pipelines/lib/private/cloud-assembly-internals.ts new file mode 100644 index 0000000000000..114ec2e228fbf --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/cloud-assembly-internals.ts @@ -0,0 +1,13 @@ +import * as cxapi from '@aws-cdk/cx-api'; + +export function isAssetManifest(s: cxapi.CloudArtifact): s is cxapi.AssetManifestArtifact { + // instanceof is too risky, and we're at a too late stage to properly fix. + // return s instanceof cxapi.AssetManifestArtifact; + return s.constructor.name === 'AssetManifestArtifact'; +} + +export function isStackArtifact(a: cxapi.CloudArtifact): a is cxapi.CloudFormationStackArtifact { + // instanceof is too risky, and we're at a too late stage to properly fix. + // return a instanceof cxapi.CloudFormationStackArtifact; + return a.constructor.name === 'CloudFormationStackArtifact'; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts b/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts index 496d33c5a1f7c..fe2ddf1953f64 100644 --- a/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts +++ b/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts @@ -4,7 +4,10 @@ import * as path from 'path'; import { App, Stage } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { IConstruct, Node } from 'constructs'; +import { Construct, IConstruct, Node } from 'constructs'; + +// eslint-disable-next-line no-duplicate-imports,import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; export function appOf(construct: IConstruct): App { const root = Node.of(construct).root; @@ -35,4 +38,12 @@ export function embeddedAsmPath(scope: IConstruct) { */ export function cloudAssemblyBuildSpecDir(scope: IConstruct) { return assemblyBuilderOf(appOf(scope)).outdir; +} + +export function obtainScope(parent: Construct, id: string): Construct { + const existing = Node.of(parent).tryFindChild(id); + if (existing) { + return existing as Construct; + } + return new CoreConstruct(parent, id); } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/javascript.ts b/packages/@aws-cdk/pipelines/lib/private/javascript.ts new file mode 100644 index 0000000000000..bed84e5eb3932 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/javascript.ts @@ -0,0 +1,90 @@ +export function addAll(into: Set, from: Iterable) { + for (const x of from) { + into.add(x); + } +} + +export function extract(from: Map, key: A): B | undefined { + const ret = from.get(key); + from.delete(key); + return ret; +} + +export function* flatMap(xs: Iterable, fn: (x: A) => Iterable): IterableIterator { + for (const x of xs) { + for (const y of fn(x)) { + yield y; + } + } +} + +export function* enumerate(xs: Iterable): IterableIterator<[number, A]> { + let i = 0; + for (const x of xs) { + yield [i++, x]; + } +} + + +export function expectProp(obj: A, key: B): NonNullable { + if (!obj[key]) { throw new Error(`Expecting '${key}' to be set!`); } + return obj[key] as any; +} + +export function* flatten(xs: Iterable): IterableIterator { + for (const x of xs) { + for (const y of x) { + yield y; + } + } +} + +export function filterEmpty(xs: Array): string[] { + return xs.filter(x => x) as any; +} + +export function mapValues(xs: Record, fn: (x: A) => B): Record { + const ret: Record = {}; + for (const [k, v] of Object.entries(xs)) { + ret[k] = fn(v); + } + return ret; +} + +export function mkdict(xs: Array): Record { + const ret: Record = {}; + for (const [k, v] of xs) { + ret[k] = v; + } + return ret; +} + +export function noEmptyObject(xs: Record): Record | undefined { + if (Object.keys(xs).length === 0) { return undefined; } + return xs; +} + +export function noUndefined(xs: Record): Record> { + return mkdict(Object.entries(xs).filter(([_, v]) => isDefined(v))) as any; +} + +export function maybeSuffix(x: string | undefined, suffix: string): string | undefined { + if (x === undefined) { return undefined; } + return `${x}${suffix}`; +} + +/** + * Partition a collection by dividing it into two collections, one that matches the predicate and one that don't + */ +export function partition(xs: T[], pred: (x: T) => boolean): [T[], T[]] { + const yes: T[] = []; + const no: T[] = []; + for (const x of xs) { + (pred(x) ? yes : no).push(x); + } + return [yes, no]; +} + +export function isDefined(x: A): x is NonNullable { + return x !== undefined; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/template-configuration.ts b/packages/@aws-cdk/pipelines/lib/private/template-configuration.ts new file mode 100644 index 0000000000000..bc78424ce22a4 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/template-configuration.ts @@ -0,0 +1,21 @@ +import * as fs from 'fs'; + +/** + * Write template configuration to the given file + */ +export function writeTemplateConfiguration(filename: string, config: TemplateConfiguration) { + fs.writeFileSync(filename, JSON.stringify(config, undefined, 2), { encoding: 'utf-8' }); +} + +/** + * Template configuration in a CodePipeline + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-cfn-artifacts.html#w2ab1c13c17c15 + */ +export interface TemplateConfiguration { + readonly Parameters?: Record; + readonly Tags?: Record; + readonly StackPolicy?: { + readonly Statements: Array>; + }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 7c24bb5b83505..1db513581a96e 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -32,18 +32,21 @@ "organization": true }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, "peerDependencies": { "constructs": "^3.3.69", "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", @@ -53,12 +56,14 @@ "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/cx-api": "0.0.0" }, "dependencies": { "constructs": "^3.3.69", "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", @@ -68,6 +73,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/cx-api": "0.0.0" }, diff --git a/packages/@aws-cdk/pipelines/test/actions/update-pipeline-action.test.ts b/packages/@aws-cdk/pipelines/test/actions/update-pipeline-action.test.ts deleted file mode 100644 index d47167d6a7e6e..0000000000000 --- a/packages/@aws-cdk/pipelines/test/actions/update-pipeline-action.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - arrayWith, -} from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as cp from '@aws-cdk/aws-codepipeline'; -import { Stack } from '@aws-cdk/core'; -import * as cdkp from '../../lib'; -import { behavior } from '../helpers/compliance'; -import { TestApp } from '../testutil'; - -let app: TestApp; -let pipelineStack: Stack; - -behavior('self-update project role has proper permissions', (suite) => { - suite.legacy(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack'); - - new cdkp.UpdatePipelineAction(pipelineStack, 'Update', { - cloudAssemblyInput: new cp.Artifact(), - pipelineStackHierarchicalId: pipelineStack.node.path, - projectName: 'pipeline-selfupdate', - }); - - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith( - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Resource: { 'Fn::Join': ['', ['arn:*:iam::', { Ref: 'AWS::AccountId' }, ':role/*']] }, - Condition: { - 'ForAnyValue:StringEquals': { - 'iam:ResourceTag/aws-cdk:bootstrap-role': ['image-publishing', 'file-publishing', 'deploy'], - }, - }, - }, - { - Action: 'cloudformation:DescribeStacks', - Effect: 'Allow', - Resource: '*', - }, - { - Action: 's3:ListBucket', - Effect: 'Allow', - Resource: '*', - }, - ), - }, - }); - }); -}); diff --git a/packages/@aws-cdk/pipelines/test/blueprint/fixtures/file-asset1.txt b/packages/@aws-cdk/pipelines/test/blueprint/fixtures/file-asset1.txt new file mode 100644 index 0000000000000..6765125a23c6b --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/fixtures/file-asset1.txt @@ -0,0 +1 @@ +Hello, file! \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts new file mode 100644 index 0000000000000..f577ffae4f80c --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts @@ -0,0 +1,52 @@ +import { GraphNode } from '../../../lib/helpers-internal'; +import { mkGraph, nodeNames } from './util'; + +describe('with nested graphs', () => { + const graph = mkGraph('G', G => { + let aa: GraphNode; + + const A = G.graph('A', [], GA => { + aa = GA.node('aa'); + }); + + // B -> A, (same-level dependency) + G.graph('B', [A], B => { + // bbb -> bb + const bb = B.node('bb'); + B.node('bbb', [bb]); + }); + + // cc -> aa (cross-subgraph dependency) + G.graph('C', [], C => { + C.node('cc', [aa]); + }); + + // D -> aa (down-dependency) + G.graph('D', [aa!], C => { + C.node('dd', [aa]); + }); + + // ee -> A (up-dependency) + G.graph('E', [], C => { + C.node('ee', [A]); + }); + }); + + test('can get up-projected dependency list from graph', () => { + const sorted = graph.sortedChildren(); + + expect(nodeNames(sorted)).toEqual([ + ['A'], + ['B', 'C', 'D', 'E'], + ]); + }); + + test('can get down-projected dependency list from graph', () => { + const sorted = graph.sortedLeaves(); + expect(nodeNames(sorted)).toEqual([ + ['aa'], + ['bb', 'cc', 'dd', 'ee'], + ['bbb'], + ]); + }); +}); diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/graph.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/graph.test.ts new file mode 100644 index 0000000000000..30a022e347932 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/graph.test.ts @@ -0,0 +1,40 @@ +import { GraphNode } from '../../../lib/helpers-internal'; +import { flatten } from '../../../lib/private/javascript'; +import { mkGraph } from './util'; + + +test('"uniqueId" renders a graph-wide unique id for each node', () => { + const g = mkGraph('MyGraph', G => { + G.graph('g1', [], G1 => { + G1.node('n1'); + G1.node('n2'); + G1.graph('g2', [], G2 => { + G2.node('n3'); + }); + }); + G.node('n4'); + }); + + expect(Array.from(flatten(g.sortedLeaves())).map(n => n.uniqueId)).toStrictEqual([ + 'g1-n1', + 'g1-n2', + 'g1-g2-n3', + 'n4', + ]); +}); + +test('"allDeps" combines node deps and parent deps', () => { + let n4: any; + mkGraph('MyGraph', G => { + G.graph('g1', [], G1 => { + G1.node('n1'); + const n2 = G1.node('n2'); + G1.graph('g2', [n2], G2 => { + const n3 = G2.node('n3'); + n4 = G2.node('n4', [n3]); + }); + }); + }); + + expect((n4 as GraphNode).allDeps.map(x => x.uniqueId)).toStrictEqual(['g1-g2-n3', 'g1-n2']); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts new file mode 100644 index 0000000000000..3b28d4f410a61 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts @@ -0,0 +1,264 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import '@aws-cdk/assert-internal/jest'; +import * as cdkp from '../../../lib'; +import { Graph, GraphNode, PipelineGraph } from '../../../lib/helpers-internal'; +import { flatten } from '../../../lib/private/javascript'; +import { AppWithOutput, OneStackApp, TestApp } from '../../testhelpers/test-app'; + +let app: TestApp; + +beforeEach(() => { + app = new TestApp(); +}); + +afterEach(() => { + app.cleanup(); +}); + +describe('blueprint with one stage', () => { + let blueprint: Blueprint; + beforeEach(() => { + blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['build'], + }), + }); + blueprint.addStage(new OneStackApp(app, 'CrossAccount', { env: { account: 'you' } })); + }); + + test('simple app gets graphed correctly', () => { + // WHEN + const graph = new PipelineGraph(blueprint).graph; + + // THEN + expect(childrenAt(graph)).toEqual([ + 'Source', + 'Build', + 'CrossAccount', + ]); + + expect(childrenAt(graph, 'CrossAccount')).toEqual([ + 'Stack', + ]); + + expect(childrenAt(graph, 'CrossAccount', 'Stack')).toEqual([ + 'Prepare', + 'Deploy', + ]); + }); + + test('self mutation gets inserted at the right place', () => { + // WHEN + const graph = new PipelineGraph(blueprint, { selfMutation: true }).graph; + + // THEN + expect(childrenAt(graph)).toEqual([ + 'Source', + 'Build', + 'UpdatePipeline', + 'CrossAccount', + ]); + + expect(childrenAt(graph, 'UpdatePipeline')).toEqual([ + 'SelfMutate', + ]); + }); +}); + +describe('blueprint with wave and stage', () => { + let blueprint: Blueprint; + beforeEach(() => { + blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['build'], + }), + }); + + const wave = blueprint.addWave('Wave'); + wave.addStage(new OneStackApp(app, 'Alpha')); + wave.addStage(new OneStackApp(app, 'Beta')); + }); + + test('post-action gets added inside stage graph', () => { + // GIVEN + blueprint.waves[0].stages[0].addPost(new cdkp.ManualApprovalStep('Approve')); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + + // THEN + expect(childrenAt(graph, 'Wave')).toEqual([ + 'Alpha', + 'Beta', + ]); + + expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([ + 'Stack', + 'Approve', + ]); + }); + + test('pre-action gets added inside stage graph', () => { + // GIVEN + blueprint.waves[0].stages[0].addPre(new cdkp.ManualApprovalStep('Gogogo')); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + + // THEN + expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([ + 'Gogogo', + 'Stack', + ]); + }); +}); + +describe('options for other engines', () => { + test('"publishTemplate" will add steps to publish CFN templates as assets', () => { + // GIVEN + const blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + commands: ['build'], + }), + }); + blueprint.addStage(new OneStackApp(app, 'Alpha')); + + // WHEN + const graph = new PipelineGraph(blueprint, { + publishTemplate: true, + }); + + // THEN + expect(childrenAt(graph.graph, 'Assets')).toStrictEqual(['FileAsset1']); + }); + + test('"prepareStep: false" can be used to disable the "prepare" step for stack deployments', () => { + // GIVEN + const blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + commands: ['build'], + }), + }); + blueprint.addStage(new OneStackApp(app, 'Alpha')); + + // WHEN + const graph = new PipelineGraph(blueprint, { + prepareStep: false, + }); + + // THEN + // if "prepareStep" was true (default), the "Stack" node would have "Prepare" and "Deploy" + // since "prepareStep" is false, it only has "Deploy". + expect(childrenAt(graph.graph, 'Alpha', 'Stack')).toStrictEqual(['Deploy']); + }); +}); + + +describe('with app with output', () => { + let blueprint: Blueprint; + let myApp: AppWithOutput; + let scriptStep: cdkp.ShellStep; + beforeEach(() => { + blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['build'], + }), + }); + + myApp = new AppWithOutput(app, 'Alpha'); + scriptStep = new cdkp.ShellStep('PrintBucketName', { + envFromCfnOutputs: { + BUCKET_NAME: myApp.theOutput, + }, + commands: ['echo $BUCKET_NAME'], + }); + }); + + test('post-action using stack output has dependency on execute node', () => { + // GIVEN + blueprint.addStage(myApp, { + post: [scriptStep], + }); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + + // THEN + expect(childrenAt(graph, 'Alpha')).toEqual([ + 'Stack', + 'PrintBucketName', + ]); + + expect(nodeAt(graph, 'Alpha', 'PrintBucketName').dependencies).toContain( + nodeAt(graph, 'Alpha', 'Stack', 'Deploy')); + }); + + test('pre-action cannot use stack output', () => { + // GIVEN + blueprint.addStage(myApp, { + pre: [scriptStep], + }); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + expect(() => { + assertGraph(nodeAt(graph, 'Alpha')).sortedLeaves(); + }).toThrow(/Dependency cycle/); + }); + + test('cannot use output from stack not in the pipeline', () => { + // GIVEN + blueprint.addStage(new AppWithOutput(app, 'OtherApp'), { + pre: [scriptStep], + }); + + // WHEN + expect(() => { + new PipelineGraph(blueprint).graph; + }).toThrow(/is not in the pipeline/); + }); +}); + +function childrenAt(g: Graph, ...descend: string[]) { + for (const d of descend) { + const child = g.tryGetChild(d); + if (!child) { + throw new Error(`No node named '${d}' in ${g}`); + } + g = assertGraph(child); + } + return childNames(g); +} + +function nodeAt(g: Graph, ...descend: string[]) { + for (const d of descend.slice(0, descend.length - 1)) { + const child = g.tryGetChild(d); + if (!child) { + throw new Error(`No node named '${d}' in ${g}`); + } + g = assertGraph(child); + } + const child = g.tryGetChild(descend[descend.length - 1]); + if (!child) { + throw new Error(`No node named '${descend[descend.length - 1]}' in ${g}`); + } + return child; +} + +function childNames(g: Graph) { + return Array.from(flatten(g.sortedChildren())).map(n => n.id); +} + +function assertGraph(g: GraphNode | undefined): Graph { + if (!g) { throw new Error('Expected a graph node, got undefined'); } + if (!(g instanceof Graph)) { throw new Error(`Expected a Graph, got: ${g}`); } + return g; +} + +class Blueprint extends cdkp.PipelineBase { + protected doBuildPipeline(): void { + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/util.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/util.ts new file mode 100644 index 0000000000000..61e899aef71ce --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/util.ts @@ -0,0 +1,38 @@ +import { Graph, GraphNode } from '../../../lib/helpers-internal'; + +class PlainNode extends GraphNode { } + +export function mkGraph(name: string, block: (b: GraphBuilder) => void) { + const graph = new Graph(name); + block({ + graph(name2, deps, block2) { + const innerG = mkGraph(name2, block2); + innerG.dependOn(...deps); + graph.add(innerG); + return innerG; + }, + node(name2, deps) { + const innerN = new PlainNode(name2); + innerN.dependOn(...deps ?? []); + graph.add(innerN); + return innerN; + }, + }); + return graph; +} + + +interface GraphBuilder { + graph(name: string, deps: GraphNode[], block: (b: GraphBuilder) => void): Graph; + node(name: string, deps?: GraphNode[]): GraphNode; +} + + +export function nodeNames(n: GraphNode): string; +export function nodeNames(ns: GraphNode[]): string[]; +export function nodeNames(ns: GraphNode[][]): string[][]; +export function nodeNames(n: any): any { + if (n instanceof GraphNode) { return n.id; } + if (Array.isArray(n)) { return n.map(nodeNames); } + throw new Error('oh no'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/logicalid-stability.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/logicalid-stability.test.ts new file mode 100644 index 0000000000000..319d25203c92b --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/logicalid-stability.test.ts @@ -0,0 +1,122 @@ +import '@aws-cdk/assert-internal/jest'; +import { Stack } from '@aws-cdk/core'; +import { mkdict } from '../../lib/private/javascript'; +import { PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, MegaAssetsApp, stackTemplate } from '../testhelpers'; + +let legacyApp: TestApp; +let modernApp: TestApp; + +let legacyPipelineStack: Stack; +let modernPipelineStack: Stack; + +beforeEach(() => { + legacyApp = new TestApp({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + 'aws:cdk:enable-path-metadata': true, + }, + }); + modernApp = new TestApp({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + 'aws:cdk:enable-path-metadata': true, + }, + }); + legacyPipelineStack = new Stack(legacyApp, 'PipelineStack', { env: PIPELINE_ENV }); + modernPipelineStack = new Stack(modernApp, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + legacyApp.cleanup(); + modernApp.cleanup(); +}); + +test('stateful or nameable resources have the same logicalID between old and new API', () => { + const legacyPipe = new LegacyTestGitHubNpmPipeline(legacyPipelineStack, 'Cdk'); + legacyPipe.addApplicationStage(new MegaAssetsApp(legacyPipelineStack, 'MyApp', { + numAssets: 2, + })); + + const modernPipe = new ModernTestGitHubNpmPipeline(modernPipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + modernPipe.addStage(new MegaAssetsApp(modernPipelineStack, 'MyApp', { + numAssets: 2, + })); + + const legacyTemplate = stackTemplate(legacyPipelineStack).template; + const modernTemplate = stackTemplate(modernPipelineStack).template; + + const legacyStateful = filterR(legacyTemplate.Resources, isStateful); + const modernStateful = filterR(modernTemplate.Resources, isStateful); + + expect(mapR(modernStateful, typeOfRes)).toEqual(mapR(legacyStateful, typeOfRes)); +}); + +test('nameable resources have the same names between old and new API', () => { + const legacyPipe = new LegacyTestGitHubNpmPipeline(legacyPipelineStack, 'Cdk', { + pipelineName: 'asdf', + }); + legacyPipe.addApplicationStage(new MegaAssetsApp(legacyPipelineStack, 'MyApp', { + numAssets: 2, + })); + + const modernPipe = new ModernTestGitHubNpmPipeline(modernPipelineStack, 'Cdk', { + pipelineName: 'asdf', + crossAccountKeys: true, + }); + modernPipe.addStage(new MegaAssetsApp(modernPipelineStack, 'MyApp', { + numAssets: 2, + })); + + const legacyTemplate = stackTemplate(legacyPipelineStack).template; + const modernTemplate = stackTemplate(modernPipelineStack).template; + + const legacyNamed = filterR(legacyTemplate.Resources, hasName); + const modernNamed = filterR(modernTemplate.Resources, hasName); + + expect(mapR(modernNamed, nameProps)).toEqual(mapR(legacyNamed, nameProps)); +}); + + +const STATEFUL_TYPES = [ + // Holds state + 'AWS::S3::Bucket', + 'AWS::KMS::Key', + 'AWS::KMS::Alias', + // Can be physical-named so will be impossible to replace + 'AWS::CodePipeline::Pipeline', + 'AWS::CodeBuild::Project', +]; + +function filterR(resources: Record, fn: (x: Resource) => boolean): Record { + return mkdict(Object.entries(resources).filter(([, resource]) => fn(resource))); +} + +function mapR(resources: Record, fn: (x: Resource) => A): Record { + return mkdict(Object.entries(resources).map(([lid, resource]) => [lid, fn(resource)] as const)); +} + +function typeOfRes(r: Resource) { + return r.Type; +} + +function isStateful(r: Resource) { + return STATEFUL_TYPES.includes(r.Type); +} + +function nameProps(r: Resource) { + return Object.entries(r.Properties).filter(([prop, _]) => + // Don't care about policy names + prop.endsWith('Name') && prop !== 'PolicyName'); +} + +function hasName(r: Resource) { + return nameProps(r).length > 0; +} + +interface Resource { + readonly Type: string; + readonly Properties: Record; + readonly Metadata?: Record; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/stack-deployment.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/stack-deployment.test.ts new file mode 100644 index 0000000000000..ee9d5b29240ce --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/stack-deployment.test.ts @@ -0,0 +1,66 @@ +import * as path from 'path'; +import * as assets from '@aws-cdk/aws-s3-assets'; +import { Stack, Stage } from '@aws-cdk/core'; +import { StageDeployment } from '../../lib'; +import { TestApp } from '../testhelpers/test-app'; + +test('"templateAsset" represents the CFN template of the stack', () => { + // GIVEN + const stage = new Stage(new TestApp(), 'MyStage'); + new Stack(stage, 'MyStack'); + + // WHEN + const sd = StageDeployment.fromStage(stage); + + // THEN + expect(sd.stacks[0].templateAsset).not.toBeUndefined(); + expect(sd.stacks[0].templateAsset?.assetId).not.toBeUndefined(); + expect(sd.stacks[0].templateAsset?.assetManifestPath).not.toBeUndefined(); + expect(sd.stacks[0].templateAsset?.assetSelector).not.toBeUndefined(); + expect(sd.stacks[0].templateAsset?.assetType).toBe('file'); + expect(sd.stacks[0].templateAsset?.isTemplate).toBeTruthy(); +}); + +describe('templateUrl', () => { + test('includes the https:// s3 URL of the template file', () => { + // GIVEN + const stage = new Stage(new TestApp(), 'MyStage', { env: { account: '111', region: 'us-east-1' } }); + new Stack(stage, 'MyStack'); + + // WHEN + const sd = StageDeployment.fromStage(stage); + + // THEN + expect(sd.stacks[0].templateUrl).toBe('https://cdk-hnb659fds-assets-111-us-east-1.s3.us-east-1.amazonaws.com/4ef627170a212f66f5d1d9240d967ef306f4820ff9cb05b3a7ec703df6af6c3e.json'); + }); + + test('without region', () => { + // GIVEN + const stage = new Stage(new TestApp(), 'MyStage', { env: { account: '111' } }); + new Stack(stage, 'MyStack'); + + // WHEN + const sd = StageDeployment.fromStage(stage); + + // THEN + expect(sd.stacks[0].templateUrl).toBe('https://cdk-hnb659fds-assets-111-.s3.amazonaws.com/$%7BAWS::Region%7D/4ef627170a212f66f5d1d9240d967ef306f4820ff9cb05b3a7ec703df6af6c3e.json'); + }); + +}); + + +test('"requiredAssets" contain only assets that are not the template', () => { + // GIVEN + const stage = new Stage(new TestApp(), 'MyStage'); + const stack = new Stack(stage, 'MyStack'); + new assets.Asset(stack, 'Asset', { path: path.join(__dirname, 'fixtures') }); + + // WHEN + const sd = StageDeployment.fromStage(stage); + + // THEN + expect(sd.stacks[0].assets.length).toBe(1); + expect(sd.stacks[0].assets[0].assetType).toBe('file'); + expect(sd.stacks[0].assets[0].isTemplate).toBeFalsy(); +}); + diff --git a/packages/@aws-cdk/pipelines/test/build-role-policy-statements.test.ts b/packages/@aws-cdk/pipelines/test/build-role-policy-statements.test.ts deleted file mode 100644 index 01650e730d53e..0000000000000 --- a/packages/@aws-cdk/pipelines/test/build-role-policy-statements.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { arrayWith, deepObjectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import { PolicyStatement } from '@aws-cdk/aws-iam'; -import { Stack } from '@aws-cdk/core'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let sourceArtifact: codepipeline.Artifact; -let cloudAssemblyArtifact: codepipeline.Artifact; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStackPolicy', { env: PIPELINE_ENV }); - sourceArtifact = new codepipeline.Artifact(); - cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('Build project includes codeartifact policy statements for role', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - rolePolicyStatements: [ - new PolicyStatement({ - actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], - resources: ['arn:my:arn'], - }), - ], - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith(deepObjectLike({ - Action: [ - 'codeartifact:*', - 'sts:GetServiceBearerToken', - ], - Resource: 'arn:my:arn', - })), - }, - }); - }); -}); diff --git a/packages/@aws-cdk/pipelines/test/builds.test.ts b/packages/@aws-cdk/pipelines/test/builds.test.ts deleted file mode 100644 index 70c4d31a18907..0000000000000 --- a/packages/@aws-cdk/pipelines/test/builds.test.ts +++ /dev/null @@ -1,621 +0,0 @@ -import { arrayWith, deepObjectLike, encodedJson, objectLike, Capture } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as cbuild from '@aws-cdk/aws-codebuild'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecr from '@aws-cdk/aws-ecr'; -import * as s3 from '@aws-cdk/aws-s3'; -import { Stack } from '@aws-cdk/core'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let sourceArtifact: codepipeline.Artifact; -let cloudAssemblyArtifact: codepipeline.Artifact; - -beforeEach(() => { - app = new TestApp({ outdir: 'testcdk.out' }); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - sourceArtifact = new codepipeline.Artifact(); - cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('SimpleSynthAction takes arrays of commands', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: new cdkp.SimpleSynthAction({ - sourceArtifact, - cloudAssemblyArtifact, - installCommands: ['install1', 'install2'], - buildCommands: ['build1', 'build2'], - testCommands: ['test1', 'test2'], - synthCommand: 'cdk synth', - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: [ - 'install1', - 'install2', - ], - }, - build: { - commands: [ - 'build1', - 'build2', - 'test1', - 'test2', - 'cdk synth', - ], - }, - }, - })), - }, - }); - }); -}); - -behavior('%s build automatically determines artifact base-directory', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ sourceArtifact, cloudAssemblyArtifact }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - artifacts: { - 'base-directory': 'cdk.out', - }, - })), - }, - }); - }); -}); - -behavior('%s build respects subdirectory', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ - sourceArtifact, - cloudAssemblyArtifact, - subdirectory: 'subdir', - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: arrayWith('cd subdir'), - }, - }, - artifacts: { - 'base-directory': 'subdir/cdk.out', - }, - })), - }, - }); - }); -}); - -behavior('%s build sets UNSAFE_PERM=true', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ - sourceArtifact, - cloudAssemblyArtifact, - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - EnvironmentVariables: [ - { - Name: 'NPM_CONFIG_UNSAFE_PERM', - Type: 'PLAINTEXT', - Value: 'true', - }, - ], - }, - }); - }); -}); - -behavior('%s assumes no build step by default', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ sourceArtifact, cloudAssemblyArtifact }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: ['npx cdk synth'], - }, - }, - })), - }, - }); - }); -}); - -behavior('environmentVariables must be rendered in the action', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: new cdkp.SimpleSynthAction({ - sourceArtifact, - cloudAssemblyArtifact, - environmentVariables: { - VERSION: { value: codepipeline.GlobalVariables.executionId }, - }, - synthCommand: 'synth', - }), - }); - - // THEN - const theHash = Capture.aString(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Build', - Actions: [ - objectLike({ - Name: 'Synth', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ - { - name: 'VERSION', - type: 'PLAINTEXT', - value: '#{codepipeline.PipelineExecutionId}', - }, - { - name: '_PROJECT_CONFIG_HASH', - type: 'PLAINTEXT', - value: theHash.capture(), - }, - ]), - }), - }), - ], - }), - }); - }); -}); - -behavior('complex setup with environment variables still renders correct project', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: new cdkp.SimpleSynthAction({ - sourceArtifact, - cloudAssemblyArtifact, - environmentVariables: { - SOME_ENV_VAR: { value: 'SomeValue' }, - }, - environment: { - environmentVariables: { - INNER_VAR: { value: 'InnerValue' }, - }, - privileged: true, - }, - installCommands: [ - 'install1', - 'install2', - ], - synthCommand: 'synth', - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ - PrivilegedMode: true, - EnvironmentVariables: [ - { - Name: 'INNER_VAR', - Type: 'PLAINTEXT', - Value: 'InnerValue', - }, - ], - }), - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: ['install1', 'install2'], - }, - build: { - commands: ['synth'], - }, - }, - })), - }, - }); - }); -}); - -behavior('%s can have its install command overridden', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ - sourceArtifact, - cloudAssemblyArtifact, - installCommand: '/bin/true', - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: ['/bin/true'], - }, - }, - })), - }, - }); - }); -}); - -behavior('%s can have its test commands set', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ - sourceArtifact, - cloudAssemblyArtifact, - installCommand: '/bin/true', - testCommands: ['echo "Running tests"'], - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(objectLike({ - phases: { - pre_build: { - commands: ['/bin/true'], - }, - build: { - commands: ['echo "Running tests"', 'npx cdk synth'], - }, - }, - })), - }, - }); - }); -}); - -behavior('Standard (NPM) synth can output additional artifacts', (suite) => { - suite.legacy(() => { - // WHEN - const addlArtifact = new codepipeline.Artifact('IntegTest'); - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - additionalArtifacts: [ - { - artifact: addlArtifact, - directory: 'test', - }, - ], - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - artifacts: { - 'secondary-artifacts': { - CloudAsm: { - 'base-directory': 'cdk.out', - 'files': '**/*', - }, - IntegTest: { - 'base-directory': 'test', - 'files': '**/*', - }, - }, - }, - })), - }, - }); - }); -}); - -behavior('Standard (NPM) synth can run in a VPC', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - vpc: new ec2.Vpc(pipelineStack, 'NpmSynthTestVpc'), - sourceArtifact, - cloudAssemblyArtifact, - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - VpcConfig: { - SecurityGroupIds: [ - { 'Fn::GetAtt': ['CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', 'GroupId'] }, - ], - Subnets: [ - { Ref: 'NpmSynthTestVpcPrivateSubnet1Subnet81E3AA56' }, - { Ref: 'NpmSynthTestVpcPrivateSubnet2SubnetC1CA3EF0' }, - { Ref: 'NpmSynthTestVpcPrivateSubnet3SubnetA04163EE' }, - ], - VpcId: { Ref: 'NpmSynthTestVpc5E703F25' }, - }, - }); - - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - Roles: [ - { Ref: 'CdkPipelineBuildSynthCdkBuildProjectRole5E173C62' }, - ], - PolicyDocument: { - Statement: arrayWith({ - Action: arrayWith('ec2:DescribeSecurityGroups'), - Effect: 'Allow', - Resource: '*', - }), - }, - }); - }); -}); - -behavior('Standard (Yarn) synth can run in a VPC', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardYarnSynth({ - vpc: new ec2.Vpc(pipelineStack, 'YarnSynthTestVpc'), - sourceArtifact, - cloudAssemblyArtifact, - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - VpcConfig: { - SecurityGroupIds: [ - { - 'Fn::GetAtt': [ - 'CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', - 'GroupId', - ], - }, - ], - Subnets: [ - { - Ref: 'YarnSynthTestVpcPrivateSubnet1Subnet2805334B', - }, - { - Ref: 'YarnSynthTestVpcPrivateSubnet2SubnetDCFBF596', - }, - { - Ref: 'YarnSynthTestVpcPrivateSubnet3SubnetE11E0C86', - }, - ], - VpcId: { - Ref: 'YarnSynthTestVpc5F654735', - }, - }, - }); - }); -}); - -behavior('Pipeline action contains a hash that changes as the buildspec changes', (suite) => { - suite.legacy(() => { - const hash1 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - })); - - // To make sure the hash is not just random :) - const hash1prime = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - })); - - const hash2 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - installCommand: 'do install', - })); - const hash3 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - environment: { - computeType: cbuild.ComputeType.LARGE, - }, - })); - const hash4 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - environment: { - environmentVariables: { - xyz: { value: 'SOME-VALUE' }, - }, - }, - })); - - expect(hash1).toEqual(hash1prime); - - expect(hash1).not.toEqual(hash2); - expect(hash1).not.toEqual(hash3); - expect(hash1).not.toEqual(hash4); - expect(hash2).not.toEqual(hash3); - expect(hash2).not.toEqual(hash4); - expect(hash3).not.toEqual(hash4); - - function synthWithAction(cb: (sourceArtifact: codepipeline.Artifact, cloudAssemblyArtifact: codepipeline.Artifact) => codepipeline.IAction) { - const _app = new TestApp({ outdir: 'testcdk.out' }); - const _pipelineStack = new Stack(_app, 'PipelineStack', { env: PIPELINE_ENV }); - const _sourceArtifact = new codepipeline.Artifact(); - const _cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); - - new TestGitHubNpmPipeline(_pipelineStack, 'Cdk', { - sourceArtifact: _sourceArtifact, - cloudAssemblyArtifact: _cloudAssemblyArtifact, - synthAction: cb(_sourceArtifact, _cloudAssemblyArtifact), - }); - - const theHash = Capture.aString(); - expect(_pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Build', - Actions: [ - objectLike({ - Name: 'Synth', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ - { - name: '_PROJECT_CONFIG_HASH', - type: 'PLAINTEXT', - value: theHash.capture(), - }, - ]), - }), - }), - ], - }), - }); - - return theHash.capturedValue; - } - }); -}); - -behavior('SimpleSynthAction is IGrantable', (suite) => { - suite.legacy(() => { - // GIVEN - const synthAction = cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - }); - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction, - }); - const bucket = new s3.Bucket(pipelineStack, 'Bucket'); - - // WHEN - bucket.grantRead(synthAction); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith(deepObjectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - })), - }, - }); - }); -}); - -behavior('SimpleSynthAction can reference an imported ECR repo', (suite) => { - suite.legacy(() => { - // Repro from https://github.com/aws/aws-cdk/issues/10535 - - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - environment: { - buildImage: cbuild.LinuxBuildImage.fromEcrRepository( - ecr.Repository.fromRepositoryName(pipelineStack, 'ECRImage', 'my-repo-name'), - ), - }, - }), - }); - - // THEN -- no exception (necessary for linter) - expect(true).toBeTruthy(); - }); -}); - -function npmYarnBuild(npmYarn: string) { - if (npmYarn === 'npm') { return cdkp.SimpleSynthAction.standardNpmSynth; } - if (npmYarn === 'yarn') { return cdkp.SimpleSynthAction.standardYarnSynth; } - throw new Error(`Expecting npm|yarn: ${npmYarn}`); -} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts similarity index 56% rename from packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts rename to packages/@aws-cdk/pipelines/test/compliance/assets.test.ts index 55e45d1808476..0f71dbde34650 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts @@ -1,18 +1,11 @@ import * as fs from 'fs'; import * as path from 'path'; -import { arrayWith, deepObjectLike, encodedJson, notMatching, objectLike, ResourcePart, stringLike, SynthUtils } from '@aws-cdk/assert-internal'; +import { arrayWith, Capture, deepObjectLike, encodedJson, notMatching, objectLike, ResourcePart, stringLike, SynthUtils } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; import * as cb from '@aws-cdk/aws-codebuild'; -import * as cp from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; -import * as s3_assets from '@aws-cdk/aws-s3-assets'; -import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Stack, Stage, StageProps } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubAction, TestGitHubNpmPipeline } from './testutil'; +import { Stack } from '@aws-cdk/core'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, FileAssetApp, MegaAssetsApp, TwoFileAssetsApp, DockerAssetApp, PlainStackApp } from '../testhelpers'; const FILE_ASSET_SOURCE_HASH = '8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5'; const FILE_ASSET_SOURCE_HASH2 = 'ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e'; @@ -22,40 +15,58 @@ const IMAGE_PUBLISHING_ROLE = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role let app: TestApp; let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); afterEach(() => { app.cleanup(); }); describe('basic pipeline', () => { - beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); - }); - behavior('no assets stage if the application has no assets', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new PlainStackApp(app, 'App')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new PlainStackApp(app, 'App')); + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // THEN expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: notMatching(arrayWith(objectLike({ Name: 'Assets', }))), }); - }); + } }); describe('asset stage placement', () => { behavior('assets stage comes before any user-defined stages', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new FileAssetApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ objectLike({ Name: 'Source' }), @@ -65,52 +76,26 @@ describe('basic pipeline', () => { objectLike({ Name: 'App' }), ], }); - }); + } }); - behavior('assets stage inserted after existing pipeline actions', (suite) => { + behavior('up to 50 assets fit in a single stage', (suite) => { suite.legacy(() => { // WHEN - const sourceArtifact = new cp.Artifact(); - const cloudAssemblyArtifact = new cp.Artifact(); - const existingCodePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { - stages: [ - { - stageName: 'CustomSource', - actions: [new TestGitHubAction(sourceArtifact)], - }, - { - stageName: 'CustomBuild', - actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], - }, - ], - }); - pipeline = new cdkp.CdkPipeline(pipelineStack, 'CdkEmptyPipeline', { - cloudAssemblyArtifact: cloudAssemblyArtifact, - selfMutating: false, - codePipeline: existingCodePipeline, - // No source/build actions - }); - pipeline.addApplicationStage(new FileAssetApp(app, 'App')); + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 50 })); - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'CustomSource' }), - objectLike({ Name: 'CustomBuild' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'App' }), - ], - }); + THEN_codePipelineExpectation(); }); - }); - behavior('up to 50 assets fit in a single stage', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 50 })); + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new MegaAssetsApp(app, 'App', { numAssets: 50 })); + + THEN_codePipelineExpectation(); + }); - // THEN + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ objectLike({ Name: 'Source' }), @@ -120,55 +105,87 @@ describe('basic pipeline', () => { objectLike({ Name: 'App' }), ], }); - }); + } }); behavior('51 assets triggers a second stage', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 51 })); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new MegaAssetsApp(app, 'App', { numAssets: 51 })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ objectLike({ Name: 'Source' }), objectLike({ Name: 'Build' }), objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'Assets2' }), + objectLike({ Name: stringLike('Assets*') }), + objectLike({ Name: stringLike('Assets*2') }), objectLike({ Name: 'App' }), ], }); - }); + } }); behavior('101 assets triggers a third stage', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 101 })); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new MegaAssetsApp(app, 'App', { numAssets: 101 })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ objectLike({ Name: 'Source' }), objectLike({ Name: 'Build' }), objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'Assets2' }), - objectLike({ Name: 'Assets3' }), + objectLike({ Name: stringLike('Assets*') }), // 'Assets' vs 'Assets.1' + objectLike({ Name: stringLike('Assets*2') }), + objectLike({ Name: stringLike('Assets*3') }), objectLike({ Name: 'App' }), ], }); - }); + } }); }); behavior('command line properly locates assets in subassembly', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new FileAssetApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', @@ -183,15 +200,26 @@ describe('basic pipeline', () => { })), }, }); - }); + } }); behavior('multiple assets are published in parallel', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: arrayWith({ Name: 'Assets', @@ -201,7 +229,7 @@ describe('basic pipeline', () => { ], }), }); - }); + } }); behavior('assets are also published when using the lower-level addStackArtifactDeployment', (suite) => { @@ -210,6 +238,7 @@ describe('basic pipeline', () => { const asm = new FileAssetApp(app, 'FileAssetApp').synth(); // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addStage('SomeStage').addStackArtifactDeployment(asm.getStackByName('FileAssetApp-Stack')); // THEN @@ -225,14 +254,29 @@ describe('basic pipeline', () => { }), }); }); + + // This function does not exist in the modern API + suite.doesNotApply.modern(); }); behavior('file image asset publishers do not use privilegedmode', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new FileAssetApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Source: { BuildSpec: encodedJson(deepObjectLike({ @@ -248,15 +292,25 @@ describe('basic pipeline', () => { Image: 'aws/codebuild/standard:5.0', }), }); - }); + } }); behavior('docker image asset publishers use privilegedmode', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new DockerAssetApp(app, 'DockerAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Source: { BuildSpec: encodedJson(deepObjectLike({ @@ -272,20 +326,30 @@ describe('basic pipeline', () => { PrivilegedMode: true, }), }); - }); + } }); - behavior('can control fix/CLI version used in pipeline selfupdate', (suite) => { + behavior('can control fix/CLI version used in asset publishing', (suite) => { suite.legacy(() => { - // WHEN - const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); - const pipeline2 = new TestGitHubNpmPipeline(stack2, 'Cdk2', { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { cdkCliVersion: '1.2.3', }); - pipeline2.addApplicationStage(new FileAssetApp(stack2, 'FileAssetApp')); + pipeline.addApplicationStage(new FileAssetApp(pipelineStack, 'FileAssetApp')); - // THEN - expect(stack2).toHaveResourceLike('AWS::CodeBuild::Project', { + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + cliVersion: '1.2.3', + }); + pipeline.addStage(new FileAssetApp(pipelineStack, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, @@ -299,14 +363,29 @@ describe('basic pipeline', () => { })), }, }); - }); + } }); describe('asset roles and policies', () => { behavior('includes file publishing assets role for apps with file assets', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + pipeline.addStage(new FileAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ @@ -325,11 +404,12 @@ describe('basic pipeline', () => { }); expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); - }); + } }); behavior('publishing assets role may assume roles from multiple environments', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); pipeline.addApplicationStage(new FileAssetApp(app, 'App2', { env: { @@ -338,27 +418,80 @@ describe('basic pipeline', () => { }, })); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + + pipeline.addStage(new FileAssetApp(app, 'App1')); + pipeline.addStage(new FileAssetApp(app, 'App2', { + env: { + account: '0123456789012', + region: 'eu-west-1', + }, + })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy([FILE_PUBLISHING_ROLE, 'arn:${AWS::Partition}:iam::0123456789012:role/cdk-hnb659fds-file-publishing-role-0123456789012-eu-west-1'], 'CdkAssetsFileRole6BE17A07')); - }); + } }); behavior('publishing assets role de-dupes assumed roles', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); pipeline.addApplicationStage(new FileAssetApp(app, 'App2')); pipeline.addApplicationStage(new FileAssetApp(app, 'App3')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + pipeline.addStage(new FileAssetApp(app, 'App1')); + pipeline.addStage(new FileAssetApp(app, 'App2')); + pipeline.addStage(new FileAssetApp(app, 'App3')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); - }); + } }); behavior('includes image publishing assets role for apps with Docker assets', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ @@ -377,37 +510,72 @@ describe('basic pipeline', () => { }); expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(IMAGE_PUBLISHING_ROLE, 'CdkAssetsDockerRole484B6DD3')); - }); + } }); behavior('includes both roles for apps with both file and Docker assets', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); pipeline.addApplicationStage(new DockerAssetApp(app, 'App2')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + pipeline.addStage(new FileAssetApp(app, 'App1')); + pipeline.addStage(new DockerAssetApp(app, 'App2')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(IMAGE_PUBLISHING_ROLE, 'CdkAssetsDockerRole484B6DD3')); - }); + } }); }); }); - behavior('can supply pre-install scripts to asset upload', (suite) => { suite.legacy(() => { - // WHEN - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { assetPreInstallCommands: [ 'npm config set registry https://registry.com', ], }); pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + assetPublishingCodeBuildDefaults: { + partialBuildSpec: cb.BuildSpec.fromObject({ + version: '0.2', + phases: { + install: { + commands: [ + 'npm config set registry https://registry.com', + ], + }, + }, + }), + }, + }); + pipeline.addStage(new FileAssetApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', @@ -422,27 +590,35 @@ behavior('can supply pre-install scripts to asset upload', (suite) => { })), }, }); - - app.cleanup(); - }); + } }); describe('pipeline with VPC', () => { let vpc: ec2.Vpc; beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); vpc = new ec2.Vpc(pipelineStack, 'Vpc'); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - vpc, - }); }); behavior('asset CodeBuild Project uses VPC subnets', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + vpc, + }); pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + pipeline.addStage(new DockerAssetApp(app, 'DockerAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // THEN expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { VpcConfig: objectLike({ @@ -457,16 +633,27 @@ describe('pipeline with VPC', () => { VpcId: { Ref: 'Vpc8378EB38' }, }), }); - }); + } }); behavior('Pipeline-generated CodeBuild Projects have appropriate execution role permissions', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + vpc, + }); pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); - // THEN + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + pipeline.addStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); + function THEN_codePipelineExpectation() { // Assets Project expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { Roles: [ @@ -480,13 +667,28 @@ describe('pipeline with VPC', () => { }), }, }); - }); + } }); behavior('Asset publishing CodeBuild Projects have a dependency on attached policies to the role', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + vpc, + }); pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + pipeline.addStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // Assets Project expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Properties: { @@ -501,32 +703,33 @@ describe('pipeline with VPC', () => { 'CdkAssetsDockerRoleVpcPolicy86CA024B', ], }, ResourcePart.CompleteDefinition); - }); + } }); }); describe('pipeline with single asset publisher', () => { - let otherPipelineStack: Stack; - let otherPipeline: cdkp.CdkPipeline; - - beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - singlePublisherPerType: true, - }); - otherPipelineStack = new Stack(app, 'OtherPipelineStack', { env: PIPELINE_ENV }); - otherPipeline = new TestGitHubNpmPipeline(otherPipelineStack, 'Cdk', { - singlePublisherPerType: true, - }); - }); - behavior('multiple assets are using the same job in singlePublisherMode', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + singlePublisherPerType: true, + }); pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + publishAssetsInParallel: false, + }); + pipeline.addStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // THEN + const buildSpecName = Capture.aString(); expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: arrayWith({ Name: 'Assets', @@ -541,306 +744,69 @@ describe('pipeline with single asset publisher', () => { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml', + BuildSpec: buildSpecName.capture(stringLike('buildspec-*.yaml')), }, }); const assembly = SynthUtils.synthesize(pipelineStack, { skipValidation: true }).assembly; - const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml')).toString()); + + const actualFileName = buildSpecName.capturedValue; + + const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, actualFileName), { encoding: 'utf-8' })); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`); - }); + } }); behavior('other pipeline writes to separate assets build spec file', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + singlePublisherPerType: true, + }); pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); - otherPipeline.addApplicationStage(new TwoFileAssetsApp(app, 'OtherFileAssetApp')); - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Source: { - BuildSpec: 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml', - }, + const pipelineStack2 = new Stack(app, 'PipelineStack2', { env: PIPELINE_ENV }); + const otherPipeline = new LegacyTestGitHubNpmPipeline(pipelineStack2, 'Cdk', { + singlePublisherPerType: true, }); - expect(otherPipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Source: { - BuildSpec: 'buildspec-assets-OtherPipelineStack-Cdk-Assets-FileAsset.yaml', - }, - }); - }); - }); -}); - -describe('pipeline with Docker credentials', () => { - const secretSynthArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:synth-012345'; - const secretUpdateArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:update-012345'; - const secretPublishArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:publish-012345'; - let secretSynth: secretsmanager.ISecret; - let secretUpdate: secretsmanager.ISecret; - let secretPublish: secretsmanager.ISecret; + otherPipeline.addApplicationStage(new TwoFileAssetsApp(app, 'OtherFileAssetApp')); - beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - - secretSynth = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret1', secretSynthArn); - secretUpdate = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret2', secretUpdateArn); - secretPublish = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret3', secretPublishArn); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - dockerCredentials: [ - cdkp.DockerCredential.customRegistry('synth.example.com', secretSynth, { - usages: [cdkp.DockerCredentialUsage.SYNTH], - }), - cdkp.DockerCredential.customRegistry('selfupdate.example.com', secretUpdate, { - usages: [cdkp.DockerCredentialUsage.SELF_UPDATE], - }), - cdkp.DockerCredential.customRegistry('publish.example.com', secretPublish, { - usages: [cdkp.DockerCredentialUsage.ASSET_PUBLISHING], - }), - ], + THEN_codePipelineExpectation(pipelineStack2); }); - }); - behavior('synth action receives install commands and access to relevant credentials', (suite) => { - suite.legacy(() => { - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); - - const expectedCredsConfig = JSON.stringify({ - version: '1.0', - domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + publishAssetsInParallel: false, }); + pipeline.addStage(new TwoFileAssetsApp(app, 'FileAssetApp')); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { Image: 'aws/codebuild/standard:5.0' }, - // Prove we're looking at the Synth project - ServiceRole: { 'Fn::GetAtt': ['CdkPipelineBuildSynthCdkBuildProjectRole5E173C62', 'Arn'] }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: [ - 'npm ci', - 'mkdir $HOME/.cdk', - `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - ], - }, - }, - })), - }, + const pipelineStack2 = new Stack(app, 'PipelineStack2', { env: PIPELINE_ENV }); + const otherPipeline = new ModernTestGitHubNpmPipeline(pipelineStack2, 'Cdk', { + publishAssetsInParallel: false, }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith({ - Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], - Effect: 'Allow', - Resource: secretSynthArn, - }), - Version: '2012-10-17', - }, - Roles: [{ Ref: 'CdkPipelineBuildSynthCdkBuildProjectRole5E173C62' }], - }); - }); - }); + otherPipeline.addStage(new TwoFileAssetsApp(app, 'OtherFileAssetApp')); - behavior('synth action receives Windows install commands if a Windows image is detected', (suite) => { - suite.legacy(() => { - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk2', { - dockerCredentials: [ - cdkp.DockerCredential.customRegistry('synth.example.com', secretSynth, { - usages: [cdkp.DockerCredentialUsage.SYNTH], - }), - cdkp.DockerCredential.customRegistry('selfupdate.example.com', secretUpdate, { - usages: [cdkp.DockerCredentialUsage.SELF_UPDATE], - }), - cdkp.DockerCredential.customRegistry('publish.example.com', secretPublish, { - usages: [cdkp.DockerCredentialUsage.ASSET_PUBLISHING], - }), - ], - npmSynthOptions: { - environment: { - buildImage: cb.WindowsBuildImage.WINDOWS_BASE_2_0, - }, - }, - }); - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); - - const expectedCredsConfig = JSON.stringify({ - version: '1.0', - domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, - }); - - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { Image: 'aws/codebuild/windows-base:2.0' }, - // Prove we're looking at the Synth project - ServiceRole: { 'Fn::GetAtt': ['Cdk2PipelineBuildSynthCdkBuildProjectRole9869128F', 'Arn'] }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: [ - 'npm ci', - 'mkdir %USERPROFILE%\\.cdk', - `echo '${expectedCredsConfig}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, - ], - }, - }, - })), - }, - }); + THEN_codePipelineExpectation(pipelineStack2); }); - }); - - behavior('self-update receives install commands and access to relevant credentials', (suite) => { - suite.legacy(() => { - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); - - const expectedCredsConfig = JSON.stringify({ - version: '1.0', - domainCredentials: { 'selfupdate.example.com': { secretsManagerSecretId: secretUpdateArn } }, - }); + function THEN_codePipelineExpectation(pipelineStack2: Stack) { + // THEN + const buildSpecName1 = Capture.aString(); + const buildSpecName2 = Capture.aString(); expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { Image: 'aws/codebuild/standard:5.0' }, - // Prove we're looking at the SelfMutate project - ServiceRole: { 'Fn::GetAtt': ['CdkUpdatePipelineSelfMutationRoleAAF1B580', 'Arn'] }, Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: [ - 'npm install -g aws-cdk', - 'mkdir $HOME/.cdk', - `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - ], - }, - }, - })), - }, - }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith({ - Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], - Effect: 'Allow', - Resource: secretUpdateArn, - }), - Version: '2012-10-17', + BuildSpec: buildSpecName1.capture(stringLike('buildspec-*.yaml')), }, - Roles: [{ Ref: 'CdkUpdatePipelineSelfMutationRoleAAF1B580' }], }); - }); - }); - - behavior('asset publishing receives install commands and access to relevant credentials', (suite) => { - suite.legacy(() => { - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); - - const expectedCredsConfig = JSON.stringify({ - version: '1.0', - domainCredentials: { 'publish.example.com': { secretsManagerSecretId: secretPublishArn } }, - }); - - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { Image: 'aws/codebuild/standard:5.0' }, - // Prove we're looking at the Publishing project - ServiceRole: { 'Fn::GetAtt': ['CdkAssetsDockerRole484B6DD3', 'Arn'] }, + expect(pipelineStack2).toHaveResourceLike('AWS::CodeBuild::Project', { Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: [ - 'mkdir $HOME/.cdk', - `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - 'npm install -g cdk-assets', - ], - }, - }, - })), + BuildSpec: buildSpecName2.capture(stringLike('buildspec-*.yaml')), }, }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith({ - Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], - Effect: 'Allow', - Resource: secretPublishArn, - }), - Version: '2012-10-17', - }, - Roles: [{ Ref: 'CdkAssetsDockerRole484B6DD3' }], - }); - }); - }); - -}); - -class PlainStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - new BucketStack(this, 'Stack'); - } -} - -class FileAssetApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - const stack = new Stack(this, 'Stack'); - new s3_assets.Asset(stack, 'Asset', { - path: path.join(__dirname, 'test-file-asset.txt'), - }); - } -} -class TwoFileAssetsApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - const stack = new Stack(this, 'Stack'); - new s3_assets.Asset(stack, 'Asset1', { - path: path.join(__dirname, 'test-file-asset.txt'), - }); - new s3_assets.Asset(stack, 'Asset2', { - path: path.join(__dirname, 'test-file-asset-two.txt'), - }); - } -} - -class DockerAssetApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - const stack = new Stack(this, 'Stack'); - new ecr_assets.DockerImageAsset(stack, 'Asset', { - directory: path.join(__dirname, 'test-docker-asset'), - }); - } -} - -interface MegaAssetsAppProps extends StageProps { - readonly numAssets: number; -} - -// Creates a mix of file and image assets, up to a specified count -class MegaAssetsApp extends Stage { - constructor(scope: Construct, id: string, props: MegaAssetsAppProps) { - super(scope, id, props); - const stack = new Stack(this, 'Stack'); - - let assetCount = 0; - for (; assetCount < props.numAssets / 2; assetCount++) { - new s3_assets.Asset(stack, `Asset${assetCount}`, { - path: path.join(__dirname, 'test-file-asset.txt'), - assetHash: `FileAsset${assetCount}`, - }); - } - for (; assetCount < props.numAssets; assetCount++) { - new ecr_assets.DockerImageAsset(stack, `Asset${assetCount}`, { - directory: path.join(__dirname, 'test-docker-asset'), - extraHash: `FileAsset${assetCount}`, - }); + expect(buildSpecName1.capturedValue).not.toEqual(buildSpecName2.capturedValue); } - } -} - + }); +}); function expectedAssetRolePolicy(assumeRolePattern: string | string[], attachedRole: string) { if (typeof assumeRolePattern === 'string') { assumeRolePattern = [assumeRolePattern]; } diff --git a/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts b/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts new file mode 100644 index 0000000000000..1248831737bdf --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts @@ -0,0 +1,228 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import * as fs from 'fs'; +import * as path from 'path'; +import { arrayWith, Capture, objectLike, stringLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { Stack, Stage, StageProps, Tags } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { behavior, LegacyTestGitHubNpmPipeline, OneStackApp, BucketStack, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('stack templates in nested assemblies are correctly addressed', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new OneStackApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'App', + Actions: arrayWith( + objectLike({ + Name: stringLike('*Prepare'), + InputArtifacts: [objectLike({})], + Configuration: objectLike({ + StackName: 'App-Stack', + TemplatePath: stringLike('*::assembly-App/*.template.json'), + }), + }), + ), + }), + }); + } +}); + +behavior('obvious error is thrown when stage contains no stacks', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + // WHEN + expect(() => { + pipeline.addApplicationStage(new Stage(app, 'EmptyStage')); + }).toThrow(/should contain at least one Stack/); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + // WHEN + expect(() => { + pipeline.addStage(new Stage(app, 'EmptyStage')); + }).toThrow(/should contain at least one Stack/); + }); +}); + +behavior('overridden stack names are respected', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackAppWithCustomName(app, 'App1')); + pipeline.addApplicationStage(new OneStackAppWithCustomName(app, 'App2')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new OneStackAppWithCustomName(app, 'App1')); + pipeline.addStage(new OneStackAppWithCustomName(app, 'App2')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith( + { + Name: 'App1', + Actions: arrayWith(objectLike({ + Name: stringLike('*Prepare'), + Configuration: objectLike({ + StackName: 'MyFancyStack', + }), + })), + }, + { + Name: 'App2', + Actions: arrayWith(objectLike({ + Name: stringLike('*Prepare'), + Configuration: objectLike({ + StackName: 'MyFancyStack', + }), + })), + }, + ), + }); + } +}); + +behavior('changing CLI version leads to a different pipeline structure (restarting it)', (suite) => { + suite.legacy(() => { + // GIVEN + const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); + const stack3 = new Stack(app, 'Stack3', { env: PIPELINE_ENV }); + + // WHEN + new LegacyTestGitHubNpmPipeline(stack2, 'Cdk', { + cdkCliVersion: '1.2.3', + }); + new LegacyTestGitHubNpmPipeline(stack3, 'Cdk', { + cdkCliVersion: '4.5.6', + }); + + THEN_codePipelineExpectation(stack2, stack3); + }); + + suite.modern(() => { + // GIVEN + const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); + const stack3 = new Stack(app, 'Stack3', { env: PIPELINE_ENV }); + + // WHEN + new ModernTestGitHubNpmPipeline(stack2, 'Cdk', { + cliVersion: '1.2.3', + }); + new ModernTestGitHubNpmPipeline(stack3, 'Cdk', { + cliVersion: '4.5.6', + }); + + THEN_codePipelineExpectation(stack2, stack3); + }); + + function THEN_codePipelineExpectation(stack2: Stack, stack3: Stack) { + // THEN + const structure2 = Capture.anyType(); + const structure3 = Capture.anyType(); + + expect(stack2).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: structure2.capture(), + }); + expect(stack3).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: structure3.capture(), + }); + + expect(JSON.stringify(structure2.capturedValue)).not.toEqual(JSON.stringify(structure3.capturedValue)); + } +}); + +behavior('tags get reflected in pipeline', (suite) => { + suite.legacy(() => { + // WHEN + const stage = new OneStackApp(app, 'App'); + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + Tags.of(stage).add('CostCenter', 'F00B4R'); + pipeline.addApplicationStage(stage); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const stage = new OneStackApp(app, 'App'); + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + Tags.of(stage).add('CostCenter', 'F00B4R'); + pipeline.addStage(stage); + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + const templateConfig = Capture.aString(); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'App', + Actions: arrayWith( + objectLike({ + Name: stringLike('*Prepare'), + InputArtifacts: [objectLike({})], + Configuration: objectLike({ + StackName: 'App-Stack', + TemplateConfiguration: templateConfig.capture(stringLike('*::assembly-App/*.template.*json')), + }), + }), + ), + }), + }); + + const [, relConfigFile] = templateConfig.capturedValue.split('::'); + const absConfigFile = path.join(app.outdir, relConfigFile); + const configFile = JSON.parse(fs.readFileSync(absConfigFile, { encoding: 'utf-8' })); + expect(configFile).toEqual(expect.objectContaining({ + Tags: { + CostCenter: 'F00B4R', + }, + })); + } +}); + +class OneStackAppWithCustomName extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + new BucketStack(this, 'Stack', { + stackName: 'MyFancyStack', + }); + } +} diff --git a/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts b/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts new file mode 100644 index 0000000000000..5ada88b49b937 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts @@ -0,0 +1,292 @@ +import { arrayWith, deepObjectLike, encodedJson, stringLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cb from '@aws-cdk/aws-codebuild'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cdkp from '../../lib'; +import { CodeBuildStep } from '../../lib'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, DockerAssetApp } from '../testhelpers'; + +const secretSynthArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:synth-012345'; +const secretUpdateArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:update-012345'; +const secretPublishArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:publish-012345'; + +let app: TestApp; +let pipelineStack: Stack; +let secretSynth: secretsmanager.ISecret; +let secretUpdate: secretsmanager.ISecret; +let secretPublish: secretsmanager.ISecret; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + secretSynth = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret1', secretSynthArn); + secretUpdate = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret2', secretUpdateArn); + secretPublish = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret3', secretPublishArn); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('synth action receives install commands and access to relevant credentials', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + const expectedCredsConfig = JSON.stringify({ + version: '1.0', + domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { Image: 'aws/codebuild/standard:5.0' }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + pre_build: { + commands: arrayWith( + 'mkdir $HOME/.cdk', + `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, + ), + }, + // Prove we're looking at the Synth project + build: { + commands: arrayWith(stringLike('*cdk*synth*')), + }, + }, + })), + }, + }); + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], + Effect: 'Allow', + Resource: secretSynthArn, + }), + Version: '2012-10-17', + }, + Roles: [{ Ref: stringLike('Cdk*BuildProjectRole*') }], + }); + } +}); + +behavior('synth action receives Windows install commands if a Windows image is detected', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyPipelineWithCreds(pipelineStack, 'Cdk2', { + npmSynthOptions: { + environment: { + buildImage: cb.WindowsBuildImage.WINDOWS_BASE_2_0, + }, + }, + }); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernPipelineWithCreds(pipelineStack, 'Cdk2', { + synth: new CodeBuildStep('Synth', { + commands: ['cdk synth'], + primaryOutputDirectory: 'cdk.out', + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + buildEnvironment: { + buildImage: cb.WindowsBuildImage.WINDOWS_BASE_2_0, + computeType: cb.ComputeType.MEDIUM, + }, + }), + }); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + const expectedCredsConfig = JSON.stringify({ + version: '1.0', + domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { Image: 'aws/codebuild/windows-base:2.0' }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + pre_build: { + commands: arrayWith( + 'mkdir %USERPROFILE%\\.cdk', + `echo '${expectedCredsConfig}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, + ), + }, + // Prove we're looking at the Synth project + build: { + commands: arrayWith(stringLike('*cdk*synth*')), + }, + }, + })), + }, + }); + } +}); + +behavior('self-update receives install commands and access to relevant credentials', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation('install'); + }); + + suite.modern(() => { + const pipeline = new ModernPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation('pre_build'); + }); + + function THEN_codePipelineExpectation(expectedPhase: string) { + const expectedCredsConfig = JSON.stringify({ + version: '1.0', + domainCredentials: { 'selfupdate.example.com': { secretsManagerSecretId: secretUpdateArn } }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { Image: 'aws/codebuild/standard:5.0' }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [expectedPhase]: { + commands: arrayWith( + 'mkdir $HOME/.cdk', + `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, + ), + }, + // Prove we're looking at the SelfMutate project + build: { + commands: arrayWith( + stringLike('cdk * deploy PipelineStack*'), + ), + }, + }, + })), + }, + }); + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], + Effect: 'Allow', + Resource: secretUpdateArn, + }), + Version: '2012-10-17', + }, + Roles: [{ Ref: stringLike('*SelfMutat*Role*') }], + }); + } +}); + +behavior('asset publishing receives install commands and access to relevant credentials', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation('install'); + }); + + suite.modern(() => { + const pipeline = new ModernPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation('pre_build'); + }); + + function THEN_codePipelineExpectation(expectedPhase: string) { + const expectedCredsConfig = JSON.stringify({ + version: '1.0', + domainCredentials: { 'publish.example.com': { secretsManagerSecretId: secretPublishArn } }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { Image: 'aws/codebuild/standard:5.0' }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [expectedPhase]: { + commands: arrayWith( + 'mkdir $HOME/.cdk', + `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, + ), + }, + // Prove we're looking at the Publishing project + build: { + commands: arrayWith(stringLike('cdk-assets*')), + }, + }, + })), + }, + }); + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], + Effect: 'Allow', + Resource: secretPublishArn, + }), + Version: '2012-10-17', + }, + Roles: [{ Ref: 'CdkAssetsDockerRole484B6DD3' }], + }); + } +}); + +class LegacyPipelineWithCreds extends LegacyTestGitHubNpmPipeline { + constructor(scope: Construct, id: string, props?: ConstructorParameters[2]) { + super(scope, id, { + dockerCredentials: [ + cdkp.DockerCredential.customRegistry('synth.example.com', secretSynth, { + usages: [cdkp.DockerCredentialUsage.SYNTH], + }), + cdkp.DockerCredential.customRegistry('selfupdate.example.com', secretUpdate, { + usages: [cdkp.DockerCredentialUsage.SELF_UPDATE], + }), + cdkp.DockerCredential.customRegistry('publish.example.com', secretPublish, { + usages: [cdkp.DockerCredentialUsage.ASSET_PUBLISHING], + }), + ], + ...props, + }); + } +} + +class ModernPipelineWithCreds extends ModernTestGitHubNpmPipeline { + constructor(scope: Construct, id: string, props?: ConstructorParameters[2]) { + super(scope, id, { + dockerCredentials: [ + cdkp.DockerCredential.customRegistry('synth.example.com', secretSynth, { + usages: [cdkp.DockerCredentialUsage.SYNTH], + }), + cdkp.DockerCredential.customRegistry('selfupdate.example.com', secretUpdate, { + usages: [cdkp.DockerCredentialUsage.SELF_UPDATE], + }), + cdkp.DockerCredential.customRegistry('publish.example.com', secretPublish, { + usages: [cdkp.DockerCredentialUsage.ASSET_PUBLISHING], + }), + ], + ...props, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts b/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts new file mode 100644 index 0000000000000..d30e5a423fcb3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts @@ -0,0 +1,391 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { arrayWith, objectLike, stringLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { Stack } from '@aws-cdk/core'; +import { behavior, LegacyTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('action has right settings for same-env deployment', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'Same')); + + THEN_codePipelineExpection(agnosticRole); + }); + + suite.additional('legacy: even if env is specified but the same as the pipeline', () => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'Same', { + env: PIPELINE_ENV, + })); + + THEN_codePipelineExpection(pipelineEnvRole); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new OneStackApp(app, 'Same')); + + THEN_codePipelineExpection(agnosticRole); + }); + + suite.additional('modern: even if env is specified but the same as the pipeline', () => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new OneStackApp(app, 'Same', { + env: PIPELINE_ENV, + })); + + THEN_codePipelineExpection(pipelineEnvRole); + }); + + function THEN_codePipelineExpection(roleArn: (x: string) => any) { + // THEN: pipeline structure is correct + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Same', + Actions: [ + objectLike({ + Name: stringLike('*Prepare'), + RoleArn: roleArn('deploy-role'), + Configuration: objectLike({ + StackName: 'Same-Stack', + RoleArn: roleArn('cfn-exec-role'), + }), + }), + objectLike({ + Name: stringLike('*Deploy'), + RoleArn: roleArn('deploy-role'), + Configuration: objectLike({ + StackName: 'Same-Stack', + }), + }), + ], + }), + }); + + // THEN: artifact bucket can be read by deploy role + expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Principal: { + AWS: roleArn('deploy-role'), + }, + })), + }, + }); + } +}); + +behavior('action has right settings for cross-account deployment', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'CrossAccount', { env: { account: 'you' } })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + pipeline.addStage(new OneStackApp(app, 'CrossAccount', { env: { account: 'you' } })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN: Pipelien structure is correct + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'CrossAccount', + Actions: [ + objectLike({ + Name: stringLike('*Prepare'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-deploy-role-you-', + { Ref: 'AWS::Region' }, + ]], + }, + Configuration: objectLike({ + StackName: 'CrossAccount-Stack', + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-cfn-exec-role-you-', + { Ref: 'AWS::Region' }, + ]], + }, + }), + }), + objectLike({ + Name: stringLike('*Deploy'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-deploy-role-you-', + { Ref: 'AWS::Region' }, + ]], + }, + Configuration: objectLike({ + StackName: 'CrossAccount-Stack', + }), + }), + ], + }), + }); + + // THEN: Artifact bucket can be read by deploy role + expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Principal: { + AWS: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + stringLike('*-deploy-role-*'), + { Ref: 'AWS::Region' }, + ]], + }, + }, + })), + }, + }); + } +}); + +behavior('action has right settings for cross-region deployment', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'CrossRegion', { env: { region: 'elsewhere' } })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + pipeline.addStage(new OneStackApp(app, 'CrossRegion', { env: { region: 'elsewhere' } })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'CrossRegion', + Actions: [ + objectLike({ + Name: stringLike('*Prepare'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::', + { Ref: 'AWS::AccountId' }, + ':role/cdk-hnb659fds-deploy-role-', + { Ref: 'AWS::AccountId' }, + '-elsewhere', + ]], + }, + Region: 'elsewhere', + Configuration: objectLike({ + StackName: 'CrossRegion-Stack', + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::', + { Ref: 'AWS::AccountId' }, + ':role/cdk-hnb659fds-cfn-exec-role-', + { Ref: 'AWS::AccountId' }, + '-elsewhere', + ]], + }, + }), + }), + objectLike({ + Name: stringLike('*Deploy'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::', + { Ref: 'AWS::AccountId' }, + ':role/cdk-hnb659fds-deploy-role-', + { Ref: 'AWS::AccountId' }, + '-elsewhere', + ]], + }, + Region: 'elsewhere', + Configuration: objectLike({ + StackName: 'CrossRegion-Stack', + }), + }), + ], + }), + }); + } +}); + +behavior('action has right settings for cross-account/cross-region deployment', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'CrossBoth', { + env: { + account: 'you', + region: 'elsewhere', + }, + })); + + THEN_codePipelineExpectations(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + pipeline.addStage(new OneStackApp(app, 'CrossBoth', { + env: { + account: 'you', + region: 'elsewhere', + }, + })); + + THEN_codePipelineExpectations(); + }); + + function THEN_codePipelineExpectations() { + // THEN: pipeline structure must be correct + expect(app.stackArtifact(pipelineStack)).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'CrossBoth', + Actions: [ + objectLike({ + Name: stringLike('*Prepare'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-deploy-role-you-elsewhere', + ]], + }, + Region: 'elsewhere', + Configuration: objectLike({ + StackName: 'CrossBoth-Stack', + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-cfn-exec-role-you-elsewhere', + ]], + }, + }), + }), + objectLike({ + Name: stringLike('*Deploy'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-deploy-role-you-elsewhere', + ]], + }, + Region: 'elsewhere', + Configuration: objectLike({ + StackName: 'CrossBoth-Stack', + }), + }), + ], + }), + }); + + // THEN: artifact bucket can be read by deploy role + const supportStack = 'PipelineStack-support-elsewhere'; + expect(app.stackArtifact(supportStack)).toHaveResourceLike('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: arrayWith('s3:GetObject*', 's3:GetBucket*', 's3:List*'), + Principal: { + AWS: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + stringLike('*-deploy-role-*'), + ]], + }, + }, + })), + }, + }); + + // And the key to go along with it + expect(app.stackArtifact(supportStack)).toHaveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: arrayWith(objectLike({ + Action: arrayWith('kms:Decrypt', 'kms:DescribeKey'), + Principal: { + AWS: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + stringLike('*-deploy-role-*'), + ]], + }, + }, + })), + }, + }); + } +}); + + +function agnosticRole(roleName: string) { + return { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::', + { Ref: 'AWS::AccountId' }, + `:role/cdk-hnb659fds-${roleName}-`, + { Ref: 'AWS::AccountId' }, + '-', + { Ref: 'AWS::Region' }, + ]], + }; +} + +function pipelineEnvRole(roleName: string) { + return { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + `:iam::${PIPELINE_ENV.account}:role/cdk-hnb659fds-${roleName}-${PIPELINE_ENV.account}-${PIPELINE_ENV.region}`, + ]], + }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts b/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts new file mode 100644 index 0000000000000..82754f52d5cba --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts @@ -0,0 +1,274 @@ +import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import * as cpa from '@aws-cdk/aws-codepipeline-actions'; +import { SecretValue, Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { CodePipelineFileSet } from '../../lib'; +import { behavior, FileAssetApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, PIPELINE_ENV, TestApp, TestGitHubAction } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; +let sourceArtifact: cp.Artifact; +let cloudAssemblyArtifact: cp.Artifact; +let codePipeline: cp.Pipeline; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + sourceArtifact = new cp.Artifact(); + cloudAssemblyArtifact = new cp.Artifact(); +}); + +afterEach(() => { + app.cleanup(); +}); + +describe('with empty existing CodePipeline', () => { + beforeEach(() => { + codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline'); + }); + + behavior('both actions are required', (suite) => { + suite.legacy(() => { + // WHEN + expect(() => { + new cdkp.CdkPipeline(pipelineStack, 'Cdk', { cloudAssemblyArtifact, codePipeline }); + }).toThrow(/You must pass a 'sourceAction'/); + }); + + // 'synth' is not optional so this doesn't apply + suite.doesNotApply.modern(); + }); + + behavior('can give both actions', (suite) => { + suite.legacy(() => { + // WHEN + new cdkp.CdkPipeline(pipelineStack, 'Cdk', { + cloudAssemblyArtifact, + codePipeline, + sourceAction: new TestGitHubAction(sourceArtifact), + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + new cdkp.CodePipeline(pipelineStack, 'Cdk', { + codePipeline, + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['true'], + }), + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'Source' }), + objectLike({ Name: 'Build' }), + objectLike({ Name: 'UpdatePipeline' }), + ], + }); + } + }); +}); + +describe('with custom Source stage in existing Pipeline', () => { + beforeEach(() => { + codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [new TestGitHubAction(sourceArtifact)], + }, + ], + }); + }); + + behavior('Work with synthAction', (suite) => { + suite.legacy(() => { + // WHEN + new cdkp.CdkPipeline(pipelineStack, 'Cdk', { + codePipeline, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new cdkp.CodePipeline(pipelineStack, 'Cdk', { + codePipeline, + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineFileSet.fromArtifact(sourceArtifact), + commands: ['true'], + }), + }); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'CustomSource' }), + objectLike({ Name: 'Build' }), + objectLike({ Name: 'UpdatePipeline' }), + ], + }); + } + }); +}); + +describe('with Source and Build stages in existing Pipeline', () => { + beforeEach(() => { + codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [new TestGitHubAction(sourceArtifact)], + }, + { + stageName: 'CustomBuild', + actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], + }, + ], + }); + }); + + behavior('can supply no actions', (suite) => { + suite.legacy(() => { + // WHEN + new cdkp.CdkPipeline(pipelineStack, 'Cdk', { + codePipeline, + cloudAssemblyArtifact, + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new cdkp.CodePipeline(pipelineStack, 'Cdk', { + codePipeline, + synth: cdkp.CodePipelineFileSet.fromArtifact(cloudAssemblyArtifact), + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'CustomSource' }), + objectLike({ Name: 'CustomBuild' }), + objectLike({ Name: 'UpdatePipeline' }), + ], + }); + } + }); +}); + +behavior('can add another action to an existing stage', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.stage('Source').addAction(new cpa.GitHubSourceAction({ + actionName: 'GitHub2', + oauthToken: SecretValue.plainText('oops'), + output: new cp.Artifact(), + owner: 'OWNER', + repo: 'REPO', + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.buildPipeline(); + + pipeline.pipeline.stages[0].addAction(new cpa.GitHubSourceAction({ + actionName: 'GitHub2', + oauthToken: SecretValue.plainText('oops'), + output: new cp.Artifact(), + owner: 'OWNER', + repo: 'REPO', + })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Source', + Actions: [ + objectLike({ ActionTypeId: objectLike({ Provider: 'GitHub' }) }), + objectLike({ ActionTypeId: objectLike({ Provider: 'GitHub' }), Name: 'GitHub2' }), + ], + }), + }); + } +}); + + +behavior('assets stage inserted after existing pipeline actions', (suite) => { + let existingCodePipeline: cp.Pipeline; + beforeEach(() => { + existingCodePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [new TestGitHubAction(sourceArtifact)], + }, + { + stageName: 'CustomBuild', + actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], + }, + ], + }); + }); + + suite.legacy(() => { + const pipeline = new cdkp.CdkPipeline(pipelineStack, 'CdkEmptyPipeline', { + cloudAssemblyArtifact: cloudAssemblyArtifact, + selfMutating: false, + codePipeline: existingCodePipeline, + // No source/build actions + }); + pipeline.addApplicationStage(new FileAssetApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new cdkp.CodePipeline(pipelineStack, 'CdkEmptyPipeline', { + codePipeline: existingCodePipeline, + selfMutation: false, + synth: CodePipelineFileSet.fromArtifact(cloudAssemblyArtifact), + // No source/build actions + }); + pipeline.addStage(new FileAssetApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'CustomSource' }), + objectLike({ Name: 'CustomBuild' }), + objectLike({ Name: 'Assets' }), + objectLike({ Name: 'App' }), + ], + }); + } +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts new file mode 100644 index 0000000000000..8aa1ed8293c30 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts @@ -0,0 +1,241 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { anything, arrayWith, deepObjectLike, encodedJson, notMatching, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import { Stack, Stage } from '@aws-cdk/core'; +import { behavior, LegacyTestGitHubNpmPipeline, PIPELINE_ENV, stackTemplate, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('CodePipeline has self-mutation stage', (suite) => { + suite.legacy(() => { + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'UpdatePipeline', + Actions: [ + objectLike({ + Name: 'SelfMutate', + Configuration: objectLike({ + ProjectName: { Ref: anything() }, + }), + }), + ], + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + install: { + commands: ['npm install -g aws-cdk'], + }, + build: { + commands: arrayWith('cdk -a . deploy PipelineStack --require-approval=never --verbose'), + }, + }, + })), + Type: 'CODEPIPELINE', + }, + }); + } +}); + +behavior('selfmutation stage correctly identifies nested assembly of pipeline stack', (suite) => { + suite.legacy(() => { + const pipelineStage = new Stage(app, 'PipelineStage'); + const nestedPipelineStack = new Stack(pipelineStage, 'PipelineStack', { env: PIPELINE_ENV }); + new LegacyTestGitHubNpmPipeline(nestedPipelineStack, 'Cdk'); + + THEN_codePipelineExpectation(nestedPipelineStack); + }); + + suite.modern(() => { + const pipelineStage = new Stage(app, 'PipelineStage'); + const nestedPipelineStack = new Stack(pipelineStage, 'PipelineStack', { env: PIPELINE_ENV }); + new ModernTestGitHubNpmPipeline(nestedPipelineStack, 'Cdk'); + + THEN_codePipelineExpectation(nestedPipelineStack); + }); + + function THEN_codePipelineExpectation(nestedPipelineStack: Stack) { + expect(stackTemplate(nestedPipelineStack)).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: arrayWith('cdk -a assembly-PipelineStage deploy PipelineStage/PipelineStack --require-approval=never --verbose'), + }, + }, + })), + }, + }); + } +}); + +behavior('selfmutation feature can be turned off', (suite) => { + suite.legacy(() => { + const cloudAssemblyArtifact = new cp.Artifact(); + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + cloudAssemblyArtifact, + selfMutating: false, + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + selfMutation: false, + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: notMatching(arrayWith({ + Name: 'UpdatePipeline', + Actions: anything(), + })), + }); + } +}); + +behavior('can control fix/CLI version used in pipeline selfupdate', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + pipelineName: 'vpipe', + cdkCliVersion: '1.2.3', + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + pipelineName: 'vpipe', + cliVersion: '1.2.3', + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Name: 'vpipe-selfupdate', + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + install: { + commands: ['npm install -g aws-cdk@1.2.3'], + }, + }, + })), + }, + }); + } +}); + +behavior('Pipeline stack itself can use assets (has implications for selfupdate)', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'PrivilegedPipeline', { + supportDockerAssets: true, + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + PrivilegedMode: true, + }, + }); + }); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'PrivilegedPipeline', { + dockerEnabledForSelfMutation: true, + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + PrivilegedMode: true, + }, + }); + }); +}); + +behavior('self-update project role uses tagged bootstrap-role permissions', (suite) => { + suite.legacy(() => { + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + THEN_codePipelineExpectations(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + THEN_codePipelineExpectations(); + }); + + function THEN_codePipelineExpectations() { + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith( + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: 'arn:*:iam::123pipeline:role/*', + Condition: { + 'ForAnyValue:StringEquals': { + 'iam:ResourceTag/aws-cdk:bootstrap-role': ['image-publishing', 'file-publishing', 'deploy'], + }, + }, + }, + { + Action: 'cloudformation:DescribeStacks', + Effect: 'Allow', + Resource: '*', + }, + { + Action: 's3:ListBucket', + Effect: 'Allow', + Resource: '*', + }, + ), + }, + }); + } +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/stack-ordering.test.ts b/packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts similarity index 67% rename from packages/@aws-cdk/pipelines/test/stack-ordering.test.ts rename to packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts index 50d1b892cc5cb..cb21139b16364 100644 --- a/packages/@aws-cdk/pipelines/test/stack-ordering.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts @@ -1,27 +1,32 @@ import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; -import { App, Stack, Stage, StageProps } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { sortedByRunOrder } from './testmatchers'; -import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; +import { App, Stack } from '@aws-cdk/core'; +import { behavior, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, sortedByRunOrder, TestApp, ThreeStackApp, TwoStackApp } from '../testhelpers'; let app: App; let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; beforeEach(() => { app = new TestApp(); pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); }); behavior('interdependent stacks are in the right order', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new TwoStackApp(app, 'MyApp')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoStackApp(app, 'MyApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // THEN expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: arrayWith({ @@ -34,15 +39,26 @@ behavior('interdependent stacks are in the right order', (suite) => { ]), }), }); - }); + } }); behavior('multiple independent stacks go in parallel', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new ThreeStackApp(app, 'MyApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new ThreeStackApp(app, 'MyApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: arrayWith({ Name: 'MyApp', @@ -58,12 +74,13 @@ behavior('multiple independent stacks go in parallel', (suite) => { ]), }), }); - }); + } }); -behavior('manual approval is inserted in correct location', (suite) => { +behavior('user can request manual change set approvals', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new TwoStackApp(app, 'MyApp'), { manualApprovals: true, }); @@ -83,11 +100,15 @@ behavior('manual approval is inserted in correct location', (suite) => { }), }); }); + + // No change set approvals in Modern API for now. + suite.doesNotApply.modern(); }); -behavior('extra space for sequential intermediary actions is reserved', (suite) => { +behavior('user can request extra runorder space between prepare and deploy', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new TwoStackApp(app, 'MyApp'), { extraRunOrderSpace: 1, }); @@ -117,11 +138,15 @@ behavior('extra space for sequential intermediary actions is reserved', (suite) }), }); }); + + // No change set approvals in Modern API for now. + suite.doesNotApply.modern(); }); -behavior('combination of manual approval and extraRunOrderSpace', (suite) => { +behavior('user can request both manual change set approval and extraRunOrderSpace', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new OneStackApp(app, 'MyApp'), { extraRunOrderSpace: 1, manualApprovals: true, @@ -133,7 +158,7 @@ behavior('combination of manual approval and extraRunOrderSpace', (suite) => { Name: 'MyApp', Actions: sortedByRunOrder([ objectLike({ - Name: 'Stack1.Prepare', + Name: 'Stack.Prepare', RunOrder: 1, }), objectLike({ @@ -141,46 +166,14 @@ behavior('combination of manual approval and extraRunOrderSpace', (suite) => { RunOrder: 2, }), objectLike({ - Name: 'Stack1.Deploy', + Name: 'Stack.Deploy', RunOrder: 4, }), ]), }), }); }); -}); - -class OneStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - - new BucketStack(this, 'Stack1'); - } -} -class TwoStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - - const stack2 = new BucketStack(this, 'Stack2'); - const stack1 = new BucketStack(this, 'Stack1'); - - stack2.addDependency(stack1); - } -} - -/** - * Three stacks where the last one depends on the earlier 2 - */ -class ThreeStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - - const stack1 = new BucketStack(this, 'Stack1'); - const stack2 = new BucketStack(this, 'Stack2'); - const stack3 = new BucketStack(this, 'Stack3'); - - stack3.addDependency(stack1); - stack3.addDependency(stack2); - } -} + // No change set approvals in Modern API for now. + suite.doesNotApply.modern(); +}); diff --git a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts new file mode 100644 index 0000000000000..92d1c9164fcba --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts @@ -0,0 +1,981 @@ +import { arrayWith, deepObjectLike, encodedJson, objectLike, Capture, anything } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cbuild from '@aws-cdk/aws-codebuild'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecr from '@aws-cdk/aws-ecr'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { CodeBuildStep } from '../../lib'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, ModernTestGitHubNpmPipelineProps } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; +let sourceArtifact: codepipeline.Artifact; +let cloudAssemblyArtifact: codepipeline.Artifact; + +// Must be unique across all test files, but preferably also consistent +const OUTDIR = 'testcdk0.out'; + +// What phase install commands get rendered to +const LEGACY_INSTALLS = 'pre_build'; +const MODERN_INSTALLS = 'install'; + +beforeEach(() => { + app = new TestApp({ outdir: OUTDIR }); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + sourceArtifact = new codepipeline.Artifact(); + cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('synth takes arrays of commands', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: new cdkp.SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + installCommands: ['install1', 'install2'], + buildCommands: ['build1', 'build2'], + testCommands: ['test1', 'test2'], + synthCommand: 'cdk synth', + }), + }); + + THEN_codePipelineExpectation(LEGACY_INSTALLS); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + installCommands: ['install1', 'install2'], + commands: ['build1', 'build2', 'test1', 'test2', 'cdk synth'], + }); + + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + function THEN_codePipelineExpectation(installPhase: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [installPhase]: { + commands: [ + 'install1', + 'install2', + ], + }, + build: { + commands: [ + 'build1', + 'build2', + 'test1', + 'test2', + 'cdk synth', + ], + }, + }, + })), + }, + }); + } +}); + +behavior('synth sets artifact base-directory to cdk.out', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), + }); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + artifacts: { + 'base-directory': 'cdk.out', + }, + })), + }, + }); + } +}); + +behavior('synth supports setting subdirectory', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + subdirectory: 'subdir', + }), + }); + + THEN_codePipelineExpectation(LEGACY_INSTALLS); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + installCommands: ['cd subdir'], + commands: ['true'], + primaryOutputDirectory: 'subdir/cdk.out', + }); + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + function THEN_codePipelineExpectation(installPhase: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [installPhase]: { + commands: arrayWith('cd subdir'), + }, + }, + artifacts: { + 'base-directory': 'subdir/cdk.out', + }, + })), + }, + }); + } +}); + +behavior('npm synth sets, or allows setting, UNSAFE_PERM=true', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + env: { + NPM_CONFIG_UNSAFE_PERM: 'true', + }, + }); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + EnvironmentVariables: [ + { + Name: 'NPM_CONFIG_UNSAFE_PERM', + Type: 'PLAINTEXT', + Value: 'true', + }, + ], + }, + }); + } +}); + +behavior('synth assumes a JavaScript project by default (no build, yes synth)', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + pre_build: { + commands: ['npm ci'], + }, + build: { + commands: ['npx cdk synth'], + }, + }, + })), + }, + }); + }); + + // Modern pipeline does not assume anything anymore + suite.doesNotApply.modern(); +}); + +behavior('Magic CodePipeline variables passed to synth envvars must be rendered in the action', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: new cdkp.SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + environmentVariables: { + VERSION: { value: codepipeline.GlobalVariables.executionId }, + }, + synthCommand: 'synth', + }), + }); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + env: { + VERSION: codepipeline.GlobalVariables.executionId, + }, + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Build', + Actions: [ + objectLike({ + Name: 'Synth', + Configuration: objectLike({ + EnvironmentVariables: encodedJson(arrayWith( + { + name: 'VERSION', + type: 'PLAINTEXT', + value: '#{codepipeline.PipelineExecutionId}', + }, + )), + }), + }), + ], + }), + }); + } +}); + +behavior('CodeBuild: environment variables specified in multiple places are correctly merged', (suite) => { + // We don't support merging environment variables in this way in the legacy API + suite.doesNotApply.legacy(); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new CodeBuildStep('Synth', { + env: { + SOME_ENV_VAR: 'SomeValue', + }, + installCommands: [ + 'install1', + 'install2', + ], + commands: ['synth'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: 'cdk.out', + buildEnvironment: { + environmentVariables: { + INNER_VAR: { value: 'InnerValue' }, + }, + privileged: true, + }, + }), + }); + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + suite.additional('modern2, using the specific CodeBuild action', () => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.CodeBuildStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: '.', + env: { + SOME_ENV_VAR: 'SomeValue', + }, + installCommands: [ + 'install1', + 'install2', + ], + commands: ['synth'], + buildEnvironment: { + environmentVariables: { + INNER_VAR: { value: 'InnerValue' }, + }, + privileged: true, + }, + }), + }); + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + function THEN_codePipelineExpectation(installPhase: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: objectLike({ + PrivilegedMode: true, + EnvironmentVariables: arrayWith( + { + Name: 'SOME_ENV_VAR', + Type: 'PLAINTEXT', + Value: 'SomeValue', + }, + { + Name: 'INNER_VAR', + Type: 'PLAINTEXT', + Value: 'InnerValue', + }, + ), + }), + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [installPhase]: { + commands: ['install1', 'install2'], + }, + build: { + commands: ['synth'], + }, + }, + })), + }, + }); + } +}); + +behavior('install command can be overridden/specified', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + installCommand: '/bin/true', + }), + }); + + THEN_codePipelineExpectation(LEGACY_INSTALLS); + }); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + installCommands: ['/bin/true'], + }); + + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + function THEN_codePipelineExpectation(installPhase: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [installPhase]: { + commands: ['/bin/true'], + }, + }, + })), + }, + }); + } +}); + +behavior('synth can have its test commands set', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + installCommand: '/bin/true', + testCommands: ['echo "Running tests"'], + }), + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(objectLike({ + phases: { + pre_build: { + commands: ['/bin/true'], + }, + build: { + commands: ['echo "Running tests"', 'npx cdk synth'], + }, + }, + })), + }, + }); + }); + + // There are no implicit commands in modern synth + suite.doesNotApply.modern(); +}); + +behavior('Synth can output additional artifacts', (suite) => { + suite.legacy(() => { + // WHEN + const addlArtifact = new codepipeline.Artifact('IntegTest'); + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + additionalArtifacts: [ + { + artifact: addlArtifact, + directory: 'test', + }, + ], + }), + }); + + THEN_codePipelineExpectation('CloudAsm', 'IntegTest'); + }); + + suite.modern(() => { + // WHEN + const synth = new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['cdk synth'], + }); + synth.addOutputDirectory('test'); + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: synth, + }); + + THEN_codePipelineExpectation('Synth_Output', 'Synth_test'); + }); + + function THEN_codePipelineExpectation(asmArtifact: string, testArtifact: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + artifacts: { + 'secondary-artifacts': { + [asmArtifact]: { + 'base-directory': 'cdk.out', + 'files': '**/*', + }, + [testArtifact]: { + 'base-directory': 'test', + 'files': '**/*', + }, + }, + }, + })), + }, + }); + } +}); + +behavior('Synth can be made to run in a VPC', (suite) => { + let vpc: ec2.Vpc; + beforeEach(() => { + vpc = new ec2.Vpc(pipelineStack, 'NpmSynthTestVpc'); + }); + + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + vpc, + sourceArtifact, + cloudAssemblyArtifact, + }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + }); + + suite.additional('Modern, using CodeBuildStep', () => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new CodeBuildStep('Synth', { + commands: ['asdf'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: 'cdk.out', + buildEnvironment: { + computeType: cbuild.ComputeType.LARGE, + }, + }), + codeBuildDefaults: { vpc }, + }); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + VpcConfig: { + SecurityGroupIds: [ + { 'Fn::GetAtt': ['CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', 'GroupId'] }, + ], + Subnets: [ + { Ref: 'NpmSynthTestVpcPrivateSubnet1Subnet81E3AA56' }, + { Ref: 'NpmSynthTestVpcPrivateSubnet2SubnetC1CA3EF0' }, + { Ref: 'NpmSynthTestVpcPrivateSubnet3SubnetA04163EE' }, + ], + VpcId: { Ref: 'NpmSynthTestVpc5E703F25' }, + }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Roles: [ + { Ref: 'CdkPipelineBuildSynthCdkBuildProjectRole5E173C62' }, + ], + PolicyDocument: { + Statement: arrayWith({ + Action: arrayWith('ec2:DescribeSecurityGroups'), + Effect: 'Allow', + Resource: '*', + }), + }, + }); + } +}); + +behavior('Pipeline action contains a hash that changes as the buildspec changes', (suite) => { + suite.legacy(() => { + const hash1 = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + })); + + // To make sure the hash is not just random :) + const hash1prime = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + })); + + const hash2 = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + installCommand: 'do install', + })); + const hash3 = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + environment: { + computeType: cbuild.ComputeType.LARGE, + }, + })); + const hash4 = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + environment: { + environmentVariables: { + xyz: { value: 'SOME-VALUE' }, + }, + }, + })); + + expect(hash1).toEqual(hash1prime); + + expect(hash1).not.toEqual(hash2); + expect(hash1).not.toEqual(hash3); + expect(hash1).not.toEqual(hash4); + expect(hash2).not.toEqual(hash3); + expect(hash2).not.toEqual(hash4); + expect(hash3).not.toEqual(hash4); + }); + + suite.modern(() => { + const hash1 = modernSynthWithAction(() => ({ commands: ['asdf'] })); + + // To make sure the hash is not just random :) + const hash1prime = modernSynthWithAction(() => ({ commands: ['asdf'] })); + + const hash2 = modernSynthWithAction(() => ({ + installCommands: ['do install'], + })); + const hash3 = modernSynthWithAction(() => ({ + synth: new CodeBuildStep('Synth', { + commands: ['asdf'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: 'cdk.out', + buildEnvironment: { + computeType: cbuild.ComputeType.LARGE, + }, + }), + })); + + const hash4 = modernSynthWithAction(() => ({ + env: { + xyz: 'SOME-VALUE', + }, + })); + + expect(hash1).toEqual(hash1prime); + + expect(hash1).not.toEqual(hash2); + expect(hash1).not.toEqual(hash3); + expect(hash1).not.toEqual(hash4); + expect(hash2).not.toEqual(hash3); + expect(hash2).not.toEqual(hash4); + expect(hash3).not.toEqual(hash4); + }); + + // eslint-disable-next-line max-len + function legacySynthWithAction(cb: (sourceArtifact: codepipeline.Artifact, cloudAssemblyArtifact: codepipeline.Artifact) => codepipeline.IAction) { + const _app = new TestApp({ outdir: OUTDIR }); + const _pipelineStack = new Stack(_app, 'PipelineStack', { env: PIPELINE_ENV }); + const _sourceArtifact = new codepipeline.Artifact(); + const _cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); + + new LegacyTestGitHubNpmPipeline(_pipelineStack, 'Cdk', { + sourceArtifact: _sourceArtifact, + cloudAssemblyArtifact: _cloudAssemblyArtifact, + synthAction: cb(_sourceArtifact, _cloudAssemblyArtifact), + }); + + return captureProjectConfigHash(_pipelineStack); + } + + function modernSynthWithAction(cb: () => ModernTestGitHubNpmPipelineProps) { + const _app = new TestApp({ outdir: OUTDIR }); + const _pipelineStack = new Stack(_app, 'PipelineStack', { env: PIPELINE_ENV }); + + new ModernTestGitHubNpmPipeline(_pipelineStack, 'Cdk', cb()); + + return captureProjectConfigHash(_pipelineStack); + } + + function captureProjectConfigHash(_pipelineStack: Stack) { + const theHash = Capture.aString(); + expect(_pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Build', + Actions: [ + objectLike({ + Name: 'Synth', + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { + name: '_PROJECT_CONFIG_HASH', + type: 'PLAINTEXT', + value: theHash.capture(), + }, + ]), + }), + }), + ], + }), + }); + + return theHash.capturedValue; + } +}); + +behavior('Synth CodeBuild project role can be granted permissions', (suite) => { + let bucket: s3.IBucket; + beforeEach(() => { + bucket = s3.Bucket.fromBucketArn(pipelineStack, 'Bucket', 'arn:aws:s3:::ThisParticularBucket'); + }); + + + suite.legacy(() => { + // GIVEN + const synthAction = cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + }); + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction, + }); + + // WHEN + bucket.grantRead(synthAction); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // GIVEN + const pipe = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipe.buildPipeline(); + + // WHEN + bucket.grantRead(pipe.synthProject); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(deepObjectLike({ + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Resource: ['arn:aws:s3:::ThisParticularBucket', 'arn:aws:s3:::ThisParticularBucket/*'], + })), + }, + }); + } +}); + +behavior('Synth can reference an imported ECR repo', (suite) => { + // Repro from https://github.com/aws/aws-cdk/issues/10535 + + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + environment: { + buildImage: cbuild.LinuxBuildImage.fromEcrRepository( + ecr.Repository.fromRepositoryName(pipelineStack, 'ECRImage', 'my-repo-name'), + ), + }, + }), + }); + + // THEN -- no exception (necessary for linter) + expect(true).toBeTruthy(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.CodeBuildStep('Synth', { + commands: ['build'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: 'cdk.out', + buildEnvironment: { + buildImage: cbuild.LinuxBuildImage.fromEcrRepository( + ecr.Repository.fromRepositoryName(pipelineStack, 'ECRImage', 'my-repo-name'), + ), + }, + }), + }); + + // THEN -- no exception (necessary for linter) + expect(true).toBeTruthy(); + }); +}); + +behavior('CodeBuild: Can specify additional policy statements', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], + resources: ['arn:my:arn'], + }), + ], + }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.CodeBuildStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: '.', + commands: ['synth'], + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], + resources: ['arn:my:arn'], + }), + ], + }), + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(deepObjectLike({ + Action: [ + 'codeartifact:*', + 'sts:GetServiceBearerToken', + ], + Resource: 'arn:my:arn', + })), + }, + }); + } +}); + +behavior('Multiple input sources in side-by-side directories', (suite) => { + // Legacy API does not support this + suite.doesNotApply.legacy(); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['false'], + additionalInputs: { + '../sibling': cdkp.CodePipelineSource.gitHub('foo/bar', 'main'), + 'sub': new cdkp.ShellStep('Prebuild', { + input: cdkp.CodePipelineSource.gitHub('pre/build', 'main'), + commands: ['true'], + primaryOutputDirectory: 'built', + }), + }, + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith( + { + Name: 'Source', + Actions: [ + objectLike({ Configuration: objectLike({ Repo: 'bar' }) }), + objectLike({ Configuration: objectLike({ Repo: 'build' }) }), + objectLike({ Configuration: objectLike({ Repo: 'test' }) }), + ], + }, + { + Name: 'Build', + Actions: [ + objectLike({ Name: 'Prebuild', RunOrder: 1 }), + objectLike({ + Name: 'Synth', + RunOrder: 2, + InputArtifacts: [ + // 3 input artifacts + anything(), + anything(), + anything(), + ], + }), + ], + }, + ), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + install: { + commands: [ + 'ln -s "$CODEBUILD_SRC_DIR_foo_bar_Source" "../sibling"', + 'ln -s "$CODEBUILD_SRC_DIR_Prebuild_Output" "sub"', + ], + }, + build: { + commands: [ + 'false', + ], + }, + }, + })), + }, + }); + }); +}); + +behavior('Can easily switch on privileged mode for synth', (suite) => { + // Legacy API does not support this + suite.doesNotApply.legacy(); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + dockerEnabledForSynth: true, + commands: ['LookAtMe'], + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: objectLike({ + PrivilegedMode: true, + }), + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'LookAtMe', + ], + }, + }, + })), + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts b/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts new file mode 100644 index 0000000000000..447e22da59124 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts @@ -0,0 +1,799 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { anything, arrayWith, Capture, deepObjectLike, encodedJson, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { CodePipelineSource, ShellStep } from '../../lib'; +import { AppWithOutput, behavior, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, sortedByRunOrder, StageWithStackOutput, stringNoLongerThan, TestApp, TwoStackApp } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('can add manual approval after app', (suite) => { + // No need to be backwards compatible + suite.doesNotApply.legacy(); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoStackApp(app, 'MyApp'), { + post: [ + new cdkp.ManualApprovalStep('Approve'), + ], + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'MyApp', + Actions: sortedByRunOrder([ + objectLike({ Name: 'Stack1.Prepare' }), + objectLike({ Name: 'Stack1.Deploy' }), + objectLike({ Name: 'Stack2.Prepare' }), + objectLike({ Name: 'Stack2.Deploy' }), + objectLike({ Name: 'Approve' }), + ]), + }), + }); + }); +}); + +behavior('can add steps to wave', (suite) => { + // No need to be backwards compatible + suite.doesNotApply.legacy(); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const wave = pipeline.addWave('MyWave', { + post: [ + new cdkp.ManualApprovalStep('Approve'), + ], + }); + wave.addStage(new OneStackApp(pipelineStack, 'Stage1')); + wave.addStage(new OneStackApp(pipelineStack, 'Stage2')); + wave.addStage(new OneStackApp(pipelineStack, 'Stage3')); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'MyWave', + Actions: sortedByRunOrder([ + objectLike({ Name: 'Stage1.Stack.Prepare' }), + objectLike({ Name: 'Stage2.Stack.Prepare' }), + objectLike({ Name: 'Stage3.Stack.Prepare' }), + objectLike({ Name: 'Stage1.Stack.Deploy' }), + objectLike({ Name: 'Stage2.Stack.Deploy' }), + objectLike({ Name: 'Stage3.Stack.Deploy' }), + objectLike({ Name: 'Approve' }), + ]), + }), + }); + }); +}); + + +behavior('script validation steps can use stack outputs as environment variables', (suite) => { + suite.legacy(() => { + // GIVEN + const { pipeline } = legacySetup(); + const stage = new StageWithStackOutput(app, 'MyApp'); + + // WHEN + const pipeStage = pipeline.addApplicationStage(stage); + pipeStage.addActions(new cdkp.ShellScriptAction({ + actionName: 'TestOutput', + useOutputs: { + BUCKET_NAME: pipeline.stackOutput(stage.output), + }, + commands: ['echo $BUCKET_NAME'], + })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'MyApp', + Actions: arrayWith( + deepObjectLike({ + Name: 'Stack.Deploy', + OutputArtifacts: [{ Name: anything() }], + Configuration: { + OutputFileName: 'outputs.json', + }, + }), + deepObjectLike({ + ActionTypeId: { + Provider: 'CodeBuild', + }, + Configuration: { + ProjectName: anything(), + }, + InputArtifacts: [{ Name: anything() }], + Name: 'TestOutput', + }), + ), + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'set -eu', + 'export BUCKET_NAME="$(node -pe \'require(process.env.CODEBUILD_SRC_DIR + "/outputs.json")["BucketName"]\')"', + 'echo $BUCKET_NAME', + ], + }, + }, + })), + Type: 'CODEPIPELINE', + }, + }); + }); + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const myApp = new AppWithOutput(app, 'Alpha'); + pipeline.addStage(myApp, { + post: [ + new cdkp.ShellStep('Approve', { + commands: ['/bin/true'], + envFromCfnOutputs: { + THE_OUTPUT: myApp.theOutput, + }, + }), + ], + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Alpha', + Actions: arrayWith( + objectLike({ + Name: 'Stack.Deploy', + Namespace: 'AlphaStack6B3389FA', + }), + objectLike({ + Name: 'Approve', + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { name: 'THE_OUTPUT', value: '#{AlphaStack6B3389FA.MyOutput}', type: 'PLAINTEXT' }, + ]), + }), + }), + ), + }), + }); + }); +}); + +behavior('stackOutput generates names limited to 100 characters', (suite) => { + suite.legacy(() => { + const { pipeline } = legacySetup(); + const stage = new StageWithStackOutput(app, 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild'); + const pipeStage = pipeline.addApplicationStage(stage); + pipeStage.addActions(new cdkp.ShellScriptAction({ + actionName: 'TestOutput', + useOutputs: { + BUCKET_NAME: pipeline.stackOutput(stage.output), + }, + commands: ['echo $BUCKET_NAME'], + })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', + Actions: arrayWith( + deepObjectLike({ + Name: 'Stack.Deploy', + OutputArtifacts: [{ Name: stringNoLongerThan(100) }], + Configuration: { + OutputFileName: 'outputs.json', + }, + }), + deepObjectLike({ + ActionTypeId: { + Provider: 'CodeBuild', + }, + Configuration: { + ProjectName: anything(), + }, + InputArtifacts: [{ Name: stringNoLongerThan(100) }], + Name: 'TestOutput', + }), + ), + }), + }); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const stage = new StageWithStackOutput(app, 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild'); + pipeline.addStage(stage, { + post: [ + new cdkp.ShellStep('TestOutput', { + commands: ['echo $BUCKET_NAME'], + envFromCfnOutputs: { + BUCKET_NAME: stage.output, + }, + }), + ], + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', + Actions: arrayWith( + deepObjectLike({ + Name: 'Stack.Deploy', + Namespace: stringNoLongerThan(100), + }), + ), + }), + }); + }); +}); + +behavior('validation step can run from scripts in source', (suite) => { + suite.legacy(() => { + const { pipeline, sourceArtifact } = legacySetup(); + + // WHEN + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'UseSources', + additionalArtifacts: [sourceArtifact], + commands: ['true'], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [ + new cdkp.ShellStep('UseSources', { + input: pipeline.gitHubSource, + commands: ['set -eu', 'true'], + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + const sourceArtifact = Capture.aString(); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Source', + Actions: [ + deepObjectLike({ + OutputArtifacts: [{ Name: sourceArtifact.capture() }], + }), + ], + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: arrayWith( + deepObjectLike({ + Name: 'UseSources', + InputArtifacts: [{ Name: sourceArtifact.capturedValue }], + }), + ), + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'set -eu', + 'true', + ], + }, + }, + })), + }, + }); + } +}); + +behavior('can use additional output artifacts from build', (suite) => { + suite.legacy(() => { + // WHEN + const { pipeline, integTestArtifact } = legacySetup(); + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'UseBuildArtifact', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const synth = new ShellStep('Synth', { + input: CodePipelineSource.gitHub('test/test', 'main'), + commands: ['synth'], + }); + + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth, + }); + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [ + new cdkp.ShellStep('UseBuildArtifact', { + input: synth.addOutputDirectory('test'), + commands: ['set -eu', 'true'], + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + const integArtifact = Capture.aString(); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Build', + Actions: [ + deepObjectLike({ + Name: 'Synth', + OutputArtifacts: [ + { Name: anything() }, // It's not the first output + { Name: integArtifact.capture() }, + ], + }), + ], + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: arrayWith( + deepObjectLike({ + Name: 'UseBuildArtifact', + InputArtifacts: [{ Name: integArtifact.capturedValue }], + }), + ), + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'set -eu', + 'true', + ], + }, + }, + })), + }, + }); + } +}); + +behavior('can add policy statements to shell script action', (suite) => { + suite.legacy(() => { + // WHEN + const { pipeline, integTestArtifact } = legacySetup(); + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'Boop', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['s3:Banana'], + resources: ['*'], + }), + ], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [ + new cdkp.CodeBuildStep('Boop', { + commands: ['true'], + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['s3:Banana'], + resources: ['*'], + }), + ], + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(deepObjectLike({ + Action: 's3:Banana', + Resource: '*', + })), + }, + }); + } +}); + +behavior('can grant permissions to shell script action', (suite) => { + let bucket: s3.IBucket; + beforeEach(() => { + bucket = s3.Bucket.fromBucketArn(pipelineStack, 'Bucket', 'arn:aws:s3:::ThisParticularBucket'); + }); + + suite.legacy(() => { + const { pipeline, integTestArtifact } = legacySetup(); + const action = new cdkp.ShellScriptAction({ + actionName: 'Boop', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + }); + pipeline.addStage('Test').addActions(action); + + // WHEN + bucket.grantRead(action); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + const codeBuildStep = new cdkp.CodeBuildStep('Boop', { + commands: ['true'], + }); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [codeBuildStep], + }); + + pipeline.buildPipeline(); + + // WHEN + bucket.grantRead(codeBuildStep.project); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(deepObjectLike({ + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Resource: ['arn:aws:s3:::ThisParticularBucket', 'arn:aws:s3:::ThisParticularBucket/*'], + })), + }, + }); + } +}); + +behavior('can run shell script actions in a VPC', (suite) => { + let vpc: ec2.Vpc; + beforeEach(() => { + vpc = new ec2.Vpc(pipelineStack, 'VPC'); + }); + + suite.legacy(() => { + const { pipeline, integTestArtifact } = legacySetup(); + + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + vpc, + actionName: 'VpcAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // All CodeBuild jobs automatically go into the VPC + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + + pipeline.addStage(new TwoStackApp(app, 'MyApp'), { + post: [new cdkp.ShellStep('VpcAction', { + commands: ['set -eu', 'true'], + })], + }); + + THEN_codePipelineExpectation(); + }); + + suite.additional('modern, alternate API', () => { + // Can also explicitly specify a VPC when going to the "full config" class + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + pipeline.addStage(new TwoStackApp(app, 'MyApp'), { + post: [new cdkp.CodeBuildStep('VpcAction', { + commands: ['set -eu', 'true'], + vpc, + })], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + VpcConfig: { + Subnets: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + { + Ref: 'VPCPrivateSubnet3Subnet3EDCD457', + }, + ], + VpcId: { + Ref: 'VPCB9E5F0B4', + }, + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'set -eu', + 'true', + ], + }, + }, + })), + }, + }); + } +}); + +behavior('can run shell script actions with a specific SecurityGroup', (suite) => { + let vpc: ec2.Vpc; + let sg: ec2.SecurityGroup; + beforeEach(() => { + vpc = new ec2.Vpc(pipelineStack, 'VPC'); + sg = new ec2.SecurityGroup(pipelineStack, 'SG', { vpc }); + }); + + suite.legacy(() => { + // WHEN + const { pipeline, integTestArtifact } = legacySetup(); + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + vpc, + securityGroups: [sg], + actionName: 'sgAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // All CodeBuild jobs automatically go into the VPC + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [new cdkp.CodeBuildStep('sgAction', { + commands: ['set -eu', 'true'], + vpc, + securityGroups: [sg], + })], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: arrayWith( + deepObjectLike({ + Name: 'sgAction', + }), + ), + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + VpcConfig: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'SGADB53937', + 'GroupId', + ], + }, + ], + VpcId: { + Ref: 'VPCB9E5F0B4', + }, + }, + }); + } +}); + +behavior('can run scripts with specified BuildEnvironment', (suite) => { + suite.legacy(() => { + let { pipeline, integTestArtifact } = legacySetup(); + + // WHEN + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'imageAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_2_0 }, + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // Run all Build jobs with the given image + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { + buildEnvironment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_2_0, + }, + }, + }); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [new cdkp.ShellStep('imageAction', { + commands: ['true'], + })], + }); + + THEN_codePipelineExpectation(); + }); + + suite.additional('modern, alternative API', () => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [new cdkp.CodeBuildStep('imageAction', { + commands: ['true'], + buildEnvironment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_2_0, + }, + })], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:2.0', + }, + }); + } +}); + +behavior('can run scripts with magic environment variables', (suite) => { + suite.legacy(() => { + const { pipeline, integTestArtifact } = legacySetup(); + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'imageAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + environmentVariables: { + VERSION: { value: codepipeline.GlobalVariables.executionId }, + }, + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // Run all Build jobs with the given image + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [new cdkp.ShellStep('imageAction', { + commands: ['true'], + env: { + VERSION: codepipeline.GlobalVariables.executionId, + }, + })], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: arrayWith( + objectLike({ + Name: 'imageAction', + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { + name: 'VERSION', + type: 'PLAINTEXT', + value: '#{codepipeline.PipelineExecutionId}', + }, + ]), + }), + }), + ), + }), + }); + } +}); + + +/** + * Some shared setup for legacy API tests + */ +function legacySetup() { + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); + const integTestArtifact = new codepipeline.Artifact('IntegTests'); + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + additionalArtifacts: [{ directory: 'test', artifact: integTestArtifact }], + }), + }); + + return { sourceArtifact, cloudAssemblyArtifact, integTestArtifact, pipeline }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/cross-environment-infra.test.ts b/packages/@aws-cdk/pipelines/test/cross-environment-infra.test.ts deleted file mode 100644 index e442e540b1ac9..0000000000000 --- a/packages/@aws-cdk/pipelines/test/cross-environment-infra.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { arrayWith, objectLike, stringLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import { Stack, Stage, StageProps } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('in a cross-account/cross-region setup, artifact bucket can be read by deploy role', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new TestApplication(app, 'MyApp', { - env: { account: '321elsewhere', region: 'us-elsewhere' }, - })); - - // THEN - app.synth(); - const supportStack = app.node.findAll().filter(Stack.isStack).find(s => s.stackName === 'PipelineStack-support-us-elsewhere'); - expect(supportStack).not.toBeUndefined(); - - expect(supportStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: arrayWith('s3:GetObject*', 's3:GetBucket*', 's3:List*'), - Principal: { - AWS: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - stringLike('*-deploy-role-*'), - ]], - }, - }, - })), - }, - }); - - // And the key to go along with it - expect(supportStack).toHaveResourceLike('AWS::KMS::Key', { - KeyPolicy: { - Statement: arrayWith(objectLike({ - Action: arrayWith('kms:Decrypt', 'kms:DescribeKey'), - Principal: { - AWS: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - stringLike('*-deploy-role-*'), - ]], - }, - }, - })), - }, - }); - }); -}); - -behavior('in a cross-account/same-region setup, artifact bucket can be read by deploy role', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new TestApplication(app, 'MyApp', { - env: { account: '321elsewhere', region: PIPELINE_ENV.region }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - Principal: { - AWS: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - stringLike('*-deploy-role-*'), - ]], - }, - }, - })), - }, - }); - }); -}); - -behavior('in an unspecified-account setup, artifact bucket can be read by deploy role', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new TestApplication(app, 'MyApp', {})); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - Principal: { - AWS: { - 'Fn::Join': ['', arrayWith( - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - stringLike('*-deploy-role-*'), - )], - }, - }, - })), - }, - }); - }); -}); - -behavior('in a same-account setup, artifact bucket can be read by deploy role', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new TestApplication(app, 'MyApp', { - env: PIPELINE_ENV, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - Principal: { - AWS: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - stringLike('*-deploy-role-*'), - ]], - }, - }, - })), - }, - }); - }); -}); - -/** - * Our application - */ -class TestApplication extends Stage { - constructor(scope: Construct, id: string, props: StageProps) { - super(scope, id, props); - new BucketStack(this, 'Stack'); - } -} diff --git a/packages/@aws-cdk/pipelines/test/existing-pipeline.test.ts b/packages/@aws-cdk/pipelines/test/existing-pipeline.test.ts deleted file mode 100644 index b5ad74c799b77..0000000000000 --- a/packages/@aws-cdk/pipelines/test/existing-pipeline.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as cp from '@aws-cdk/aws-codepipeline'; -import { Stack } from '@aws-cdk/core'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { PIPELINE_ENV, TestApp, TestGitHubAction } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let sourceArtifact: cp.Artifact; -let cloudAssemblyArtifact: cp.Artifact; -let codePipeline: cp.Pipeline; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - sourceArtifact = new cp.Artifact(); - cloudAssemblyArtifact = new cp.Artifact(); -}); - -afterEach(() => { - app.cleanup(); -}); - -describe('with empty existing CodePipeline', () => { - beforeEach(() => { - codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline'); - }); - - behavior('both actions are required', (suite) => { - suite.legacy(() => { - // WHEN - expect(() => { - new cdkp.CdkPipeline(pipelineStack, 'Cdk', { cloudAssemblyArtifact, codePipeline }); - }).toThrow(/You must pass a 'sourceAction'/); - }); - }); - - behavior('can give both actions', (suite) => { - suite.legacy(() => { - // WHEN - new cdkp.CdkPipeline(pipelineStack, 'Cdk', { - cloudAssemblyArtifact, - codePipeline, - sourceAction: new TestGitHubAction(sourceArtifact), - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'Source' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - ], - }); - }); - }); -}); - -describe('with custom Source stage in existing Pipeline', () => { - beforeEach(() => { - codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { - stages: [ - { - stageName: 'CustomSource', - actions: [new TestGitHubAction(sourceArtifact)], - }, - ], - }); - }); - - behavior('Work with synthAction', (suite) => { - suite.legacy(() => { - // WHEN - new cdkp.CdkPipeline(pipelineStack, 'Cdk', { - codePipeline, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'CustomSource' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - ], - }); - }); - }); -}); - -describe('with Source and Build stages in existing Pipeline', () => { - beforeEach(() => { - codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { - stages: [ - { - stageName: 'CustomSource', - actions: [new TestGitHubAction(sourceArtifact)], - }, - { - stageName: 'CustomBuild', - actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], - }, - ], - }); - }); - - behavior('can supply no actions', (suite) => { - suite.legacy(() => { - // WHEN - new cdkp.CdkPipeline(pipelineStack, 'Cdk', { - codePipeline, - cloudAssemblyArtifact, - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'CustomSource' }), - objectLike({ Name: 'CustomBuild' }), - objectLike({ Name: 'UpdatePipeline' }), - ], - }); - }); - }); -}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json new file mode 100644 index 0000000000000..27f47b7a985a2 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json @@ -0,0 +1,2336 @@ +{ + "Resources": { + "PipelineArtifactsBucketAEA9A052": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucketPolicyF53CCC52": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleB27FAA37": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicy7BDC1ABB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicy7BDC1ABB", + "Roles": [ + { + "Ref": "PipelineRoleB27FAA37" + } + ] + } + }, + "Pipeline9850B417": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleB27FAA37", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "rix0rrr", + "Repo": "cdk-pipelines-demo", + "Branch": "main", + "OAuthToken": "{{resolve:secretsmanager:github-token:SecretString:::}}", + "PollForSourceChanges": false + }, + "Name": "rix0rrr_cdk-pipelines-demo", + "OutputArtifacts": [ + { + "Name": "rix0rrr_cdk-pipelines-demo_Source" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"00ebacfb32b1bde8d3638577308e7b7144dfa3b0a58a83bc6ff38a3b1f26951c\"}]" + }, + "InputArtifacts": [ + { + "Name": "rix0rrr_cdk-pipelines-demo_Source" + } + ], + "Name": "Synth", + "OutputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"9eda7f97d24aac861052bb47a41b80eecdd56096bf9a88a27c88d94c463785c8\"}]" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "SelfMutate", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "UpdatePipeline" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Beta/PipelineStackBetaStack2C79AD00A.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + } + ], + "Name": "Beta" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod1-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod1/PipelineStackProd1Stack14013D698.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod1.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod2-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod2/PipelineStackProd2Stack1FD464162.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod2.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod1-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod1.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod2-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod2.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod1-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod1/PipelineStackProd1Stack2F0681AFF.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod1.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod2-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod2/PipelineStackProd2Stack2176123EB.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod2.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod1-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod1.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod2-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod2.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + } + ], + "Name": "Wave1" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod3-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod3/PipelineStackProd3Stack1795F3D43.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod3.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod4-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod4/PipelineStackProd4Stack118F74ADB.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod4.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod5-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod5/PipelineStackProd5Stack1E7E4E4C6.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod5.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod6-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod6/PipelineStackProd6Stack1E7C34314.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod6.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod3-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod3.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod4-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod4.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod5-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod5.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod6-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod6.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod3-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod3/PipelineStackProd3Stack2DFBBA0B2.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod3.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod4-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod4/PipelineStackProd4Stack2E2CB4ED3.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod4.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod5-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod5/PipelineStackProd5Stack2C39BEE5B.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod5.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod6-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod6/PipelineStackProd6Stack2BED1BBCE.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod6.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod3-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod3.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod4-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod4.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod5-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod5.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod6-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod6.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + } + ], + "Name": "Wave2" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "Type": "S3" + }, + "RestartExecutionOnUpdate": true + }, + "DependsOn": [ + "PipelineRoleDefaultPolicy7BDC1ABB", + "PipelineRoleB27FAA37" + ] + }, + "PipelineSourcerix0rrrcdkpipelinesdemoWebhookResourceDB0C1BCA": { + "Type": "AWS::CodePipeline::Webhook", + "Properties": { + "Authentication": "GITHUB_HMAC", + "AuthenticationConfiguration": { + "SecretToken": "{{resolve:secretsmanager:github-token:SecretString:::}}" + }, + "Filters": [ + { + "JsonPath": "$.ref", + "MatchEquals": "refs/heads/{Branch}" + } + ], + "TargetAction": "rix0rrr_cdk-pipelines-demo", + "TargetPipeline": { + "Ref": "Pipeline9850B417" + }, + "TargetPipelineVersion": 1, + "RegisterWithThirdParty": true + } + }, + "PipelineBuildSynthCdkBuildProjectRole231EEA2A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C", + "Roles": [ + { + "Ref": "PipelineBuildSynthCdkBuildProjectRole231EEA2A" + } + ] + } + }, + "PipelineBuildSynthCdkBuildProject6BEFA8E6": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectRole231EEA2A", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm ci\",\n \"npm run build\",\n \"npx cdk synth\"\n ]\n }\n },\n \"artifacts\": {\n \"base-directory\": \"cdk.out\",\n \"files\": \"**/*\"\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3" + } + }, + "PipelineBuildSynthCodePipelineActionRole4E7A6C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProject6BEFA8E6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290", + "Roles": [ + { + "Ref": "PipelineBuildSynthCodePipelineActionRole4E7A6C97" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleDefaultPolicyE626265B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationDAA41400", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleDefaultPolicyE626265B", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutationRole57E559E8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Condition": { + "ForAnyValue:StringEquals": { + "iam:ResourceTag/aws-cdk:bootstrap-role": [ + "image-publishing", + "file-publishing", + "deploy" + ] + } + }, + "Effect": "Allow", + "Resource": "arn:*:iam::12345678:role/*" + }, + { + "Action": "cloudformation:DescribeStacks", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:ListBucket", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutationRole57E559E8" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutationDAA41400": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationRole57E559E8", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline.ts b/packages/@aws-cdk/pipelines/test/integ.newpipeline.ts new file mode 100644 index 0000000000000..b777b61a23e09 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline.ts @@ -0,0 +1,62 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +/// !cdk-integ PipelineStack +import * as sqs from '@aws-cdk/aws-sqs'; +import { App, Stack, StackProps, Stage, StageProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as pipelines from '../lib'; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub('rix0rrr/cdk-pipelines-demo', 'main'), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), + }); + + pipeline.addStage(new AppStage(this, 'Beta')); + + const group = pipeline.addWave('Wave1'); + group.addStage(new AppStage(this, 'Prod1')); + group.addStage(new AppStage(this, 'Prod2')); + + const group2 = pipeline.addWave('Wave2'); + group2.addStage(new AppStage(this, 'Prod3')); + group2.addStage(new AppStage(this, 'Prod4')); + group2.addStage(new AppStage(this, 'Prod5')); + group2.addStage(new AppStage(this, 'Prod6')); + } +} + +class AppStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack1 = new Stack(this, 'Stack1'); + const queue1 = new sqs.Queue(stack1, 'Queue'); + + const stack2 = new Stack(this, 'Stack2'); + new sqs.Queue(stack2, 'OtherQueue', { + deadLetterQueue: { + queue: queue1, + maxReceiveCount: 5, + }, + }); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, +}); +new PipelineStack(app, 'PipelineStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +}); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts index a4f35010a10b7..e5461ebe6efe1 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts @@ -14,10 +14,10 @@ class MyStage extends Stage { const stack = new Stack(this, 'Stack', props); new s3_assets.Asset(stack, 'Asset', { - path: path.join(__dirname, 'test-file-asset.txt'), + path: path.join(__dirname, 'testhelpers/assets/test-file-asset.txt'), }); new s3_assets.Asset(stack, 'Asset2', { - path: path.join(__dirname, 'test-file-asset-two.txt'), + path: path.join(__dirname, 'testhelpers/assets/test-file-asset-two.txt'), }); new CfnResource(stack, 'Resource', { diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts index ae9f5046137d4..41b2e6ae0cdc2 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts @@ -14,10 +14,10 @@ class MyStage extends Stage { const stack = new Stack(this, 'Stack', props); new s3_assets.Asset(stack, 'Asset', { - path: path.join(__dirname, 'test-file-asset.txt'), + path: path.join(__dirname, 'testhelpers/assets/test-file-asset.txt'), }); new s3_assets.Asset(stack, 'Asset2', { - path: path.join(__dirname, 'test-file-asset-two.txt'), + path: path.join(__dirname, 'testhelpers/assets/test-file-asset-two.txt'), }); new CfnResource(stack, 'Resource', { diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline.ts index b79dd24841472..f263e65a7f09c 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline.ts @@ -80,4 +80,4 @@ const app = new App({ new CdkpipelinesDemoPipelineStack(app, 'PipelineStack', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, }); -app.synth(); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/fs.test.ts b/packages/@aws-cdk/pipelines/test/legacy/fs.test.ts similarity index 85% rename from packages/@aws-cdk/pipelines/test/fs.test.ts rename to packages/@aws-cdk/pipelines/test/legacy/fs.test.ts index 49cbe2458e64a..da49fa9cf2986 100644 --- a/packages/@aws-cdk/pipelines/test/fs.test.ts +++ b/packages/@aws-cdk/pipelines/test/legacy/fs.test.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { toPosixPath } from '../lib/private/fs'; +import { toPosixPath } from '../../lib/private/fs'; test('translate path.sep', () => { expect(toPosixPath(`a${path.sep}b${path.sep}c`)).toEqual('a/b/c'); diff --git a/packages/@aws-cdk/pipelines/test/pipeline.test.ts b/packages/@aws-cdk/pipelines/test/pipeline.test.ts deleted file mode 100644 index fdb20d19ae396..0000000000000 --- a/packages/@aws-cdk/pipelines/test/pipeline.test.ts +++ /dev/null @@ -1,563 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { - anything, - arrayWith, - Capture, - deepObjectLike, - encodedJson, - notMatching, - objectLike, - stringLike, -} from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as cp from '@aws-cdk/aws-codepipeline'; -import * as cpa from '@aws-cdk/aws-codepipeline-actions'; -import { Stack, Stage, StageProps, SecretValue, Tags } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { BucketStack, PIPELINE_ENV, stackTemplate, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('references stack template in subassembly', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'App')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'App', - Actions: arrayWith( - objectLike({ - Name: 'Stack.Prepare', - InputArtifacts: [objectLike({})], - Configuration: objectLike({ - StackName: 'App-Stack', - TemplatePath: stringLike('*::assembly-App/*.template.json'), - }), - }), - ), - }), - }); - }); - -}); - -behavior('obvious error is thrown when stage contains no stacks', (suite) => { - suite.legacy(() => { - // WHEN - expect(() => { - pipeline.addApplicationStage(new Stage(app, 'EmptyStage')); - }).toThrow(/should contain at least one Stack/); - }); -}); - -behavior('action has right settings for same-env deployment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'Same')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Same', - Actions: [ - objectLike({ - Name: 'Stack.Prepare', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-deploy-role-', - { Ref: 'AWS::AccountId' }, - '-', - { Ref: 'AWS::Region' }, - ]], - }, - Configuration: objectLike({ - StackName: 'Same-Stack', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-cfn-exec-role-', - { Ref: 'AWS::AccountId' }, - '-', - { Ref: 'AWS::Region' }, - ]], - }, - }), - }), - objectLike({ - Name: 'Stack.Deploy', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-deploy-role-', - { Ref: 'AWS::AccountId' }, - '-', - { Ref: 'AWS::Region' }, - ]], - }, - Configuration: objectLike({ - StackName: 'Same-Stack', - }), - }), - ], - }), - }); - }); -}); - -behavior('action has right settings for cross-account deployment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'CrossAccount', { env: { account: 'you' } })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'CrossAccount', - Actions: [ - objectLike({ - Name: 'Stack.Prepare', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-deploy-role-you-', - { Ref: 'AWS::Region' }, - ]], - }, - Configuration: objectLike({ - StackName: 'CrossAccount-Stack', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-cfn-exec-role-you-', - { Ref: 'AWS::Region' }, - ]], - }, - }), - }), - objectLike({ - Name: 'Stack.Deploy', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-deploy-role-you-', - { Ref: 'AWS::Region' }, - ]], - }, - Configuration: objectLike({ - StackName: 'CrossAccount-Stack', - }), - }), - ], - }), - }); - }); -}); - -behavior('action has right settings for cross-region deployment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'CrossRegion', { env: { region: 'elsewhere' } })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'CrossRegion', - Actions: [ - objectLike({ - Name: 'Stack.Prepare', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-deploy-role-', - { Ref: 'AWS::AccountId' }, - '-elsewhere', - ]], - }, - Region: 'elsewhere', - Configuration: objectLike({ - StackName: 'CrossRegion-Stack', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-cfn-exec-role-', - { Ref: 'AWS::AccountId' }, - '-elsewhere', - ]], - }, - }), - }), - objectLike({ - Name: 'Stack.Deploy', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-deploy-role-', - { Ref: 'AWS::AccountId' }, - '-elsewhere', - ]], - }, - Region: 'elsewhere', - Configuration: objectLike({ - StackName: 'CrossRegion-Stack', - }), - }), - ], - }), - }); - }); -}); - -behavior('action has right settings for cross-account/cross-region deployment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'CrossBoth', { - env: { - account: 'you', - region: 'elsewhere', - }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'CrossBoth', - Actions: [ - objectLike({ - Name: 'Stack.Prepare', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-deploy-role-you-elsewhere', - ]], - }, - Region: 'elsewhere', - Configuration: objectLike({ - StackName: 'CrossBoth-Stack', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-cfn-exec-role-you-elsewhere', - ]], - }, - }), - }), - objectLike({ - Name: 'Stack.Deploy', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-deploy-role-you-elsewhere', - ]], - }, - Region: 'elsewhere', - Configuration: objectLike({ - StackName: 'CrossBoth-Stack', - }), - }), - ], - }), - }); - }); -}); - -behavior('pipeline has self-mutation stage', (suite) => { - suite.legacy(() => { - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'UpdatePipeline', - Actions: [ - objectLike({ - Name: 'SelfMutate', - Configuration: objectLike({ - ProjectName: { Ref: anything() }, - }), - }), - ], - }), - }); - - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - PrivilegedMode: false, - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: ['npm install -g aws-cdk'], - }, - build: { - commands: arrayWith('cdk -a . deploy PipelineStack --require-approval=never --verbose'), - }, - }, - })), - Type: 'CODEPIPELINE', - }, - }); - }); -}); - -behavior('selfmutation stage correctly identifies nested assembly of pipeline stack', (suite) => { - suite.legacy(() => { - const pipelineStage = new Stage(app, 'PipelineStage'); - const nestedPipelineStack = new Stack(pipelineStage, 'PipelineStack', { env: PIPELINE_ENV }); - new TestGitHubNpmPipeline(nestedPipelineStack, 'Cdk'); - - // THEN - expect(stackTemplate(nestedPipelineStack)).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: arrayWith('cdk -a assembly-PipelineStage deploy PipelineStage/PipelineStack --require-approval=never --verbose'), - }, - }, - })), - }, - }); - }); -}); - -behavior('selfmutation feature can be turned off', (suite) => { - suite.legacy(() => { - const stack = new Stack(); - const cloudAssemblyArtifact = new cp.Artifact(); - // WHEN - new TestGitHubNpmPipeline(stack, 'Cdk', { - cloudAssemblyArtifact, - selfMutating: false, - }); - // THEN - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: notMatching(arrayWith({ - Name: 'UpdatePipeline', - Actions: anything(), - })), - }); - }); -}); - -behavior('generates CodeBuild project in privileged mode', (suite) => { - suite.legacy(() => { - // WHEN - const stack = new Stack(app, 'PrivilegedPipelineStack', { env: PIPELINE_ENV }); - new TestGitHubNpmPipeline(stack, 'PrivilegedPipeline', { - supportDockerAssets: true, - }); - - // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - PrivilegedMode: true, - }, - }); - }); -}); - -behavior('overridden stack names are respected', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackAppWithCustomName(app, 'App1')); - pipeline.addApplicationStage(new OneStackAppWithCustomName(app, 'App2')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith( - { - Name: 'App1', - Actions: arrayWith(objectLike({ - Name: 'MyFancyStack.Prepare', - Configuration: objectLike({ - StackName: 'MyFancyStack', - }), - })), - }, - { - Name: 'App2', - Actions: arrayWith(objectLike({ - Name: 'MyFancyStack.Prepare', - Configuration: objectLike({ - StackName: 'MyFancyStack', - }), - })), - }, - ), - }); - }); -}); - -behavior('can control fix/CLI version used in pipeline selfupdate', (suite) => { - suite.legacy(() => { - // WHEN - const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); - new TestGitHubNpmPipeline(stack2, 'Cdk2', { - pipelineName: 'vpipe', - cdkCliVersion: '1.2.3', - }); - - // THEN - expect(stack2).toHaveResourceLike('AWS::CodeBuild::Project', { - Name: 'vpipe-selfupdate', - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: ['npm install -g aws-cdk@1.2.3'], - }, - }, - })), - }, - }); - }); -}); - -behavior('changing CLI version leads to a different pipeline structure (restarting it)', (suite) => { - suite.legacy(() => { - // GIVEN - const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); - const stack3 = new Stack(app, 'Stack3', { env: PIPELINE_ENV }); - const structure2 = Capture.anyType(); - const structure3 = Capture.anyType(); - - // WHEN - new TestGitHubNpmPipeline(stack2, 'Cdk', { - cdkCliVersion: '1.2.3', - }); - new TestGitHubNpmPipeline(stack3, 'Cdk', { - cdkCliVersion: '4.5.6', - }); - - // THEN - expect(stack2).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: structure2.capture(), - }); - expect(stack3).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: structure3.capture(), - }); - - expect(JSON.stringify(structure2.capturedValue)).not.toEqual(JSON.stringify(structure3.capturedValue)); - }); -}); - -behavior('add another action to an existing stage', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.stage('Source').addAction(new cpa.GitHubSourceAction({ - actionName: 'GitHub2', - oauthToken: SecretValue.plainText('oops'), - output: new cp.Artifact(), - owner: 'OWNER', - repo: 'REPO', - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Source', - Actions: [ - objectLike({ Name: 'GitHub' }), - objectLike({ Name: 'GitHub2' }), - ], - }), - }); - }); -}); - -behavior('tags get reflected in pipeline', (suite) => { - suite.legacy(() => { - // WHEN - const stage = new OneStackApp(app, 'App'); - Tags.of(stage).add('CostCenter', 'F00B4R'); - pipeline.addApplicationStage(stage); - - // THEN - const templateConfig = Capture.aString(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'App', - Actions: arrayWith( - objectLike({ - Name: 'Stack.Prepare', - InputArtifacts: [objectLike({})], - Configuration: objectLike({ - StackName: 'App-Stack', - TemplateConfiguration: templateConfig.capture(stringLike('*::assembly-App/*.template.*json')), - }), - }), - ), - }), - }); - - const [, relConfigFile] = templateConfig.capturedValue.split('::'); - const absConfigFile = path.join(app.outdir, relConfigFile); - const configFile = JSON.parse(fs.readFileSync(absConfigFile, { encoding: 'utf-8' })); - expect(configFile).toEqual(expect.objectContaining({ - Tags: { - CostCenter: 'F00B4R', - }, - })); - }); -}); - -class OneStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - new BucketStack(this, 'Stack'); - } -} - -class OneStackAppWithCustomName extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - new BucketStack(this, 'Stack', { - stackName: 'MyFancyStack', - }); - } -} diff --git a/packages/@aws-cdk/pipelines/test/test-docker-asset/Dockerfile b/packages/@aws-cdk/pipelines/test/testhelpers/assets/test-docker-asset/Dockerfile similarity index 100% rename from packages/@aws-cdk/pipelines/test/test-docker-asset/Dockerfile rename to packages/@aws-cdk/pipelines/test/testhelpers/assets/test-docker-asset/Dockerfile diff --git a/packages/@aws-cdk/pipelines/test/test-file-asset-two.txt b/packages/@aws-cdk/pipelines/test/testhelpers/assets/test-file-asset-two.txt similarity index 100% rename from packages/@aws-cdk/pipelines/test/test-file-asset-two.txt rename to packages/@aws-cdk/pipelines/test/testhelpers/assets/test-file-asset-two.txt diff --git a/packages/@aws-cdk/pipelines/test/test-file-asset.txt b/packages/@aws-cdk/pipelines/test/testhelpers/assets/test-file-asset.txt similarity index 100% rename from packages/@aws-cdk/pipelines/test/test-file-asset.txt rename to packages/@aws-cdk/pipelines/test/testhelpers/assets/test-file-asset.txt diff --git a/packages/@aws-cdk/pipelines/test/helpers/compliance.ts b/packages/@aws-cdk/pipelines/test/testhelpers/compliance.ts similarity index 62% rename from packages/@aws-cdk/pipelines/test/helpers/compliance.ts rename to packages/@aws-cdk/pipelines/test/testhelpers/compliance.ts index a152c1ef87b10..bf6603d4753cb 100644 --- a/packages/@aws-cdk/pipelines/test/helpers/compliance.ts +++ b/packages/@aws-cdk/pipelines/test/testhelpers/compliance.ts @@ -4,54 +4,43 @@ interface SkippedSuite { modern(reason?: string): void; } -interface ParameterizedSuite { - legacy(fn: (arg: any) => void): void; - - modern(fn: (arg: any) => void): void; -} - interface Suite { readonly doesNotApply: SkippedSuite; - each(cases: any[]): ParameterizedSuite; - legacy(fn: () => void): void; modern(fn: () => void): void; + + additional(description: string, fn: () => void): void; } // eslint-disable-next-line jest/no-export export function behavior(name: string, cb: (suite: Suite) => void) { // 'describe()' adds a nice grouping in Jest describe(name, () => { - const unwritten = new Set(['modern', 'legacy']); + + function scratchOff(flavor: string) { + if (!unwritten.has(flavor)) { + throw new Error(`Already had test for ${flavor}. Use .additional() to add more tests.`); + } + unwritten.delete(flavor); + } + + cb({ - each: (cases: any[]) => { - return { - legacy: (testFn) => { - unwritten.delete('legacy'); - describe('legacy', () => { - test.each(cases)(name, testFn); - }); - }, - modern: (testFn) => { - unwritten.delete('modern'); - test.each(cases)('modern', testFn); - }, - }; - }, legacy: (testFn) => { - unwritten.delete('legacy'); + scratchOff('legacy'); test('legacy', testFn); }, modern: (testFn) => { - unwritten.delete('modern'); + scratchOff('modern'); test('modern', testFn); }, + additional: test, doesNotApply: { modern: (reason?: string) => { - unwritten.delete('modern'); + scratchOff('modern'); if (reason != null) { // eslint-disable-next-line jest/no-disabled-tests @@ -60,7 +49,7 @@ export function behavior(name: string, cb: (suite: Suite) => void) { }, legacy: (reason?: string) => { - unwritten.delete('legacy'); + scratchOff('legacy'); if (reason != null) { // eslint-disable-next-line jest/no-disabled-tests diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/index.ts b/packages/@aws-cdk/pipelines/test/testhelpers/index.ts new file mode 100644 index 0000000000000..21ca108240f27 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/index.ts @@ -0,0 +1,5 @@ +export * from './compliance'; +export * from './legacy-pipeline'; +export * from './modern-pipeline'; +export * from './test-app'; +export * from './testmatchers'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/legacy-pipeline.ts b/packages/@aws-cdk/pipelines/test/testhelpers/legacy-pipeline.ts new file mode 100644 index 0000000000000..63ffec75b7188 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/legacy-pipeline.ts @@ -0,0 +1,48 @@ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import { SecretValue } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cdkp from '../../lib'; + +export interface LegacyTestGitHubNpmPipelineExtraProps { + readonly sourceArtifact?: codepipeline.Artifact; + readonly npmSynthOptions?: Partial; +} + +export class LegacyTestGitHubNpmPipeline extends cdkp.CdkPipeline { + public readonly sourceArtifact: codepipeline.Artifact; + public readonly cloudAssemblyArtifact: codepipeline.Artifact; + + constructor(scope: Construct, id: string, props?: Partial & LegacyTestGitHubNpmPipelineExtraProps) { + const sourceArtifact = props?.sourceArtifact ?? new codepipeline.Artifact(); + const cloudAssemblyArtifact = props?.cloudAssemblyArtifact ?? new codepipeline.Artifact(); + + super(scope, id, { + sourceAction: new TestGitHubAction(sourceArtifact), + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + ...props?.npmSynthOptions, + }), + cloudAssemblyArtifact, + ...props, + }); + + this.sourceArtifact = sourceArtifact; + this.cloudAssemblyArtifact = cloudAssemblyArtifact; + } +} + + +export class TestGitHubAction extends codepipeline_actions.GitHubSourceAction { + constructor(sourceArtifact: codepipeline.Artifact) { + super({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.plainText('$3kr1t'), + owner: 'test', + repo: 'test', + trigger: codepipeline_actions.GitHubTrigger.POLL, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/modern-pipeline.ts b/packages/@aws-cdk/pipelines/test/testhelpers/modern-pipeline.ts new file mode 100644 index 0000000000000..b3e783ea8f569 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/modern-pipeline.ts @@ -0,0 +1,26 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { Construct } from 'constructs'; +import * as cdkp from '../../lib'; + +export type ModernTestGitHubNpmPipelineProps = Partial & Partial; + +export class ModernTestGitHubNpmPipeline extends cdkp.CodePipeline { + public readonly gitHubSource: cdkp.CodePipelineSource; + + constructor(scope: Construct, id: string, props?: ModernTestGitHubNpmPipelineProps) { + const source = cdkp.CodePipelineSource.gitHub('test/test', 'main'); + const synth = props?.synth ?? new cdkp.ShellStep('Synth', { + input: source, + installCommands: ['npm ci'], + commands: ['npx cdk synth'], + ...props, + }); + + super(scope, id, { + synth: synth, + ...props, + }); + + this.gitHubSource = source; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/test-app.ts b/packages/@aws-cdk/pipelines/test/testhelpers/test-app.ts new file mode 100644 index 0000000000000..1f554b75e2623 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/test-app.ts @@ -0,0 +1,214 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import '@aws-cdk/assert-internal/jest'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import { App, AppProps, Environment, CfnOutput, Stage, StageProps, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { assemblyBuilderOf } from '../../lib/private/construct-internals'; + +export const PIPELINE_ENV: Environment = { + account: '123pipeline', + region: 'us-pipeline', +}; + +export class TestApp extends App { + constructor(props?: Partial) { + super({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, + stackTraces: false, + autoSynth: false, + treeMetadata: false, + ...props, + }); + } + + public stackArtifact(stackName: string | Stack) { + if (typeof stackName !== 'string') { + stackName = stackName.stackName; + } + + this.synth(); + const supportStack = this.node.findAll().filter(Stack.isStack).find(s => s.stackName === stackName); + expect(supportStack).not.toBeUndefined(); + return supportStack; + } + + public cleanup() { + rimraf(assemblyBuilderOf(this).outdir); + } +} + + +export class OneStackApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + new BucketStack(this, 'Stack'); + } +} + +export class AppWithOutput extends Stage { + public readonly theOutput: CfnOutput; + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack = new BucketStack(this, 'Stack'); + this.theOutput = new CfnOutput(stack, 'MyOutput', { value: stack.bucket.bucketName }); + } +} + +export class TwoStackApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack2 = new BucketStack(this, 'Stack2'); + const stack1 = new BucketStack(this, 'Stack1'); + + stack2.addDependency(stack1); + } +} + +/** + * Three stacks where the last one depends on the earlier 2 + */ +export class ThreeStackApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack1 = new BucketStack(this, 'Stack1'); + const stack2 = new BucketStack(this, 'Stack2'); + const stack3 = new BucketStack(this, 'Stack3'); + + stack3.addDependency(stack1); + stack3.addDependency(stack2); + } +} + +/** + * A test stack + * + * It contains a single Bucket. Such robust. Much uptime. + */ +export class BucketStack extends Stack { + public readonly bucket: s3.IBucket; + + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + this.bucket = new s3.Bucket(this, 'Bucket'); + } +} + + +/** + * rm -rf reimplementation, don't want to depend on an NPM package for this + */ +export function rimraf(fsPath: string) { + try { + const isDir = fs.lstatSync(fsPath).isDirectory(); + + if (isDir) { + for (const file of fs.readdirSync(fsPath)) { + rimraf(path.join(fsPath, file)); + } + fs.rmdirSync(fsPath); + } else { + fs.unlinkSync(fsPath); + } + } catch (e) { + // We will survive ENOENT + if (e.code !== 'ENOENT') { throw e; } + } +} + +export function stackTemplate(stack: Stack) { + const stage = Stage.of(stack); + if (!stage) { throw new Error('stack not in a Stage'); } + return stage.synth().getStackArtifact(stack.artifactId); +} + +export class StageWithStackOutput extends Stage { + public readonly output: CfnOutput; + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new BucketStack(this, 'Stack'); + + this.output = new CfnOutput(stack, 'BucketName', { + value: stack.bucket.bucketName, + }); + } +} + +export class FileAssetApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + new s3_assets.Asset(stack, 'Asset', { + path: path.join(__dirname, 'assets', 'test-file-asset.txt'), + }); + } +} + +export class TwoFileAssetsApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + new s3_assets.Asset(stack, 'Asset1', { + path: path.join(__dirname, 'assets', 'test-file-asset.txt'), + }); + new s3_assets.Asset(stack, 'Asset2', { + path: path.join(__dirname, 'assets', 'test-file-asset-two.txt'), + }); + } +} + +export class DockerAssetApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + new ecr_assets.DockerImageAsset(stack, 'Asset', { + directory: path.join(__dirname, 'assets', 'test-docker-asset'), + }); + } +} + +export interface MegaAssetsAppProps extends StageProps { + readonly numAssets: number; +} + +// Creates a mix of file and image assets, up to a specified count +export class MegaAssetsApp extends Stage { + constructor(scope: Construct, id: string, props: MegaAssetsAppProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + + let assetCount = 0; + for (; assetCount < props.numAssets / 2; assetCount++) { + new s3_assets.Asset(stack, `Asset${assetCount}`, { + path: path.join(__dirname, 'assets', 'test-file-asset.txt'), + assetHash: `FileAsset${assetCount}`, + }); + } + for (; assetCount < props.numAssets; assetCount++) { + new ecr_assets.DockerImageAsset(stack, `Asset${assetCount}`, { + directory: path.join(__dirname, 'assets', 'test-docker-asset'), + extraHash: `FileAsset${assetCount}`, + }); + } + } +} + +export class PlainStackApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + new BucketStack(this, 'Stack'); + } +} + + diff --git a/packages/@aws-cdk/pipelines/test/testmatchers.ts b/packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts similarity index 61% rename from packages/@aws-cdk/pipelines/test/testmatchers.ts rename to packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts index 90b31fb133fd4..8faa855b71abf 100644 --- a/packages/@aws-cdk/pipelines/test/testmatchers.ts +++ b/packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { annotateMatcher, InspectionFailure, matcherFrom, PropertyMatcher } from '@aws-cdk/assert-internal'; /** @@ -22,4 +23,20 @@ export function sortedByRunOrder(matcher: any): PropertyMatcher { return matcherFrom(matcher)(value, failure); }); +} + +export function stringNoLongerThan(length: number): PropertyMatcher { + return annotateMatcher({ $stringIsNoLongerThan: length }, (value: any, failure: InspectionFailure) => { + if (typeof value !== 'string') { + failure.failureReason = `Expected a string, but got '${typeof value}'`; + return false; + } + + if (value.length > length) { + failure.failureReason = `String is ${value.length} characters long. Expected at most ${length} characters`; + return false; + } + + return true; + }); } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testutil.ts b/packages/@aws-cdk/pipelines/test/testutil.ts deleted file mode 100644 index e654299d85182..0000000000000 --- a/packages/@aws-cdk/pipelines/test/testutil.ts +++ /dev/null @@ -1,136 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { annotateMatcher, InspectionFailure, PropertyMatcher } from '@aws-cdk/assert-internal'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; -import * as s3 from '@aws-cdk/aws-s3'; -import { App, AppProps, Environment, SecretValue, Stack, StackProps, Stage } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { assemblyBuilderOf } from '../lib/private/construct-internals'; - -export const PIPELINE_ENV: Environment = { - account: '123pipeline', - region: 'us-pipeline', -}; - -export class TestApp extends App { - constructor(props?: Partial) { - super({ - context: { - '@aws-cdk/core:newStyleStackSynthesis': '1', - }, - stackTraces: false, - autoSynth: false, - treeMetadata: false, - ...props, - }); - } - - public cleanup() { - rimraf(assemblyBuilderOf(this).outdir); - } -} - -export interface TestGitHubNpmPipelineExtraProps { - readonly sourceArtifact?: codepipeline.Artifact; - readonly npmSynthOptions?: Partial; -} - -export class TestGitHubNpmPipeline extends cdkp.CdkPipeline { - public readonly sourceArtifact: codepipeline.Artifact; - public readonly cloudAssemblyArtifact: codepipeline.Artifact; - - constructor(scope: Construct, id: string, props?: Partial & TestGitHubNpmPipelineExtraProps ) { - const sourceArtifact = props?.sourceArtifact ?? new codepipeline.Artifact(); - const cloudAssemblyArtifact = props?.cloudAssemblyArtifact ?? new codepipeline.Artifact(); - - super(scope, id, { - sourceAction: new TestGitHubAction(sourceArtifact), - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - ...props?.npmSynthOptions, - }), - cloudAssemblyArtifact, - ...props, - }); - - this.sourceArtifact = sourceArtifact; - this.cloudAssemblyArtifact = cloudAssemblyArtifact; - } -} - - -export class TestGitHubAction extends codepipeline_actions.GitHubSourceAction { - constructor(sourceArtifact: codepipeline.Artifact) { - super({ - actionName: 'GitHub', - output: sourceArtifact, - oauthToken: SecretValue.plainText('$3kr1t'), - owner: 'test', - repo: 'test', - trigger: codepipeline_actions.GitHubTrigger.POLL, - }); - } -} - -/** - * A test stack - * - * It contains a single Bucket. Such robust. Much uptime. - */ -export class BucketStack extends Stack { - public readonly bucket: s3.IBucket; - - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); - this.bucket = new s3.Bucket(this, 'Bucket'); - } -} - -/** - * rm -rf reimplementation, don't want to depend on an NPM package for this - */ -export function rimraf(fsPath: string) { - try { - const isDir = fs.lstatSync(fsPath).isDirectory(); - - if (isDir) { - for (const file of fs.readdirSync(fsPath)) { - rimraf(path.join(fsPath, file)); - } - fs.rmdirSync(fsPath); - } else { - fs.unlinkSync(fsPath); - } - } catch (e) { - // We will survive ENOENT - if (e.code !== 'ENOENT') { throw e; } - } -} - -/** - * Because 'expect(stack)' doesn't work correctly for stacks in nested assemblies - */ -export function stackTemplate(stack: Stack) { - const stage = Stage.of(stack); - if (!stage) { throw new Error('stack not in a Stage'); } - return stage.synth().getStackArtifact(stack.artifactId); -} - -export function stringNoLongerThan(length: number): PropertyMatcher { - return annotateMatcher({ $stringIsNoLongerThan: length }, (value: any, failure: InspectionFailure) => { - if (typeof value !== 'string') { - failure.failureReason = `Expected a string, but got '${typeof value}'`; - return false; - } - - if (value.length > length) { - failure.failureReason = `String is ${value.length} characters long. Expected at most ${length} characters`; - return false; - } - - return true; - }); -} diff --git a/packages/@aws-cdk/pipelines/test/validation.test.ts b/packages/@aws-cdk/pipelines/test/validation.test.ts deleted file mode 100644 index a75431e63266a..0000000000000 --- a/packages/@aws-cdk/pipelines/test/validation.test.ts +++ /dev/null @@ -1,497 +0,0 @@ -import { anything, arrayWith, deepObjectLike, encodedJson, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as codebuild from '@aws-cdk/aws-codebuild'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as iam from '@aws-cdk/aws-iam'; -import * as s3 from '@aws-cdk/aws-s3'; -import { CfnOutput, Stack, Stage, StageProps } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { } from './testmatchers'; -import { behavior } from './helpers/compliance'; -import { BucketStack, PIPELINE_ENV, stringNoLongerThan, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; -let sourceArtifact: codepipeline.Artifact; -let cloudAssemblyArtifact: codepipeline.Artifact; -let integTestArtifact: codepipeline.Artifact; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - sourceArtifact = new codepipeline.Artifact(); - cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); - integTestArtifact = new codepipeline.Artifact('IntegTests'); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - additionalArtifacts: [{ directory: 'test', artifact: integTestArtifact }], - }), - }); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('stackOutput generates names limited to 100 characters', (suite) => { - suite.legacy(() => { - const stage = new AppWithStackOutput(app, 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild'); - const pipeStage = pipeline.addApplicationStage(stage); - pipeStage.addActions(new cdkp.ShellScriptAction({ - actionName: 'TestOutput', - useOutputs: { - BUCKET_NAME: pipeline.stackOutput(stage.output), - }, - commands: ['echo $BUCKET_NAME'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', - Actions: arrayWith( - deepObjectLike({ - Name: 'Stack.Deploy', - OutputArtifacts: [{ Name: stringNoLongerThan(100) }], - Configuration: { - OutputFileName: 'outputs.json', - }, - }), - deepObjectLike({ - ActionTypeId: { - Provider: 'CodeBuild', - }, - Configuration: { - ProjectName: anything(), - }, - InputArtifacts: [{ Name: stringNoLongerThan(100) }], - Name: 'TestOutput', - }), - ), - }), - }); - }); -}); - -behavior('can use stack outputs as validation inputs', (suite) => { - suite.legacy(() => { - // GIVEN - const stage = new AppWithStackOutput(app, 'MyApp'); - - // WHEN - const pipeStage = pipeline.addApplicationStage(stage); - pipeStage.addActions(new cdkp.ShellScriptAction({ - actionName: 'TestOutput', - useOutputs: { - BUCKET_NAME: pipeline.stackOutput(stage.output), - }, - commands: ['echo $BUCKET_NAME'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'MyApp', - Actions: arrayWith( - deepObjectLike({ - Name: 'Stack.Deploy', - OutputArtifacts: [{ Name: anything() }], - Configuration: { - OutputFileName: 'outputs.json', - }, - }), - deepObjectLike({ - ActionTypeId: { - Provider: 'CodeBuild', - }, - Configuration: { - ProjectName: anything(), - }, - InputArtifacts: [{ Name: anything() }], - Name: 'TestOutput', - }), - ), - }), - }); - - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: [ - 'set -eu', - 'export BUCKET_NAME="$(node -pe \'require(process.env.CODEBUILD_SRC_DIR + "/outputs.json")["BucketName"]\')"', - 'echo $BUCKET_NAME', - ], - }, - }, - })), - Type: 'CODEPIPELINE', - }, - }); - }); -}); - -behavior('can use additional files from source', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'UseSources', - additionalArtifacts: [sourceArtifact], - commands: ['true'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'UseSources', - InputArtifacts: [{ Name: 'Artifact_Source_GitHub' }], - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: [ - 'set -eu', - 'true', - ], - }, - }, - })), - }, - }); - }); -}); - -behavior('can use additional files from build', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'UseBuildArtifact', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'UseBuildArtifact', - InputArtifacts: [{ Name: 'IntegTests' }], - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: [ - 'set -eu', - 'true', - ], - }, - }, - })), - }, - }); - }); -}); - -behavior('add policy statements to ShellScriptAction', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'Boop', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - rolePolicyStatements: [ - new iam.PolicyStatement({ - actions: ['s3:Banana'], - resources: ['*'], - }), - ], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith(deepObjectLike({ - Action: 's3:Banana', - Resource: '*', - })), - }, - }); - }); -}); - -behavior('ShellScriptAction is IGrantable', (suite) => { - suite.legacy(() => { - // GIVEN - const action = new cdkp.ShellScriptAction({ - actionName: 'Boop', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - }); - pipeline.addStage('Test').addActions(action); - const bucket = new s3.Bucket(pipelineStack, 'Bucket'); - - // WHEN - bucket.grantRead(action); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith(deepObjectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - })), - }, - }); - }); -}); - -behavior('run ShellScriptAction in a VPC', (suite) => { - suite.legacy(() => { - // WHEN - const vpc = new ec2.Vpc(pipelineStack, 'VPC'); - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - vpc, - actionName: 'VpcAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'VpcAction', - InputArtifacts: [{ Name: 'IntegTests' }], - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - VpcConfig: { - SecurityGroupIds: [ - { - 'Fn::GetAtt': [ - 'CdkPipelineTestVpcActionProjectSecurityGroupBA94D315', - 'GroupId', - ], - }, - ], - Subnets: [ - { - Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', - }, - { - Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', - }, - { - Ref: 'VPCPrivateSubnet3Subnet3EDCD457', - }, - ], - VpcId: { - Ref: 'VPCB9E5F0B4', - }, - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: [ - 'set -eu', - 'true', - ], - }, - }, - })), - }, - }); - }); -}); - -behavior('run ShellScriptAction with Security Group', (suite) => { - suite.legacy(() => { - // WHEN - const vpc = new ec2.Vpc(pipelineStack, 'VPC'); - const sg = new ec2.SecurityGroup(pipelineStack, 'SG', { vpc }); - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - vpc, - securityGroups: [sg], - actionName: 'sgAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'sgAction', - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - VpcConfig: { - SecurityGroupIds: [ - { - 'Fn::GetAtt': [ - 'SGADB53937', - 'GroupId', - ], - }, - ], - VpcId: { - Ref: 'VPCB9E5F0B4', - }, - }, - }); - }); -}); - -behavior('run ShellScriptAction with specified codebuild image', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'imageAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_2_0 }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'imageAction', - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:2.0', - }, - }); - }); -}); - -behavior('run ShellScriptAction with specified BuildEnvironment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'imageAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - environment: { - buildImage: codebuild.LinuxBuildImage.STANDARD_2_0, - computeType: codebuild.ComputeType.LARGE, - environmentVariables: { FOO: { value: 'BAR', type: codebuild.BuildEnvironmentVariableType.PLAINTEXT } }, - privileged: true, - }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:2.0', - PrivilegedMode: true, - ComputeType: 'BUILD_GENERAL1_LARGE', - EnvironmentVariables: [ - { - Type: 'PLAINTEXT', - Value: 'BAR', - Name: 'FOO', - }, - ], - }, - }); - }); -}); - -behavior('run ShellScriptAction with specified environment variables', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'imageAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - environmentVariables: { - VERSION: { value: codepipeline.GlobalVariables.executionId }, - }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - objectLike({ - Name: 'imageAction', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ - { - name: 'VERSION', - type: 'PLAINTEXT', - value: '#{codepipeline.PipelineExecutionId}', - }, - ]), - }), - }), - ], - }), - }); - }); -}); - -class AppWithStackOutput extends Stage { - public readonly output: CfnOutput; - - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - const stack = new BucketStack(this, 'Stack'); - - this.output = new CfnOutput(stack, 'BucketName', { - value: stack.bucket.bucketName, - }); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/region-info/package.json b/packages/@aws-cdk/region-info/package.json index ac5e03de5364b..c86fae71c220a 100644 --- a/packages/@aws-cdk/region-info/package.json +++ b/packages/@aws-cdk/region-info/package.json @@ -56,8 +56,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "fs-extra": "^9.1.0", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/yaml-cfn/package.json b/packages/@aws-cdk/yaml-cfn/package.json index c8219f94b9d38..ed33f49235536 100644 --- a/packages/@aws-cdk/yaml-cfn/package.json +++ b/packages/@aws-cdk/yaml-cfn/package.json @@ -69,7 +69,7 @@ "yaml": "1.10.2" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/yaml": "^1.9.7", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index 3b0b3312742f3..fa3ae197cad03 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -34,7 +34,7 @@ "license": "Apache-2.0", "devDependencies": { "@monocdk-experiment/rewrite-imports": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "constructs": "^3.3.69", diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index 0e8c3e2c3d042..71e1e67dbb146 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -38,8 +38,8 @@ "typescript": "~3.9.10" }, "devDependencies": { - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 8761fbbf8e44e..1a609044fbbdb 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -324,7 +324,7 @@ "@aws-cdk/lambda-layer-kubectl": "0.0.0", "@aws-cdk/pipelines": "0.0.0", "@aws-cdk/region-info": "0.0.0", - "@types/fs-extra": "^8.1.1", + "@types/fs-extra": "^8.1.2", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "constructs": "^3.3.69", diff --git a/packages/aws-cdk-migration/package.json b/packages/aws-cdk-migration/package.json index 873f3771f341b..714dd26493897 100644 --- a/packages/aws-cdk-migration/package.json +++ b/packages/aws-cdk-migration/package.json @@ -38,8 +38,8 @@ "typescript": "~3.9.10" }, "devDependencies": { - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index 2927360d7b3e2..36182c083fa55 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -281,29 +281,21 @@ Resources: - Ref: AWS::NoValue RoleName: Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region} + ManagedPolicyArns: + - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" Policies: - PolicyDocument: Statement: - - Action: - - ec2:DescribeVpcs - - ec2:DescribeAvailabilityZones - - ec2:DescribeSubnets - - ec2:DescribeRouteTables - - ec2:DescribeVpnGateways - - ec2:DescribeImages - - ec2:DescribeVpcEndpointServices - - ec2:DescribeSecurityGroups - - elasticloadbalancing:DescribeLoadBalancers - - elasticloadbalancing:DescribeTags - - elasticloadbalancing:DescribeListeners - - route53:ListHostedZonesByName - - route53:GetHostedZone - - ssm:GetParameter + - Sid: DontReadSecrets + Effect: Deny + Action: + - kms:Decrypt Resource: "*" - Effect: Allow Version: '2012-10-17' - PolicyName: - Fn::Sub: cdk-${Qualifier}-lookup-role-default-policy-${AWS::AccountId}-${AWS::Region} + PolicyName: LookupRolePolicy + Tags: + - Key: aws-cdk:bootstrap-role + Value: lookup FilePublishingRoleDefaultPolicy: Type: AWS::IAM::Policy Properties: @@ -498,7 +490,7 @@ Resources: Type: String Name: Fn::Sub: '/cdk-bootstrap/${Qualifier}/version' - Value: '7' + Value: '8' Outputs: BucketName: Description: The name of the S3 bucket owned by the CDK toolkit stack diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 6c4a2ee64d457..55ca1c6e24903 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -338,17 +338,23 @@ export class CdkToolkit { * all stacks are implicitly selected. * @param toolkitStackName the name to be used for the CDK Toolkit stack. */ - public async bootstrap(environmentSpecs: string[], bootstrapper: Bootstrapper, options: BootstrapEnvironmentOptions): Promise { + public async bootstrap(userEnvironmentSpecs: string[], bootstrapper: Bootstrapper, options: BootstrapEnvironmentOptions): Promise { // If there is an '--app' argument and an environment looks like a glob, we // select the environments from the app. Otherwise use what the user said. // By default glob for everything - environmentSpecs = environmentSpecs.length > 0 ? environmentSpecs : ['**']; + const environmentSpecs = userEnvironmentSpecs.length > 0 ? [...userEnvironmentSpecs] : ['**']; // Partition into globs and non-globs (this will mutate environmentSpecs). const globSpecs = partition(environmentSpecs, looksLikeGlob); if (globSpecs.length > 0 && !this.props.cloudExecutable.hasApp) { - throw new Error(`'${globSpecs}' is not an environment name. Run in app directory to glob or specify an environment name like \'aws://123456789012/us-east-1\'.`); + if (userEnvironmentSpecs.length > 0) { + // User did request this glob + throw new Error(`'${globSpecs}' is not an environment name. Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json' to use wildcards.`); + } else { + // User did not request anything + throw new Error('Specify an environment name like \'aws://123456789012/us-east-1\', or run in a directory with \'cdk.json\'.'); + } } const environments: cxapi.Environment[] = [ diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 547268562516c..71d15e6442fdf 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -41,27 +41,27 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/core": "0.0.0", - "@octokit/rest": "^18.6.0", - "@types/archiver": "^5.1.0", - "@types/fs-extra": "^8.1.1", - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", - "@types/minimatch": "^3.0.4", - "@types/mockery": "^1.4.29", + "@octokit/rest": "^18.6.7", + "@types/archiver": "^5.3.0", + "@types/fs-extra": "^8.1.2", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", + "@types/minimatch": "^3.0.5", + "@types/mockery": "^1.4.30", "@types/node": "^10.17.60", - "@types/promptly": "^3.0.1", - "@types/semver": "^7.3.6", + "@types/promptly": "^3.0.2", + "@types/semver": "^7.3.7", "@types/sinon": "^9.0.11", "@types/table": "^6.0.0", - "@types/uuid": "^8.3.0", + "@types/uuid": "^8.3.1", "@types/wrap-ansi": "^3.0.0", - "@types/yargs": "^15.0.13", + "@types/yargs": "^15.0.14", "aws-sdk-mock": "^5.2.1", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "make-runnable": "^1.3.10", "mockery": "^2.1.0", - "nock": "^13.1.0", + "nock": "^13.1.1", "pkglint": "0.0.0", "sinon": "^9.2.4", "ts-jest": "^26.5.6", diff --git a/packages/awslint/package.json b/packages/awslint/package.json index bb1ed6687f630..da5906149f506 100644 --- a/packages/awslint/package.json +++ b/packages/awslint/package.json @@ -18,22 +18,22 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.30.0", + "@jsii/spec": "^1.31.0", "camelcase": "^6.2.0", "colors": "^1.4.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.30.0", + "jsii-reflect": "^1.31.0", "yargs": "^16.2.0" }, "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", "pkglint": "0.0.0", "typescript": "~3.9.10", - "@typescript-eslint/eslint-plugin": "^4.28.0", - "@typescript-eslint/parser": "^4.28.0", - "eslint": "^7.29.0", + "@typescript-eslint/eslint-plugin": "^4.28.3", + "@typescript-eslint/parser": "^4.28.3", + "eslint": "^7.30.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 2ba132a9149f7..6faafe1d543e2 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -33,14 +33,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/archiver": "^5.1.0", - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", + "@types/archiver": "^5.3.0", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", "@types/jszip": "^3.4.1", "@types/mime": "^2.0.3", - "@types/mock-fs": "^4.13.0", + "@types/mock-fs": "^4.13.1", "@types/node": "^10.17.60", - "@types/yargs": "^15.0.13", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "jszip": "^3.6.0", diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index 14ea4a05f00ed..72b65b88e9070 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -28,11 +28,11 @@ }, "license": "Apache-2.0", "dependencies": { - "codemaker": "^1.30.0", + "codemaker": "^1.31.0", "yaml": "1.10.2" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/yaml": "1.9.7", "jest": "^26.6.3", "typescript": "~3.9.10" diff --git a/packages/decdk/package.json b/packages/decdk/package.json index d3db39216e69b..bb1a848f9c335 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -233,18 +233,18 @@ "@aws-cdk/region-info": "0.0.0", "constructs": "^3.3.69", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.30.0", + "jsii-reflect": "^1.31.0", "jsonschema": "^1.4.0", "yaml": "1.10.2", "yargs": "^16.2.0" }, "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", "@types/yaml": "1.9.7", - "@types/yargs": "^15.0.13", + "@types/yargs": "^15.0.14", "jest": "^26.6.3", - "jsii": "^1.30.0" + "jsii": "^1.31.0" }, "keywords": [ "aws", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index a075a584e92bd..dc27ea678f33c 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -326,7 +326,7 @@ "@aws-cdk/pipelines": "0.0.0", "@aws-cdk/region-info": "0.0.0", "@aws-cdk/yaml-cfn": "0.0.0", - "@types/fs-extra": "^8.1.1", + "@types/fs-extra": "^8.1.2", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "constructs": "^3.3.69", diff --git a/packages/monocdk/rosetta/withRepoAndKinesisStream.ts-fixture b/packages/monocdk/rosetta/withRepoAndKinesisStream.ts-fixture new file mode 100644 index 0000000000000..115e1ece7e254 --- /dev/null +++ b/packages/monocdk/rosetta/withRepoAndKinesisStream.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const stream = new kinesis.Stream(this, 'MyStream'); + + /// here + } +} diff --git a/packages/monocdk/rosetta/withRepoAndSqsQueue.ts-fixture b/packages/monocdk/rosetta/withRepoAndSqsQueue.ts-fixture new file mode 100644 index 0000000000000..98d029d8d8283 --- /dev/null +++ b/packages/monocdk/rosetta/withRepoAndSqsQueue.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const queue = new sqs.Queue(this, 'MyQueue'); + + /// here + } +} diff --git a/packages/monocdk/rosetta/withRepoAndTopic.ts-fixture b/packages/monocdk/rosetta/withRepoAndTopic.ts-fixture new file mode 100644 index 0000000000000..30c1f29cc331b --- /dev/null +++ b/packages/monocdk/rosetta/withRepoAndTopic.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as sns from '@aws-cdk/aws-sns'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const topic = new sns.Topic(this, 'MyTopic'); + + /// here + } +} diff --git a/scripts/best b/scripts/best new file mode 100755 index 0000000000000..e0d540a96a28f --- /dev/null +++ b/scripts/best @@ -0,0 +1,4 @@ +#!/bin/bash +# Run jest with the fail-fast plugin +scriptdir=$(cd $(dirname $0) && pwd) +exec $scriptdir/../tools/cdk-build-tools/node_modules/.bin/jest --setupFilesAfterEnv $scriptdir/jest-fail-fast-setup.js -- "$@" \ No newline at end of file diff --git a/scripts/jest-fail-fast-setup.js b/scripts/jest-fail-fast-setup.js new file mode 100644 index 0000000000000..85a77dd84ff74 --- /dev/null +++ b/scripts/jest-fail-fast-setup.js @@ -0,0 +1,4 @@ +// Run `jest --setupFilesAfterEnv path/to/jest-fail-fast-setup.js --` to stop after the first failing test +// Use the `best` script in this directory for convenience. +const failFast = require('jasmine-fail-fast'); +jasmine.getEnv().addReporter(failFast.init()); \ No newline at end of file diff --git a/scripts/print-construct-tree.py b/scripts/print-construct-tree.py new file mode 100755 index 0000000000000..447c3e66f60e9 --- /dev/null +++ b/scripts/print-construct-tree.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +"""Print the construct tree from a cdk.out directory.""" +import sys +import argparse +from os import path +import json + + +def main(): + dirname = sys.argv[1] + parser = argparse.ArgumentParser(description='Print the construct tree from a cdk.out directory') + parser.add_argument('dir', metavar='DIR', type=str, nargs=1, default='cdk.out', + help='cdk.out directory') + + args = parser.parse_args() + print_tree_file(path.join(args.dir[0], 'tree.json')) + + +def print_tree_file(tree_file_name): + with open(tree_file_name, 'r') as f: + contents = json.load(f) + print_tree(contents) + + +def print_tree(tree_file): + print_node(tree_file['tree']) + + +def print_node(node, prefix_here='', prefix_children=''): + info = [] + cfn_type = node.get('attributes', {}).get('aws:cdk:cloudformation:type') + if cfn_type: + info.append(cfn_type) + + print(prefix_here + node['id'] + ((' (' + ', '.join(info) + ')') if info else '')) + children = list(node.get('children', {}).values()) + for i, child in enumerate(children): + if i < len(children) - 1: + print_node(child, prefix_children + ' ├─ ', prefix_children + ' │ ') + else: + print_node(child, prefix_children + ' └─ ', prefix_children + ' ') + + +if __name__ == '__main__': + main() diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 2eeede55c8548..3443058c4ea6b 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -35,18 +35,18 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/yargs": "^15.0.13", - "@types/semver": "^7.3.6", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", + "@types/semver": "^7.3.7", "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^4.28.0", - "@typescript-eslint/parser": "^4.28.0", + "@typescript-eslint/eslint-plugin": "^4.28.3", + "@typescript-eslint/parser": "^4.28.3", "awslint": "0.0.0", "colors": "^1.4.0", - "eslint": "^7.29.0", + "eslint": "^7.30.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", @@ -55,9 +55,9 @@ "fs-extra": "^9.1.0", "jest": "^26.6.3", "jest-junit": "^11.1.0", - "jsii": "^1.30.0", - "jsii-pacmak": "^1.30.0", - "jsii-reflect": "^1.30.0", + "jsii": "^1.31.0", + "jsii-pacmak": "^1.31.0", + "jsii-reflect": "^1.31.0", "markdownlint-cli": "^0.27.1", "nodeunit": "^0.11.3", "nyc": "^15.1.0", diff --git a/tools/cdk-integ-tools/package.json b/tools/cdk-integ-tools/package.json index a7aeb12c4cb92..6491ed0c2cec0 100644 --- a/tools/cdk-integ-tools/package.json +++ b/tools/cdk-integ-tools/package.json @@ -31,8 +31,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/tools/cdk-release/package.json b/tools/cdk-release/package.json index 3608c4f212378..1f69eefdebddc 100644 --- a/tools/cdk-release/package.json +++ b/tools/cdk-release/package.json @@ -28,9 +28,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0" diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index 2aa13012c7cc8..0caf46a5aa4fe 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -32,15 +32,15 @@ "license": "Apache-2.0", "dependencies": { "@aws-cdk/cfnspec": "0.0.0", - "codemaker": "^1.30.0", + "codemaker": "^1.31.0", "fast-json-patch": "^3.0.0-1", "fs-extra": "^9.1.0", "yargs": "^16.2.0" }, "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0" diff --git a/tools/eslint-plugin-cdk/package.json b/tools/eslint-plugin-cdk/package.json index 3d05756fc8c3b..b075f070208d0 100644 --- a/tools/eslint-plugin-cdk/package.json +++ b/tools/eslint-plugin-cdk/package.json @@ -14,9 +14,9 @@ "build+extract": "npm run build" }, "devDependencies": { - "@types/eslint": "^7.2.13", - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", + "@types/eslint": "^7.28.0", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "@types/estree": "*", "eslint-plugin-rulesdir": "^0.2.0", @@ -24,8 +24,8 @@ "typescript": "~3.9.10" }, "dependencies": { - "@typescript-eslint/parser": "^4.28.0", - "eslint": "^7.29.0", + "@typescript-eslint/parser": "^4.28.3", + "eslint": "^7.30.0", "fs-extra": "^9.1.0" }, "jest": { diff --git a/tools/nodeunit-shim/package.json b/tools/nodeunit-shim/package.json index 8bcd8ba63b77a..2c2814cc08a08 100644 --- a/tools/nodeunit-shim/package.json +++ b/tools/nodeunit-shim/package.json @@ -14,7 +14,7 @@ "build+extract": "npm run build" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "typescript": "~3.9.10" }, diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index 157f5faabddb4..9f6f3446c31b3 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -37,14 +37,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", - "@types/semver": "^7.3.6", - "@types/yargs": "^15.0.13", - "@typescript-eslint/eslint-plugin": "^4.28.0", - "@typescript-eslint/parser": "^4.28.0", - "eslint": "^7.29.0", + "@types/fs-extra": "^8.1.2", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", + "@types/semver": "^7.3.7", + "@types/yargs": "^15.0.14", + "@typescript-eslint/eslint-plugin": "^4.28.3", + "@typescript-eslint/parser": "^4.28.3", + "eslint": "^7.30.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", diff --git a/tools/pkgtools/package.json b/tools/pkgtools/package.json index c7550c41d272a..cf03f974a879a 100644 --- a/tools/pkgtools/package.json +++ b/tools/pkgtools/package.json @@ -31,8 +31,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/tools/prlint/package.json b/tools/prlint/package.json index b850329eb15e6..88c3d70fb7393 100644 --- a/tools/prlint/package.json +++ b/tools/prlint/package.json @@ -19,9 +19,9 @@ "glob": "^7.1.7" }, "devDependencies": { - "@types/fs-extra": "^9.0.11", - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", + "@types/fs-extra": "^9.0.12", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", "jest": "^26.6.3", "make-runnable": "^1.3.10", "typescript": "~3.9.10" diff --git a/tools/ubergen/package.json b/tools/ubergen/package.json index 0a15079209468..8cd1057e51ffd 100644 --- a/tools/ubergen/package.json +++ b/tools/ubergen/package.json @@ -29,7 +29,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", + "@types/fs-extra": "^8.1.2", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/tools/yarn-cling/package.json b/tools/yarn-cling/package.json index 248222d13cf0a..52501bf50f83e 100644 --- a/tools/yarn-cling/package.json +++ b/tools/yarn-cling/package.json @@ -40,10 +40,10 @@ ] }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", - "@types/yarnpkg__lockfile": "^1.1.4", - "@types/semver": "^7.3.6", + "@types/yarnpkg__lockfile": "^1.1.5", + "@types/semver": "^7.3.7", "jest": "^26.6.3", "pkglint": "0.0.0", "typescript": "~3.9.10" diff --git a/version.v1.json b/version.v1.json index edaba2d69702e..b2aee66739e48 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.113.0" + "version": "1.114.0" } diff --git a/yarn.lock b/yarn.lock index c1e70ddfc3533..51786b613db78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -351,6 +351,25 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" + integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + +"@hutson/parse-repository-url@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" + integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -538,10 +557,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jsii/spec@^1.30.0": - version "1.30.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.30.0.tgz#e5b2381b2be0b9c0839190f9f45d0a038654c73d" - integrity sha512-oXIwvZyHHc/TrwA/3pzQ3gkqBe916EWBvaexNI3rnKZujlHZT4vVVHMCjQ/kUJhcR0GEaahvwlNhiPTu6roC2g== +"@jsii/spec@^1.31.0": + version "1.31.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.31.0.tgz#9298dc163fdae0bab4006b817592235a29922871" + integrity sha512-qpJqZ+xj4lnKfk/HJYdYURDmHzh9aBIVOTgwd314AxKmwubDAajlAup+D2F9z9kylAB7GsQiva/SXgUlFjBeQw== dependencies: jsonschema "^1.4.0" @@ -1017,7 +1036,7 @@ npmlog "^4.1.2" upath "^2.0.1" -"@lerna/project@4.0.0": +"@lerna/project@4.0.0", "@lerna/project@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@lerna/project/-/project-4.0.0.tgz#ff84893935833533a74deff30c0e64ddb7f0ba6b" integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== @@ -1230,9 +1249,9 @@ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz#94c23db18ee4653e129abd26fb06f870ac9e1ee2" - integrity sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA== + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" @@ -1242,7 +1261,7 @@ resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a" integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q== -"@npmcli/git@^2.0.1": +"@npmcli/git@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== @@ -1333,10 +1352,10 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^7.3.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-7.3.2.tgz#065ce49b338043ec7f741316ce06afd4d459d944" - integrity sha512-oJhK/yhl9Gt430OrZOzAl2wJqR0No9445vmZ9Ey8GjUZUpwuu/vmEFP0TDhDXdpGDoxD6/EIFHJEcY8nHXpDTA== +"@octokit/openapi-types@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-8.3.0.tgz#8bc912edae8c03e002882cf1e29b595b7da9b441" + integrity sha512-ZFyQ30tNpoATI7o+Z9MWFUzUgWisB8yduhcky7S4UYsRijgIGSnwUKzPBDGzf/Xkx1DuvUtqzvmuFlDSqPJqmQ== "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" @@ -1351,11 +1370,11 @@ "@octokit/types" "^2.0.1" "@octokit/plugin-paginate-rest@^2.6.2": - version "2.13.5" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.5.tgz#e459f9b5dccbe0a53f039a355d5b80c0a2b0dc57" - integrity sha512-3WSAKBLa1RaR/7GG+LQR/tAZ9fp9H9waE9aPXallidyci9oZsfgsLn5M836d3LuDC6Fcym+2idRTBpssHZePVg== + version "2.14.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.14.0.tgz#f469cb4a908792fb44679c5973d8bba820c88b0f" + integrity sha512-S2uEu2uHeI7Vf+Lvj8tv3O5/5TCAa8GHS0dUQN7gdM7vKA6ZHAbR6HkAVm5yMb1mbedLEbxOuQ+Fa0SQ7tCDLA== dependencies: - "@octokit/types" "^6.13.0" + "@octokit/types" "^6.18.0" "@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.2": version "1.0.4" @@ -1370,12 +1389,12 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.3.1.tgz#deddce769b4ec3179170709ab42e4e9e6195aaa9" - integrity sha512-3B2iguGmkh6bQQaVOtCsS0gixrz8Lg0v4JuXPqBcFqLKuJtxAUf3K88RxMEf/naDOI73spD+goJ/o7Ie7Cvdjg== +"@octokit/plugin-rest-endpoint-methods@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.4.1.tgz#540ec90bb753dcaa682ee9f2cd6efdde9132fa90" + integrity sha512-Nx0g7I5ayAYghsLJP4Q1Ch2W9jYYM0FlWWWZocUro8rNxVwuZXGfFd7Rcqi9XDWepSXjg1WByiNJnZza2hIOvQ== dependencies: - "@octokit/types" "^6.16.2" + "@octokit/types" "^6.18.1" deprecation "^2.3.1" "@octokit/request-error@^1.0.2": @@ -1430,15 +1449,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^18.1.0", "@octokit/rest@^18.6.0": - version "18.6.0" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.6.0.tgz#9a8457374c78c2773d3ab3f50aaffc62f3ed4f76" - integrity sha512-MdHuXHDJM7e5sUBe3K9tt7th0cs4csKU5Bb52LRi2oHAeIMrMZ4XqaTrEv660HoUPoM1iDlnj27Ab/Nh3MtwlA== +"@octokit/rest@^18.1.0", "@octokit/rest@^18.6.7": + version "18.6.7" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.6.7.tgz#89b8ecd13edd9603f00453640d1fb0b4175d4b31" + integrity sha512-Kn6WrI2ZvmAztdx+HEaf88RuJn+LK72S8g6OpciE4kbZddAN84fu4fiPGxcEu052WmqKVnA/cnQsbNlrYC6rqQ== dependencies: "@octokit/core" "^3.5.0" "@octokit/plugin-paginate-rest" "^2.6.2" "@octokit/plugin-request-log" "^1.0.2" - "@octokit/plugin-rest-endpoint-methods" "5.3.1" + "@octokit/plugin-rest-endpoint-methods" "5.4.1" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -1447,12 +1466,12 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^6.0.3", "@octokit/types@^6.13.0", "@octokit/types@^6.16.1", "@octokit/types@^6.16.2": - version "6.16.4" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.16.4.tgz#d24f5e1bacd2fe96d61854b5bda0e88cf8288dfe" - integrity sha512-UxhWCdSzloULfUyamfOg4dJxV9B+XjgrIZscI0VCbp4eNrjmorGEw+4qdwcpTsu6DIrm9tQsFQS2pK5QkqQ04A== +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.18.0", "@octokit/types@^6.18.1": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.19.0.tgz#e2b6fedb10c8b53cf4574aa5d1a8a5611295297a" + integrity sha512-9wdZFiJfonDyU6DjIgDHxAIn92vdSUBOwAXbO2F9rOFt6DJwuAkyGLu1CvdJPphCbPBoV9iSDMX7y4fu0v6AtA== dependencies: - "@octokit/openapi-types" "^7.3.2" + "@octokit/openapi-types" "^8.3.0" "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1", "@sinonjs/commons@^1.8.3": version "1.8.3" @@ -1503,22 +1522,22 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@types/archiver@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.1.0.tgz#869f4ce4028e49cf9a0243cf914415f4cc3d1f3d" - integrity sha512-baFOhanb/hxmcOd1Uey2TfFg43kTSmM6py1Eo7Rjbv/ivcl7PXLhY0QgXGf50Hx/eskGCFqPfhs/7IZLb15C5g== +"@types/archiver@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.0.tgz#2b34ba56d4d7102d256b922c7e91e09eab79db6f" + integrity sha512-qJ79qsmq7O/k9FYwsF6O1xVA1PeLV+9Bh3TYkVCu3VzMR6vN9JQkgEOh/rrQ0R+F4Ta+R3thHGewxQtFglwVfg== dependencies: "@types/glob" "*" -"@types/aws-lambda@^8.10.77": - version "8.10.77" - resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.77.tgz#04c4e3a06ab5552f2fa80816f8adca54b6bb9671" - integrity sha512-n0EMFJU/7u3KvHrR83l/zrKOVURXl5pUJPNED/Bzjah89QKCHwCiKCBoVUXRwTGRfCYGIDdinJaAlKDHZdp/Ng== +"@types/aws-lambda@^8.10.78": + version "8.10.78" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.78.tgz#dbb509837b6082962d6e7bc19f814e067ac9f5a2" + integrity sha512-+lZ8NuHT0qKEEpiZR4bF1G24SLrLwzdu0i9Cjdc3BGq6XJU6gBBYS5I0RJ8RdDCtgqgGdW8sOwsiZGHrC6mp0Q== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": - version "7.1.14" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" - integrity sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g== + version "7.1.15" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024" + integrity sha512-bxlMKPDbY8x5h6HBwVzEOk2C8fb6SLfYQ5Jw3uBYuYF1lfWk/kbLd81la82vrIkBb0l+JdmrZaDikPrNxpS/Ew== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -1527,58 +1546,58 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" - integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ== + version "7.6.3" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.3.tgz#f456b4b2ce79137f768aa130d2423d2f0ccfaba5" + integrity sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.0.tgz#0c888dd70b3ee9eebb6e4f200e809da0076262be" - integrity sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A== + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.11.1" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.1.tgz#654f6c4f67568e24c23b367e947098c6206fa639" - integrity sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw== + version "7.14.2" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" + integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== dependencies: "@babel/types" "^7.3.0" -"@types/eslint@^7.2.13": - version "7.2.13" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.13.tgz#e0ca7219ba5ded402062ad6f926d491ebb29dd53" - integrity sha512-LKmQCWAlnVHvvXq4oasNUMTJJb2GwSyTY8+1C7OH5ILR8mPLaljv1jxL1bXW3xB3jFbQxTKxJAvI8PyjB09aBg== +"@types/eslint@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a" + integrity sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A== dependencies: "@types/estree" "*" "@types/json-schema" "*" "@types/estree@*": - version "0.0.48" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.48.tgz#18dc8091b285df90db2f25aa7d906cfc394b7f74" - integrity sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew== + version "0.0.50" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== -"@types/fs-extra@^8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" - integrity sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w== +"@types/fs-extra@^8.1.2": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.2.tgz#7125cc2e4bdd9bd2fc83005ffdb1d0ba00cca61f" + integrity sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg== dependencies: "@types/node" "*" -"@types/fs-extra@^9.0.11": - version "9.0.11" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.11.tgz#8cc99e103499eab9f347dbc6ca4e99fb8d2c2b87" - integrity sha512-mZsifGG4QeQ7hlkhO56u7zt/ycBgGxSVsFI/6lGTU34VtwkiqrrSDgw0+ygs8kFGWcXnFQWMrzF2h7TtDFNixA== +"@types/fs-extra@^9.0.12": + version "9.0.12" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.12.tgz#9b8f27973df8a7a3920e8461517ebf8a7d4fdfaf" + integrity sha512-I+bsBr67CurCGnSenZZ7v94gd3tc3+Aj2taxMT4yu4ABLuOgOjeFxX3dokG24ztSRg5tnT00sL8BszO7gSMoIw== dependencies: "@types/node" "*" -"@types/glob@*", "@types/glob@^7.1.3": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== +"@types/glob@*", "@types/glob@^7.1.4": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" + integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA== dependencies: "@types/minimatch" "*" "@types/node" "*" @@ -1609,23 +1628,18 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^26.0.22", "@types/jest@^26.0.23": - version "26.0.23" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7" - integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA== +"@types/jest@^26.0.24": + version "26.0.24" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a" + integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w== dependencies: jest-diff "^26.0.0" pretty-format "^26.0.0" "@types/json-schema@*", "@types/json-schema@^7.0.7": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + version "7.0.8" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818" + integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg== "@types/jszip@^3.4.1": version "3.4.1" @@ -1634,15 +1648,15 @@ dependencies: jszip "*" -"@types/lodash@^4.14.170": - version "4.14.170" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" - integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== +"@types/lodash@^4.14.171": + version "4.14.171" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.171.tgz#f01b3a5fe3499e34b622c362a46a609fdb23573b" + integrity sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg== -"@types/md5@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.0.tgz#3b6a623091160f4dc75be3173e25f2110dc3fa1f" - integrity sha512-556YJ7ejzxIqSSxzyGGpctuZOarNZJt/zlEkhmmDc1f/slOEANHuwu2ZX7YaZ40rMiWoxt8GvAhoDpW1cmSy6A== +"@types/md5@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.1.tgz#010bcf3bb50a2cff3a574cb1c0b4051a9c67d6bc" + integrity sha512-OK3oe+ALIoPSo262lnhAYwpqFNXbiwH2a+0+Z5YBnkQEwWD8fk5+PIeRhYA48PzvX9I4SGNpWy+9bLj8qz92RQ== dependencies: "@types/node" "*" @@ -1651,32 +1665,32 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== -"@types/minimatch@*", "@types/minimatch@^3.0.3", "@types/minimatch@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" - integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== +"@types/minimatch@*", "@types/minimatch@^3.0.3", "@types/minimatch@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== "@types/minimist@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" - integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/mock-fs@^4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.0.tgz#b8b01cd2db588668b2532ecd21b1babd3fffb2c0" - integrity sha512-FUqxhURwqFtFBCuUj3uQMp7rPSQs//b3O9XecAVxhqS9y4/W8SIJEZFq2mmpnFVZBXwR/2OyPLE97CpyYiB8Mw== +"@types/mock-fs@^4.13.1": + version "4.13.1" + resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.1.tgz#9201554ceb23671badbfa8ac3f1fa9e0706305be" + integrity sha512-m6nFAJ3lBSnqbvDZioawRvpLXSaPyn52Srf7OfzjubYbYX8MTUdIgDxQl0wEapm4m/pNYSd9TXocpQ0TvZFlYA== dependencies: "@types/node" "*" -"@types/mockery@^1.4.29": - version "1.4.29" - resolved "https://registry.yarnpkg.com/@types/mockery/-/mockery-1.4.29.tgz#9ba22df37f07e3780fff8531d1a38e633f9457a5" - integrity sha1-m6It838H43gP/4Ux0aOOYz+UV6U= +"@types/mockery@^1.4.30": + version "1.4.30" + resolved "https://registry.yarnpkg.com/@types/mockery/-/mockery-1.4.30.tgz#25f07fa7340371c7ee0fb9239511a34e0a19d5b7" + integrity sha512-uv53RrNdhbkV/3VmVCtfImfYCWC3GTTRn3R11Whni3EJ+gb178tkZBVNj2edLY5CMrB749dQi+SJkg87jsN8UQ== "@types/node@*", "@types/node@>= 8": - version "15.12.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26" - integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA== + version "16.3.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.3.2.tgz#655432817f83b51ac869c2d51dd8305fb8342e16" + integrity sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw== "@types/node@^10.17.60": version "10.17.60" @@ -1684,19 +1698,19 @@ integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== "@types/node@^14.14.33": - version "14.17.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.4.tgz#218712242446fc868d0e007af29a4408c7765bc0" - integrity sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A== + version "14.17.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.5.tgz#b59daf6a7ffa461b5648456ca59050ba8e40ed54" + integrity sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA== -"@types/nodeunit@^0.0.31": - version "0.0.31" - resolved "https://registry.yarnpkg.com/@types/nodeunit/-/nodeunit-0.0.31.tgz#67eb52ad22326c7d1d9febe99d553f33b166126d" - integrity sha512-gZvDnqhHmp2IFzvQ59VJioI84/A+FZxGbp3OqoGhvQRfFQgbCqnK+SsYMWKfXODHpJfDbTnjvgoD+xeW05fQjg== +"@types/nodeunit@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/nodeunit/-/nodeunit-0.0.32.tgz#a41a76b0da07a2a79882e613f4b9fb4c4d123cc1" + integrity sha512-9n61KESiLGaKPpgp6ccSkpx0HVPe+ZNqxVdLMF2BaiQfbJIi0HIwboxmE3OxwgYqH7xsjuk/iCpSE4VVqC4w+Q== "@types/normalize-package-data@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" - integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== "@types/parse-json@^4.0.0": version "4.0.0" @@ -1704,14 +1718,14 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prettier@^2.0.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.0.tgz#2e8332cc7363f887d32ec5496b207d26ba8052bb" - integrity sha512-hkc1DATxFLQo4VxPDpMH1gCkPpBbpOoJ/4nhuXw4n63/0R6bCpQECj4+K226UJ4JO/eJQz+1mC2I7JsWanAdQw== + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.2.tgz#fc8c2825e4ed2142473b4a81064e6e081463d1b3" + integrity sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog== -"@types/promptly@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/promptly/-/promptly-3.0.1.tgz#206e29ebe55e2360f3e96067d4563efc8c29d8c7" - integrity sha512-NZkHlbRnB3ktYY9+dG38OpvXc04+eDMyFxiAr/LMLjD0bbDY9pW3HBctrXxLZUH0Tq6BkxWB6aMJJvaxQX36oA== +"@types/promptly@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/promptly/-/promptly-3.0.2.tgz#598674d4b78b3dffcb2d756b344f28a2cf7459f8" + integrity sha512-cJFwE7d8GlraY+DJoZ0NhpoJ55slkcbNsGIKMY0H+5h0xaGqXBqXz9zeu+Ey9KfN1UiHQXiIT0GroxyPYMPP/w== dependencies: "@types/node" "*" @@ -1725,10 +1739,10 @@ resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83" integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g== -"@types/semver@^7.3.6": - version "7.3.6" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.6.tgz#e9831776f4512a7ba6da53e71c26e5fb67882d63" - integrity sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw== +"@types/semver@^7.3.7": + version "7.3.7" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.7.tgz#b9eb89d7dfa70d5d1ce525bc1411a35347f533a3" + integrity sha512-4g1jrL98mdOIwSOUh6LTlB0Cs9I0dQPwINUhBg7C6pN4HLr8GS8xsksJxilW6S6dQHVi2K/o+lQuQcg7LroCnw== "@types/sinon@^9.0.11": version "9.0.11" @@ -1738,14 +1752,14 @@ "@types/sinonjs__fake-timers" "*" "@types/sinonjs__fake-timers@*": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" - integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.3.tgz#79df6f358ae8f79e628fe35a63608a0ea8e7cf08" + integrity sha512-E1dU4fzC9wN2QK2Cr1MLCfyHM8BoNnRFvuf45LYMPNDA+WqbNzC45S4UzPxvp1fFJ1rvSGU0bPvdd35VLmXG8g== "@types/stack-utils@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" - integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== "@types/string-width@^4.0.1": version "4.0.1" @@ -1761,10 +1775,10 @@ dependencies: table "*" -"@types/uuid@^8.3.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" - integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== +"@types/uuid@^8.3.1": + version "8.3.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" + integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== "@types/wrap-ansi@^3.0.0": version "3.0.0" @@ -1786,89 +1800,89 @@ yaml "*" "@types/yargs-parser@*": - version "20.2.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" - integrity sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA== + version "20.2.1" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" + integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== -"@types/yargs@^15.0.0", "@types/yargs@^15.0.13": - version "15.0.13" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.13.tgz#34f7fec8b389d7f3c1fd08026a5763e072d3c6dc" - integrity sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ== +"@types/yargs@^15.0.0", "@types/yargs@^15.0.14": + version "15.0.14" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" + integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== dependencies: "@types/yargs-parser" "*" -"@types/yarnpkg__lockfile@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" - integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== +"@types/yarnpkg__lockfile@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.5.tgz#9639020e1fb65120a2f4387db8f1e8b63efdf229" + integrity sha512-8NYnGOctzsI4W0ApsP/BIHD/LnxpJ6XaGf2AZmz4EyDYJMxtprN4279dLNI1CPZcwC9H18qYcaFv4bXi0wmokg== -"@typescript-eslint/eslint-plugin@^4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz#1a66f03b264844387beb7dc85e1f1d403bd1803f" - integrity sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ== +"@typescript-eslint/eslint-plugin@^4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.3.tgz#36cdcd9ca6f9e5cb49b9f61b970b1976708d084b" + integrity sha512-jW8sEFu1ZeaV8xzwsfi6Vgtty2jf7/lJmQmDkDruBjYAbx5DA8JtbcMnP0rNPUG+oH5GoQBTSp+9613BzuIpYg== dependencies: - "@typescript-eslint/experimental-utils" "4.28.0" - "@typescript-eslint/scope-manager" "4.28.0" + "@typescript-eslint/experimental-utils" "4.28.3" + "@typescript-eslint/scope-manager" "4.28.3" debug "^4.3.1" functional-red-black-tree "^1.0.1" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.28.0", "@typescript-eslint/experimental-utils@^4.0.1": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz#13167ed991320684bdc23588135ae62115b30ee0" - integrity sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ== +"@typescript-eslint/experimental-utils@4.28.3", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.3.tgz#976f8c1191b37105fd06658ed57ddfee4be361ca" + integrity sha512-zZYl9TnrxwEPi3FbyeX0ZnE8Hp7j3OCR+ELoUfbwGHGxWnHg9+OqSmkw2MoCVpZksPCZYpQzC559Ee9pJNHTQw== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.28.0" - "@typescript-eslint/types" "4.28.0" - "@typescript-eslint/typescript-estree" "4.28.0" + "@typescript-eslint/scope-manager" "4.28.3" + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/typescript-estree" "4.28.3" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa" - integrity sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A== +"@typescript-eslint/parser@^4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.3.tgz#95f1d475c08268edffdcb2779993c488b6434b44" + integrity sha512-ZyWEn34bJexn/JNYvLQab0Mo5e+qqQNhknxmc8azgNd4XqspVYR5oHq9O11fLwdZMRcj4by15ghSlIEq+H5ltQ== dependencies: - "@typescript-eslint/scope-manager" "4.28.0" - "@typescript-eslint/types" "4.28.0" - "@typescript-eslint/typescript-estree" "4.28.0" + "@typescript-eslint/scope-manager" "4.28.3" + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/typescript-estree" "4.28.3" debug "^4.3.1" -"@typescript-eslint/scope-manager@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz#6a3009d2ab64a30fc8a1e257a1a320067f36a0ce" - integrity sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg== +"@typescript-eslint/scope-manager@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.3.tgz#c32ad4491b3726db1ba34030b59ea922c214e371" + integrity sha512-/8lMisZ5NGIzGtJB+QizQ5eX4Xd8uxedFfMBXOKuJGP0oaBBVEMbJVddQKDXyyB0bPlmt8i6bHV89KbwOelJiQ== dependencies: - "@typescript-eslint/types" "4.28.0" - "@typescript-eslint/visitor-keys" "4.28.0" + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/visitor-keys" "4.28.3" -"@typescript-eslint/types@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0" - integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA== +"@typescript-eslint/types@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.3.tgz#8fffd436a3bada422c2c1da56060a0566a9506c7" + integrity sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA== -"@typescript-eslint/typescript-estree@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf" - integrity sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ== +"@typescript-eslint/typescript-estree@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz#253d7088100b2a38aefe3c8dd7bd1f8232ec46fb" + integrity sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w== dependencies: - "@typescript-eslint/types" "4.28.0" - "@typescript-eslint/visitor-keys" "4.28.0" + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/visitor-keys" "4.28.3" debug "^4.3.1" globby "^11.0.3" is-glob "^4.0.1" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434" - integrity sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw== +"@typescript-eslint/visitor-keys@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz#26ac91e84b23529968361045829da80a4e5251c4" + integrity sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg== dependencies: - "@typescript-eslint/types" "4.28.0" + "@typescript-eslint/types" "4.28.3" eslint-visitor-keys "^2.0.0" "@yarnpkg/lockfile@^1.1.0": @@ -1903,9 +1917,9 @@ acorn-globals@^6.0.0: acorn-walk "^7.1.1" acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^7.1.1: version "7.2.0" @@ -1918,9 +1932,9 @@ acorn@^7.1.1, acorn@^7.4.0: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.2.4: - version "8.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.0.tgz#af53266e698d7cffa416714b503066a82221be60" - integrity sha512-ULr0LDaEqQrMFGyQ3bhJkLsbtrQ8QibAseGZeaSUiT/6zb9IvIkomWHJIvgvwad+hinRAgsI51JcWk2yvwyL+w== + version "8.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" + integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== add-stream@^1.0.0: version "1.0.0" @@ -1962,9 +1976,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: uri-js "^4.2.2" ajv@^8.0.1: - version "8.6.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.0.tgz#60cc45d9c46a477d80d92c48076d972c342e5720" - integrity sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ== + version "8.6.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.1.tgz#ae65764bf1edde8cd861281cda5057852364a295" + integrity sha512-42VLtQUOLefAvKFAQIxIZDaThq6om/PrfP0CYk3/vn+y4BMNkKnbli8ON2QCiHov4KkzOSJ/xSoBJdayiiYvVQ== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -2136,11 +2150,6 @@ array-differ@^3.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -2260,9 +2269,9 @@ aws-sdk-mock@^5.2.1: traverse "^0.6.6" aws-sdk@^2.596.0, aws-sdk@^2.848.0, aws-sdk@^2.928.0: - version "2.932.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.932.0.tgz#43da32ab6de58a0eac6c7976feb6c9879fe09e7c" - integrity sha512-U6MWUtFD0npWa+ReVEgm0fCIM0fMOYahFp14GLv8fC+BWOTvh5Iwt/gF8NrLomx42bBjA1Abaw6yhmiaSJDQHQ== + version "2.945.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.945.0.tgz#ebd90832a664a192b12edf755af31be70dc18909" + integrity sha512-tkcoFAUol7c+9ZBnXsBTKfsj9bNckJ7uzj7FdD/a8AMt/6/18LlEISCiuHFl9qr8MItcON7UgnphJdFCTV7zBw== dependencies: buffer "4.9.2" events "1.1.1" @@ -2590,14 +2599,6 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -2607,11 +2608,6 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -2623,9 +2619,9 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001219: - version "1.0.30001239" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz#66e8669985bb2cb84ccb10f68c25ce6dd3e4d2b8" - integrity sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ== + version "1.0.30001245" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz#45b941bbd833cb0fa53861ff2bae746b3c6ca5d4" + integrity sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA== capture-exit@^2.0.0: version "2.0.0" @@ -2814,10 +2810,10 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= -codemaker@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.30.0.tgz#c718a5178e5bdd06d6ab2ddb629edc64de80cb51" - integrity sha512-yntR55JhhVlZTfR4CPV6IrCULovPDrk3z0yQR7/ygEtNxEOQrHhX17djJ0rVmIwCJUawv+ODTJ1ipJY9CbxJQw== +codemaker@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.31.0.tgz#1987d8d2dcb39883844134d50c85f33f29f0cb62" + integrity sha512-gyWhtZ4YU5b+pIijCfOZkGrH0DCkUQXyRG3BQtDlnwFJuXyJnDoz+dpM5ErkJuDD9w6Qns4aryyG/bU78huaSg== dependencies: camelcase "^6.2.0" decamelize "^5.0.0" @@ -2962,9 +2958,9 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= constructs@^3.3.69: - version "3.3.87" - resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.87.tgz#374d8660c6a40148af6d9c0125918405ab1366f1" - integrity sha512-/SQrY1RP9KQsSyyVeyBTSMIiC5yOJO635YVqPH6XBsS5MMkBHd4gsPOX4FkpkDLMC1shAoK0FSTLatgxFiS4+A== + version "3.3.97" + resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.97.tgz#751cc8955ee29381da9ee05f39141f02d6164aee" + integrity sha512-KDemmmUBgTDd2OKVOZkVEJM1LwP/bzm+cs2l/v1UYctIUl2X4LW+MrK7Ajd8blKkS5Vp6edkQSTSHUJnR/413w== conventional-changelog-angular@^5.0.12: version "5.0.12" @@ -2999,7 +2995,7 @@ conventional-changelog-codemirror@^2.0.8: dependencies: q "^1.5.1" -conventional-changelog-config-spec@2.1.0: +conventional-changelog-config-spec@2.1.0, conventional-changelog-config-spec@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz#874a635287ef8b581fd8558532bf655d4fb59f2d" integrity sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ== @@ -3023,15 +3019,15 @@ conventional-changelog-conventionalcommits@^4.5.0: q "^1.5.1" conventional-changelog-core@^4.2.1, conventional-changelog-core@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.2.tgz#f0897df6d53b5d63dec36b9442bd45354f8b3ce5" - integrity sha512-7pDpRUiobQDNkwHyJG7k9f6maPo9tfPzkSWbRq97GGiZqisElhnvUZSvyQH20ogfOjntB5aadvv6NNcKL1sReg== + version "4.2.3" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.3.tgz#ce44d4bbba4032e3dc14c00fcd5b53fc00b66433" + integrity sha512-MwnZjIoMRL3jtPH5GywVNqetGILC7g6RQFvdb8LRU/fA/338JbeWAku3PZ8yQ+mtVRViiISqJlb0sOz0htBZig== dependencies: add-stream "^1.0.0" - conventional-changelog-writer "^4.0.18" + conventional-changelog-writer "^5.0.0" conventional-commits-parser "^3.2.0" dateformat "^3.0.0" - get-pkg-repo "^1.0.0" + get-pkg-repo "^4.0.0" git-raw-commits "^2.0.8" git-remote-origin-url "^2.0.0" git-semver-tags "^4.1.1" @@ -3040,7 +3036,6 @@ conventional-changelog-core@^4.2.1, conventional-changelog-core@^4.2.2: q "^1.5.1" read-pkg "^3.0.0" read-pkg-up "^3.0.0" - shelljs "^0.8.3" through2 "^4.0.0" conventional-changelog-ember@^2.0.9: @@ -3084,7 +3079,7 @@ conventional-changelog-preset-loader@^2.3.4: resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== -conventional-changelog-writer@^4.0.18: +conventional-changelog-writer@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" integrity sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw== @@ -3100,6 +3095,21 @@ conventional-changelog-writer@^4.0.18: split "^1.0.0" through2 "^4.0.0" +conventional-changelog-writer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz#c4042f3f1542f2f41d7d2e0d6cad23aba8df8eec" + integrity sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g== + dependencies: + conventional-commits-filter "^2.0.7" + dateformat "^3.0.0" + handlebars "^4.7.6" + json-stringify-safe "^5.0.1" + lodash "^4.17.15" + meow "^8.0.0" + semver "^6.0.0" + split "^1.0.0" + through2 "^4.0.0" + conventional-changelog@3.1.24, conventional-changelog@^3.1.24: version "3.1.24" resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.24.tgz#ebd180b0fd1b2e1f0095c4b04fd088698348a464" @@ -3181,9 +3191,9 @@ cosmiconfig@^7.0.0: yaml "^1.10.0" coveralls@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.1.0.tgz#13c754d5e7a2dd8b44fe5269e21ca394fb4d615b" - integrity sha512-sHxOu2ELzW8/NC1UP5XVLbZDzO4S3VxfFye3XYCznopHy02YjNkHcj5bKaVw2O7hVaBdBjEdQGpie4II1mWhuQ== + version "3.1.1" + resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.1.1.tgz#f5d4431d8b5ae69c5079c8f8ca00d64ac77cf081" + integrity sha512-+dxnG2NHncSD1NrqbSM3dn/lE57O6Qf/koe9+I7c+wzkqRmEvcp0kgJdxKInzYzkICKkFMZsX3Vct3++tsF9ww== dependencies: js-yaml "^3.13.1" lcov-parse "^1.0.0" @@ -3273,13 +3283,6 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -3322,9 +3325,9 @@ dateformat@^3.0.0: integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" @@ -3355,7 +3358,7 @@ decamelize-keys@^1.1.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.0, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -3366,9 +3369,9 @@ decamelize@^5.0.0: integrity sha512-U75DcT5hrio3KNtvdULAWnLiAPbFUC4191ldxMmj4FA/mRuBnmDwU0boNfPyFRhnan+Jm+haLeSn3P0afcBn4w== decimal.js@^10.2.1: - version "10.3.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.0.tgz#96fd481189818e0d5810c18ac147824b9e4c0026" - integrity sha512-MrQRs2gyD//7NeHi9TtsfClkf+cFAewDz+PZHR8ILKglLmBMyVX3ymQ+oeznE3tjrS7beTN+6JXb2C3JDHm7ug== + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== decode-uri-component@^0.2.0: version "0.2.0" @@ -3505,7 +3508,7 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= -detect-indent@^6.0.0: +detect-indent@^6.0.0, detect-indent@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== @@ -3641,9 +3644,9 @@ ejs@^2.5.2: integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== electron-to-chromium@^1.3.723: - version "1.3.755" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.755.tgz#4b6101f13de910cf3f0a1789ddc57328133b9332" - integrity sha512-BJ1s/kuUuOeo1bF/EM2E4yqW9te0Hpof3wgwBx40AWJE18zsD1Tqo0kr7ijnOc+lRsrlrqKPauJAHqaxOItoUA== + version "1.3.775" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082" + integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q== emittery@^0.7.1: version "0.7.2" @@ -3701,7 +3704,7 @@ err-code@^2.0.2: resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3763,10 +3766,10 @@ es6-error@^4.0.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild@^0.12.9: - version "0.12.9" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.9.tgz#bed4e7087c286cd81d975631f77d47feb1660070" - integrity sha512-MWRhAbMOJ9RJygCrt778rz/qNYgA4ZVj6aXnNPxFjs7PmIpb0fuB9Gmg5uWrr6n++XKwwm/RmSz6RR5JL2Ocsw== +esbuild@^0.12.15: + version "0.12.15" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.15.tgz#9d99cf39aeb2188265c5983e983e236829f08af0" + integrity sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw== escalade@^3.1.1: version "3.1.1" @@ -3939,13 +3942,14 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint@^7.29.0: - version "7.29.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0" - integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA== +eslint@^7.30.0: + version "7.30.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.30.0.tgz#6d34ab51aaa56112fd97166226c9a97f505474f8" + integrity sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg== dependencies: "@babel/code-frame" "7.12.11" "@eslint/eslintrc" "^0.4.2" + "@humanwhocodes/config-array" "^0.5.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -4201,16 +4205,15 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + version "3.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" fast-json-patch@^2.2.1: version "2.2.1" @@ -4235,9 +4238,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + version "1.11.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.1.tgz#5d8175aae17db61947f8b162cfc7f63264d22807" + integrity sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw== dependencies: reusify "^1.0.4" @@ -4315,14 +4318,6 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -4374,9 +4369,9 @@ flatted@^2.0.1: integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== flatted@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" - integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + version "3.2.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.1.tgz#bbef080d95fca6709362c73044a1634f7c6e7d05" + integrity sha512-OMQjaErSFHmHqZe+PSidH5n8j3O0F2DdnVh8JB4j4eUQ2k6KvB0qGfrKIhapvez5JerBbmWkaLYUYWISaESoXg== follow-redirects@^1.10.0, follow-redirects@^1.11.0: version "1.14.1" @@ -4574,15 +4569,14 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-pkg-repo@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" - integrity sha1-xztInAbYDMVTbCyFP54FIyBWly0= +get-pkg-repo@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.1.2.tgz#c4ffd60015cf091be666a0212753fc158f01a4c0" + integrity sha512-/FjamZL9cBYllEbReZkxF2IMh80d8TJoC4e3bmLNif8ibHw95aj0N/tzqK0kZz9eU/3w3dL6lF4fnnX/sDdW3A== dependencies: - hosted-git-info "^2.1.4" - meow "^3.3.0" - normalize-package-data "^2.3.0" - parse-github-repo-url "^1.3.0" + "@hutson/parse-repository-url" "^3.0.0" + hosted-git-info "^4.0.0" + meow "^7.0.0" through2 "^2.0.0" get-port@^5.1.1: @@ -4590,11 +4584,6 @@ get-port@^5.1.1: resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - get-stdin@~8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" @@ -4643,7 +4632,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -git-raw-commits@^2.0.8: +git-raw-commits@^2.0.10, git-raw-commits@^2.0.8: version "2.0.10" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== @@ -4671,17 +4660,17 @@ git-semver-tags@^4.0.0, git-semver-tags@^4.1.1: semver "^6.0.0" git-up@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.2.tgz#10c3d731051b366dc19d3df454bfca3f77913a7c" - integrity sha512-kbuvus1dWQB2sSW4cbfTeGpCMd8ge9jx9RKnhXhuJ7tnvT+NIrTVfYZxjtflZddQYcmdOTlkAcjmx7bor+15AQ== + version "4.0.5" + resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.5.tgz#e7bb70981a37ea2fb8fe049669800a1f9a01d759" + integrity sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA== dependencies: is-ssh "^1.3.0" - parse-url "^5.0.0" + parse-url "^6.0.0" git-url-parse@^11.4.4: - version "11.4.4" - resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" - integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== + version "11.5.0" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.5.0.tgz#acaaf65239cb1536185b19165a24bbc754b3f764" + integrity sha512-TZYSMDeM37r71Lqg1mbnMlOqlHd7BSij9qN7XwTkRqSAYFMihGLGhfHwgqQob3GUhEneKnV4nskN9rbQw2KGxA== dependencies: git-up "^4.0.0" @@ -4702,14 +4691,14 @@ github-api@^3.4.0: js-base64 "^2.1.9" utf8 "^2.1.1" -glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@^5.1.2: +glob-parent@^5.1.1, glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@^7.0.0, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@~7.1.6: +glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@~7.1.6: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== @@ -4727,9 +4716,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.9.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb" - integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA== + version "13.10.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676" + integrity sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g== dependencies: type-fest "^0.20.2" @@ -4873,7 +4862,7 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== -hosted-git-info@^4.0.1: +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== @@ -5018,13 +5007,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -5086,11 +5068,6 @@ inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -5152,9 +5129,9 @@ is-ci@^2.0.0: ci-info "^2.0.0" is-core-module@^2.2.0, is-core-module@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" - integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" + integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg== dependencies: has "^1.0.3" @@ -5217,11 +5194,6 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -5373,11 +5345,6 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - is-weakmap@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" @@ -6016,65 +5983,65 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.30.0.tgz#b905760ddf5e29c6c6ef31b8c670a2d1db7291c0" - integrity sha512-74GeV8ab8BrS3k8h8HKnI8f5PecsRahflElxJuc6bI9xA5AhRAzBF/Lrt5HibuYPuSsyLAmhTU1GTHdRvKq8aA== +jsii-diff@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.31.0.tgz#7f32b340cf340cc1929f4d534bdfa6495fc09bed" + integrity sha512-eEKFfZXGXxlWFg7E0F4h2UGOnpVCzHclM586SE4KnMwHzSlpRrdYrXa2KhFQSLs/gpZofDV4rPLZ9UDLvNu75Q== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" fs-extra "^9.1.0" - jsii-reflect "^1.30.0" + jsii-reflect "^1.31.0" log4js "^6.3.0" typescript "~3.9.9" yargs "^16.2.0" -jsii-pacmak@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.30.0.tgz#a6a7570da1388027ce4e5ca1603d4144f341d307" - integrity sha512-hYvISYBXZ5WL/+LtG3HpVrimguqAoWa3D8jaqsnoiIGrdmaxKCZ0VnioJYxEX7wVamYuCwXu5NFx/b31BspU6A== +jsii-pacmak@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.31.0.tgz#7e4fa67f1de582be04263904aa45966d84210996" + integrity sha512-fGiAoooRPMadwTWU0vfHJdcNzeYdESnkU/8LmlI4k6yF1iIlFMIbWPulBxP6fV7SqV3CZQKGpUbcPD/Uzf1glg== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" clone "^2.1.2" - codemaker "^1.30.0" + codemaker "^1.31.0" commonmark "^0.29.3" escape-string-regexp "^4.0.0" fs-extra "^9.1.0" - jsii-reflect "^1.30.0" - jsii-rosetta "^1.30.0" + jsii-reflect "^1.31.0" + jsii-rosetta "^1.31.0" semver "^7.3.5" spdx-license-list "^6.4.0" xmlbuilder "^15.1.1" yargs "^16.2.0" -jsii-reflect@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.30.0.tgz#b079d448ed35c9d9dfea8798a8ef39487ed0c86c" - integrity sha512-t/1Zr1gGqQSYt94Lfq860VLnCr8y8MLvlLorWYqmBeWKCaSPhtYSC1blGhZhDrAW+CBXiT0Oy64j4Q++AntRmw== +jsii-reflect@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.31.0.tgz#83acdae835071c734bb8847cf3cad7ccc4497540" + integrity sha512-jKc3tryVeEyEBZFv5bDB8rOaEgW+yBPh0DE4GQCKQQLdkp76Lm9ZSkrnJk5e0gEuAWsmuc1DUs35OcVNr8QRWg== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" colors "^1.4.0" fs-extra "^9.1.0" - oo-ascii-tree "^1.30.0" + oo-ascii-tree "^1.31.0" yargs "^16.2.0" -jsii-rosetta@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.30.0.tgz#5c974eefef9a8e5e1b8364e53e6856f07c7eaf68" - integrity sha512-ChFg5qhvxCaM2bspCqizs48yMtsm5YLHjBoNZLCkbXyc3yMM5l8pnn787B5ww5TI3+tKxKYWkbiKf356kQ1OgQ== +jsii-rosetta@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.31.0.tgz#f5174b532b4c3a79eadd9ed059aa33bee21e3225" + integrity sha512-Heu6D+yI5mmUklLQdX3PdDvHUQm14618Fj4PQM9seKa4cohxzJ7EHopfRObKYHMko9awopx4Qr7Gtu6u/QPqfw== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" commonmark "^0.29.3" fs-extra "^9.1.0" typescript "~3.9.9" xmldom "^0.6.0" yargs "^16.2.0" -jsii@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.30.0.tgz#fe20f60e33d0beaae24bc6537fb623333e913da4" - integrity sha512-TfVHhGjP0QiTEkyfnxrDIE8Da+itxnNUK2YoD69qIPAzmZ58goKVqK4sbXrXz2urHSToGLDmWI8+H69cLeVjJw== +jsii@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.31.0.tgz#513ff04581eae233accef2e2ce06a19d9bd4d972" + integrity sha512-q/p5a6OLO9V0pIcyzS5sygkU9lPskY57KM7KbmppLDPVi5nIqpsRyFfsbPnGWFfDBMk//nkcfj+dbKJIplVkgg== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.5" @@ -6138,20 +6105,13 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@2.x, json5@^2.1.2: +json5@2.x, json5@^2.1.2, json5@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== dependencies: minimist "^1.2.5" -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - jsonc-parser@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" @@ -6364,17 +6324,6 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -6536,14 +6485,6 @@ log4js@^6.3.0: rfdc "^1.1.4" streamroller "^2.2.4" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -6654,7 +6595,7 @@ map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-obj@^1.0.0, map-obj@^1.0.1: +map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= @@ -6728,21 +6669,22 @@ mdurl@^1.0.1, mdurl@~1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" +meow@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" + integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.13.1" + yargs-parser "^18.1.3" meow@^8.0.0: version "8.1.2" @@ -6802,7 +6744,7 @@ micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -6853,7 +6795,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: +minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -7084,10 +7026,10 @@ nise@^5.1.0: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.1.0.tgz#41c8ce8b35ab7d618c4cbf40de1d5bce319979ba" - integrity sha512-3N3DUY8XYrxxzWazQ+nSBpiaJ3q6gcpNh4gXovC/QBxrsvNp4tq+wsLHF6mJ3nrn3lPLn7KCJqKxy/9aD+0fdw== +nock@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.1.1.tgz#3c830129d4560957f59b6f480a41ddbaf9cf57af" + integrity sha512-YKTR9MjfK3kS9/l4nuTxyYm30cgOExRHzkLNhL8nhEUyU4f8Za/dRxOqjhVT1vGs0svWo3dDnJTUX1qxYeWy5w== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -7189,7 +7131,7 @@ nopt@^5.0.0: dependencies: abbrev "1" -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -7221,10 +7163,10 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== +normalize-url@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== npm-bundled@^1.1.1, npm-bundled@^1.1.2: version "1.1.2" @@ -7417,7 +7359,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -7432,9 +7374,9 @@ object-copy@^0.1.0: kind-of "^3.0.3" object-inspect@^1.10.3, object-inspect@^1.9.0: - version "1.10.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" - integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== + version "1.11.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== object-is@^1.1.4: version "1.1.5" @@ -7510,10 +7452,10 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.30.0.tgz#5a20204d05370c0578b800836ed1e8c660d3c4e0" - integrity sha512-TzXuoCnha2QHFcAR+8+tBgD7Wnn6Uh+P3aZMoXKDJ3CVLXFnTnzHy4WMmmz01pTfv+f5haQMjhL9OIFJLEZ5kA== +oo-ascii-tree@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.31.0.tgz#36e10dcad35ba767db41c2d2050ff2174f3d5e6f" + integrity sha512-gNb2MyP1ZcF7cX0WgsAjYe4gZcx7BMLBWKE2TJZZbQ9/j4D8gbJh5Aq6RlXBgev74ODlgAVVcPr2wKU4Dufhqg== open@^7.4.2: version "7.4.2" @@ -7755,11 +7697,11 @@ package-hash@^4.0.0: release-zalgo "^1.0.0" pacote@^11.2.6: - version "11.3.4" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.4.tgz#c290b790a5cee3082bb8fa223f3f3e2fdf3d0bfc" - integrity sha512-RfahPCunM9GI7ryJV/zY0bWQiokZyLqaSNHXtbNSoLb7bwTvBbJBEyCJ01KWs4j1Gj7GmX8crYXQ1sNX6P2VKA== + version "11.3.5" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.5.tgz#73cf1fc3772b533f575e39efa96c50be8c3dc9d2" + integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== dependencies: - "@npmcli/git" "^2.0.1" + "@npmcli/git" "^2.1.0" "@npmcli/installed-package-contents" "^1.0.6" "@npmcli/promise-spawn" "^1.2.0" "@npmcli/run-script" "^1.8.2" @@ -7791,18 +7733,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-github-repo-url@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" - integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -7831,13 +7761,13 @@ parse-path@^4.0.0: qs "^6.9.4" query-string "^6.13.8" -parse-url@^5.0.0: - version "5.0.5" - resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-5.0.5.tgz#06b7f6978b65cac7851bb768bec4e0b950714e1a" - integrity sha512-AwfVhXaQrNNI6UPUJq/GJN2qoY0L9gPgxhh9VbDP0bfBAJWaC/Zh8hjQ58YKTi4AagOT70fpadkYSKPo+eFb1w== +parse-url@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-6.0.0.tgz#f5dd262a7de9ec00914939220410b66cff09107d" + integrity sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw== dependencies: is-ssh "^1.3.0" - normalize-url "4.5.0" + normalize-url "^6.1.0" parse-path "^4.0.0" protocols "^1.4.0" @@ -7870,13 +7800,6 @@ patch-package@^6.4.7: slash "^2.0.0" tmp "^0.0.33" -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -7914,15 +7837,6 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -7940,12 +7854,12 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -pify@^2.0.0, pify@^2.3.0: +pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -7965,18 +7879,6 @@ pify@^5.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - pirates@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" @@ -8279,14 +8181,6 @@ read-package-tree@^5.3.1: readdir-scoped-modules "^1.0.0" util-promisify "^2.1.0" -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -8312,15 +8206,6 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -8396,21 +8281,6 @@ readdir-scoped-modules@^1.0.0: graceful-fs "^4.1.2" once "^1.3.0" -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -8462,13 +8332,6 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -8532,7 +8395,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.20.0: +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -8740,15 +8603,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@^0.8.3: - version "0.8.4" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" - integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -9188,13 +9042,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -9215,13 +9062,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -9588,11 +9428,6 @@ traverse@^0.6.6: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -9658,12 +9493,11 @@ tsame@^2.0.1: integrity sha512-jxyxgKVKa4Bh5dPcO42TJL22lIvfd9LOVJwdovKOnJa4TLLrHxquK+DlGm4rkGmrcur+GRx+x4oW00O2pY/fFw== tsconfig-paths@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + version "3.10.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz#79ae67a68c15289fdf5c51cb74f397522d795ed7" + integrity sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q== dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" + json5 "^2.2.0" minimist "^1.2.0" strip-bom "^3.0.0" @@ -9720,6 +9554,11 @@ type-detect@4.0.8, type-detect@^4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -9796,9 +9635,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-js@^3.1.4: - version "3.13.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.9.tgz#4d8d21dcd497f29cfd8e9378b9df123ad025999b" - integrity sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g== + version "3.13.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.10.tgz#a6bd0d28d38f592c3adb6b180ea6e07e1e540a8d" + integrity sha512-57H3ACYFXeo1IaZ1w02sfA71wI60MGco/IQFjOqK+WtKoprh7Go2/yvd2HPtoJILO2Or84ncLccI4xoHMTSbGg== uid-number@0.0.6: version "0.0.6" @@ -10043,9 +9882,9 @@ whatwg-mimetype@^2.3.0: integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.6.0.tgz#27c0205a4902084b872aecb97cf0f2a7a3011f4c" - integrity sha512-os0KkeeqUOl7ccdDT1qqUcS4KH4tcBTSKK5Nl5WKb2lyxInIZ/CpjkqKa1Ss12mjfdcRX9mHmPPs7/SxG1Hbdw== + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== dependencies: lodash "^4.7.0" tr46 "^2.1.0" @@ -10213,9 +10052,9 @@ write-pkg@^4.0.0: write-json-file "^3.2.0" ws@^7.4.5: - version "7.5.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" - integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== + version "7.5.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" + integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== xml-js@^1.6.11: version "1.6.11" @@ -10330,7 +10169,7 @@ yargs-parser@^13.0.0, yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.2: +yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==