diff --git a/.gitignore b/.gitignore index a2281906f6867..70bbad3393e00 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ yarn-error.log .nzm-* /.versionrc.json +RELEASE_NOTES.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0127076effa..f254623e858c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ 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.128.0](https://github.com/aws/aws-cdk/compare/v1.127.0...v1.128.0) (2021-10-14) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **assertions:** Starting this release, the `assertions` module will be +published to Maven with the name 'assertions' instead of +'cdk-assertions'. + +### Features + +* **apigatewayv2-integrations:** http api - support for request parameter mapping ([#15630](https://github.com/aws/aws-cdk/issues/15630)) ([0452aed](https://github.com/aws/aws-cdk/commit/0452aed2f00198e05bd65b1d20246f7de0b24e20)) +* **cli:** hotswap deployments for ECS Services ([#16864](https://github.com/aws/aws-cdk/issues/16864)) ([ad7288f](https://github.com/aws/aws-cdk/commit/ad7288f35a17fcfbecd7080e99ece4873fa99ad2)) +* **codepipeline:** add support for string user parameters to the Lambda invoke action ([#16946](https://github.com/aws/aws-cdk/issues/16946)) ([e19ea31](https://github.com/aws/aws-cdk/commit/e19ea31dbf62446edaf5131c75246098ab05da6e)), closes [#16776](https://github.com/aws/aws-cdk/issues/16776) +* **lambda:** docker platform for architecture ([#16858](https://github.com/aws/aws-cdk/issues/16858)) ([5c258a3](https://github.com/aws/aws-cdk/commit/5c258a30367a4922e404eb26e5aa076720846fbe)) +* **lambda-event-sources:** self managed kafka: support sasl/plain authentication ([#16712](https://github.com/aws/aws-cdk/issues/16712)) ([d4ad93f](https://github.com/aws/aws-cdk/commit/d4ad93f30877b26b851caa81d3a4a1d80df55164)) +* **stepfunctions-tasks:** AWS SDK service integrations ([#16746](https://github.com/aws/aws-cdk/issues/16746)) ([ae840ff](https://github.com/aws/aws-cdk/commit/ae840ff1abb8283a1290dae5859f5729a9cf72b1)), closes [#16780](https://github.com/aws/aws-cdk/issues/16780) + + +### Bug Fixes + +* **ecs:** add ASG capacity via Capacity Provider by not specifying machineImageType ([#16361](https://github.com/aws/aws-cdk/issues/16361)) ([93b3fdc](https://github.com/aws/aws-cdk/commit/93b3fdce80f0997d7b809f9ef7e3edd1e75e1f42)), closes [#16360](https://github.com/aws/aws-cdk/issues/16360) +* **servicecatalog:** Allow users to create multiple product versions from assets. ([#16914](https://github.com/aws/aws-cdk/issues/16914)) ([958d755](https://github.com/aws/aws-cdk/commit/958d755ff7acaf016e3f8969bf5ab07d4dc2977b)) +* **codebuild:** add build image AMAZON_LINUX_2_ARM_2 ([#16931](https://github.com/aws/aws-cdk/issues/16931)) ([370cb31](https://github.com/aws/aws-cdk/commit/370cb310cce3fccc5381d8d53130e21b266de868)), closes [#16930](https://github.com/aws/aws-cdk/issues/16930) +* **core:** asset hash is different between linux and windows ([#16945](https://github.com/aws/aws-cdk/issues/16945)) ([59950dd](https://github.com/aws/aws-cdk/commit/59950dd331635fb707aac819529614c0f3e47ee5)), closes [#14555](https://github.com/aws/aws-cdk/issues/14555) [#16928](https://github.com/aws/aws-cdk/issues/16928) +* **ecs-patterns:** minScalingCapacity cannot be set to 0 ([#16961](https://github.com/aws/aws-cdk/issues/16961)) ([589f284](https://github.com/aws/aws-cdk/commit/589f284acec8530aa9824b75a5daef4632e98985)), closes [#15632](https://github.com/aws/aws-cdk/issues/15632) [#14336](https://github.com/aws/aws-cdk/issues/14336) +* **ssm:** StringParameter accepts ParameterType.AWS_EC2_IMAGE_ID as type ([#16884](https://github.com/aws/aws-cdk/issues/16884)) ([2b353be](https://github.com/aws/aws-cdk/commit/2b353be5291cbcdc56a8863038eed4a5f2adc65f)), closes [#16806](https://github.com/aws/aws-cdk/issues/16806) +* use registry.npmjs.com to fix shinkwrap resolves ([#16607](https://github.com/aws/aws-cdk/issues/16607)) ([8f91531](https://github.com/aws/aws-cdk/commit/8f91531c3c25900316d40d5564450566a03e27ee)) + + +### Miscellaneous Chores + +* **assertions:** consistent naming in maven ([#16921](https://github.com/aws/aws-cdk/issues/16921)) ([0dcd9ec](https://github.com/aws/aws-cdk/commit/0dcd9eca3a1014c39f92d9e052b67974fc751af0)) + ## [1.127.0](https://github.com/aws/aws-cdk/compare/v1.126.0...v1.127.0) (2021-10-08) diff --git a/build.sh b/build.sh index 99719cd328874..aae39e94ea730 100755 --- a/build.sh +++ b/build.sh @@ -93,4 +93,11 @@ if [ "$check_compat" == "true" ]; then /bin/bash scripts/check-api-compatibility.sh fi +# Create the release notes for the current version. These are ephemeral and not saved in source. +# Skip this step for a "bump candidate" build, where a new, fake version number has been created +# without any corresponding changelog entries. +if ! ${BUMP_CANDIDATE:-false}; then + node ./scripts/create-release-notes.js +fi + touch $BUILD_INDICATOR diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index cce77fd6398e6..14eb72a56e7a4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -21,6 +21,7 @@ - [Lambda Integration](#lambda) - [HTTP Proxy Integration](#http-proxy) - [Private Integration](#private-integration) + - [Request Parameters](#request-parameters) - [WebSocket APIs](#websocket-apis) - [Lambda WebSocket Integration](#lambda-websocket-integration) @@ -149,6 +150,40 @@ const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { }); ``` +### Request Parameters + +Request parameter mapping allows API requests from clients to be modified before they reach backend integrations. +Parameter mapping can be used to specify modifications to request parameters. See [Transforming API requests and +responses](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html). + +The following example creates a new header - `header2` - as a copy of `header1` and removes `header1`. + +```ts +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpAlbIntegration({ + // ... + requestParameters: new ParameterMapping() + .appendHeader('header2', MappingValue.header('header1')) + .removeHeader('header1'); + }), + }), +}); +``` + +To add mapping keys and values not yet supported by the CDK, use the `custom()` method: + +```ts +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpAlbIntegration({ + listener, + requestParameters: new ParameterMapping() + .custom('myKey', 'myValue'), + }), + }), +}); +``` + + ## WebSocket APIs WebSocket integrations connect a route to backend resources. The following integrations are supported in the CDK. diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts index 656e0a550408f..e5e6d5c448663 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts @@ -44,6 +44,7 @@ export class HttpAlbIntegration extends HttpPrivateIntegration { connectionId: vpcLink.vpcLinkId, uri: this.props.listener.listenerArn, secureServerName: this.props.secureServerName, + parameterMapping: this.props.parameterMapping, }; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts index db14e50f7fc54..1627b9b0c4deb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts @@ -1,4 +1,4 @@ -import { HttpMethod, IVpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { HttpMethod, IVpcLink, ParameterMapping } from '@aws-cdk/aws-apigatewayv2'; /** * Base options for private integration @@ -24,4 +24,11 @@ export interface HttpPrivateIntegrationOptions { */ readonly secureServerName?: string; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts index a7ef2d1b4d7b9..70873c9582fc8 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/http-proxy.ts @@ -4,6 +4,7 @@ import { HttpRouteIntegrationConfig, HttpMethod, IHttpRouteIntegration, + ParameterMapping, PayloadFormatVersion, } from '@aws-cdk/aws-apigatewayv2'; @@ -21,6 +22,13 @@ export interface HttpProxyIntegrationProps { * @default HttpMethod.ANY */ readonly method?: HttpMethod; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } /** @@ -36,6 +44,7 @@ export class HttpProxyIntegration implements IHttpRouteIntegration { payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, // 1.0 is required and is the only supported format type: HttpIntegrationType.HTTP_PROXY, uri: this.props.url, + parameterMapping: this.props.parameterMapping, }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts index 220d3dca57210..358263f724bda 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts @@ -4,6 +4,7 @@ import { HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion, + ParameterMapping, } from '@aws-cdk/aws-apigatewayv2'; import { ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; @@ -24,6 +25,13 @@ export interface LambdaProxyIntegrationProps { * @default PayloadFormatVersion.VERSION_2_0 */ readonly payloadFormatVersion?: PayloadFormatVersion; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } /** @@ -50,6 +58,7 @@ export class LambdaProxyIntegration implements IHttpRouteIntegration { type: HttpIntegrationType.LAMBDA_PROXY, uri: this.props.handler.functionArn, payloadFormatVersion: this.props.payloadFormatVersion ?? PayloadFormatVersion.VERSION_2_0, + parameterMapping: this.props.parameterMapping, }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts index 1c405b51b3bfd..7aae0aa002354 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts @@ -44,6 +44,7 @@ export class HttpNlbIntegration extends HttpPrivateIntegration { connectionId: vpcLink.vpcLinkId, uri: this.props.listener.listenerArn, secureServerName: this.props.secureServerName, + parameterMapping: this.props.parameterMapping, }; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts index f9f204b6eba3e..6f3b8eedbea0a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts @@ -34,6 +34,7 @@ export class HttpServiceDiscoveryIntegration extends HttpPrivateIntegration { connectionId: this.props.vpcLink.vpcLinkId, uri: this.props.service.serviceArn, secureServerName: this.props.secureServerName, + parameterMapping: this.props.parameterMapping, }; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts index e5871da260bc2..3c25e92fe6d21 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink, ParameterMapping, MappingValue } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { Stack } from '@aws-cdk/core'; @@ -143,4 +143,34 @@ describe('HttpAlbIntegration', () => { }, }); }); + + test('parameterMapping option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpAlbIntegration({ + listener, + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/http-proxy.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/http-proxy.test.ts index 0c76996fe7867..0f29ec0915fd9 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/http-proxy.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/http-proxy.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpIntegration, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteKey, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpIntegration, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteKey, MappingValue, ParameterMapping, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; import { Stack } from '@aws-cdk/core'; import { HttpProxyIntegration } from '../../lib'; @@ -71,4 +71,26 @@ describe('HttpProxyIntegration', () => { IntegrationUri: 'some-target-url', }); }); + + test('parameterMapping is correctly recognized', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpIntegration(stack, 'HttpInteg', { + httpApi: api, + integrationType: HttpIntegrationType.HTTP_PROXY, + integrationUri: 'some-target-url', + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + IntegrationUri: 'some-target-url', + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/lambda.test.ts index d0ead43945ec4..85bb624a25d54 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/lambda.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/lambda.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpRoute, HttpRouteKey, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpRoute, HttpRouteKey, MappingValue, ParameterMapping, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; import { App, Stack } from '@aws-cdk/core'; import { LambdaProxyIntegration } from '../../lib'; @@ -41,6 +41,28 @@ describe('LambdaProxyIntegration', () => { }); }); + test('parameterMapping selection', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'LambdaProxyRoute', { + httpApi: api, + integration: new LambdaProxyIntegration({ + handler: fooFunction(stack, 'Fn'), + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); + test('no dependency cycles', () => { const app = new App(); const lambdaStack = new Stack(app, 'lambdaStack'); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts index a32d448d8e448..e1e18c43f49aa 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, MappingValue, ParameterMapping, VpcLink } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { Stack } from '@aws-cdk/core'; @@ -140,4 +140,34 @@ describe('HttpNlbIntegration', () => { }, }); }); + + test('paramaterMapping option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.NetworkLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpNlbIntegration({ + listener, + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts index 4d3bef328a637..e037004cada0e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, MappingValue, ParameterMapping, VpcLink } from '@aws-cdk/aws-apigatewayv2'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as servicediscovery from '@aws-cdk/aws-servicediscovery'; import { Stack } from '@aws-cdk/core'; @@ -125,4 +125,38 @@ describe('HttpServiceDiscoveryIntegration', () => { }, }); }); + + test('parameterMapping option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); + const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'Namespace', { + name: 'foobar.com', + vpc, + }); + const service = namespace.createService('Service'); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpServiceDiscoveryIntegration({ + vpcLink, + service, + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 254a29ea6d28b..32dfe0a0c2120 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -305,6 +305,7 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, secureServerName: config.secureServerName, + parameterMapping: config.parameterMapping, }); this._integrationCache.saveIntegration(scope, config, integration); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index f832b5b7e3b21..df0cf84c13da0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -3,6 +3,7 @@ import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; import { IIntegration } from '../common'; +import { ParameterMapping } from '../parameter-mapping'; import { IHttpApi } from './api'; import { HttpMethod, IHttpRoute } from './route'; @@ -128,6 +129,13 @@ export interface HttpIntegrationProps { * @default undefined private integration traffic will use HTTP protocol */ readonly secureServerName?: string; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } /** @@ -149,6 +157,7 @@ export class HttpIntegration extends Resource implements IHttpIntegration { connectionId: props.connectionId, connectionType: props.connectionType, payloadFormatVersion: props.payloadFormatVersion?.version, + requestParameters: props.parameterMapping?.mappings, }); if (props.secureServerName) { @@ -237,4 +246,11 @@ export interface HttpRouteIntegrationConfig { * @default undefined private integration traffic will use HTTP protocol */ readonly secureServerName?: string; + + /** + * Specifies how to transform HTTP requests before sending them to the backend + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html + * @default undefined requests are sent to the backend unmodified + */ + readonly parameterMapping?: ParameterMapping; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index 709f207a04435..ca40349975ec1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -18,6 +18,11 @@ export interface IHttpStage extends IStage { */ readonly api: IHttpApi; + /** + * The custom domain URL to this stage + */ + readonly domainUrl: string; + /** * Metric for the number of client-side errors captured in a given period. * @@ -96,6 +101,7 @@ export interface HttpStageAttributes extends StageAttributes { } abstract class HttpStageBase extends StageBase implements IHttpStage { + public abstract readonly domainUrl: string; public abstract readonly api: IHttpApi; public metricClientError(props?: MetricOptions): Metric { @@ -140,6 +146,10 @@ export class HttpStage extends HttpStageBase { get url(): string { throw new Error('url is not available for imported stages.'); } + + get domainUrl(): string { + throw new Error('domainUrl is not available for imported stages.'); + } } return new Import(scope, id); } @@ -177,9 +187,6 @@ export class HttpStage extends HttpStageBase { return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } - /** - * The custom domain URL to this stage - */ public get domainUrl(): string { if (!this._apiMapping) { throw new Error('domainUrl is not available when no API mapping is associated with the Stage'); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts index 12dd8113f8b4c..81df171d98aa1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts @@ -2,3 +2,4 @@ export * from './apigatewayv2.generated'; export * from './common'; export * from './http'; export * from './websocket'; +export * from './parameter-mapping'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts new file mode 100644 index 0000000000000..deb967d572de2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts @@ -0,0 +1,145 @@ +/** + * Represents a Mapping Value. + */ +export interface IMappingValue { + /** + * Represents a Mapping Value. + */ + readonly value: string; +}; + +/** + * Represents a Mapping Value. + */ +export class MappingValue implements IMappingValue { + /** + * Creates an empty mapping value. + */ + public static readonly NONE = new MappingValue(''); + + /** + * Creates a header mapping value. + */ + public static requestHeader(name: string) { return new MappingValue(`$request.header.${name}`); } + + /** + * Creates a query string mapping value. + */ + public static requestQueryString(name: string) { return new MappingValue(`$request.querystring.${name}`); } + + /** + * Creates a request body mapping value. + */ + public static requestBody(name: string) { return new MappingValue(`$request.body.${name}`); } + + /** + * Creates a request path mapping value. + */ + public static requestPath() { return new MappingValue('$request.path'); } + + /** + * Creates a request path parameter mapping value. + */ + public static requestPathParam(name: string) { return new MappingValue(`$request.path.${name}`); } + + /** + * Creates a context variable mapping value. + */ + public static contextVariable(variableName: string) { return new MappingValue(`$context.${variableName}`); } + + /** + * Creates a stage variable mapping value. + */ + public static stageVariable(variableName: string) { return new MappingValue(`$stageVariables.${variableName}`); } + + /** + * Creates a custom mapping value. + */ + public static custom(value: string) { return new MappingValue(value); } + + /** + * Represents a Mapping Value. + */ + public readonly value: string + + protected constructor(value: string) { + this.value = value; + } +} + +/** + * Represents a Parameter Mapping. + */ +export class ParameterMapping { + /** + * Represents all created parameter mappings. + */ + public readonly mappings: { [key: string]: string } + constructor() { + this.mappings = {}; + } + + /** + * Creates a mapping to append a header. + */ + public appendHeader(name: string, value: MappingValue): ParameterMapping { + this.mappings[`append:header.${name}`] = value.value; + return this; + } + + /** + * Creates a mapping to overwrite a header. + */ + public overwriteHeader(name: string, value: MappingValue): ParameterMapping { + this.mappings[`overwrite:header.${name}`] = value.value; + return this; + } + + /** + * Creates a mapping to remove a header. + */ + public removeHeader(name: string): ParameterMapping { + this.mappings[`remove:header.${name}`] = ''; + return this; + } + + /** + * Creates a mapping to append a query string. + */ + public appendQueryString(name: string, value: MappingValue): ParameterMapping { + this.mappings[`append:querystring.${name}`] = value.value; + return this; + } + + /** + * Creates a mapping to overwrite a querystring. + */ + public overwriteQueryString(name: string, value: MappingValue): ParameterMapping { + this.mappings[`overwrite:querystring.${name}`] = value.value; + return this; + } + + /** + * Creates a mapping to remove a querystring. + */ + public removeQueryString(name: string): ParameterMapping { + this.mappings[`remove:querystring.${name}`] = ''; + return this; + } + + /** + * Creates a mapping to overwrite a path. + */ + public overwritePath(value: MappingValue): ParameterMapping { + this.mappings['overwrite:path'] = value.value; + return this; + } + + /** + * Creates a custom mapping. + */ + public custom(key: string, value: string): ParameterMapping { + this.mappings[key] = value; + return this; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index 5b7f1052bfe35..200933eefbca9 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -1,9 +1,10 @@ import { Match, Template } from '@aws-cdk/assertions'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; import { - CorsHttpMethod, + CorsHttpMethod, DomainName, HttpApi, HttpAuthorizer, HttpIntegrationType, HttpMethod, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, HttpNoneAuthorizer, PayloadFormatVersion, } from '../../lib'; @@ -374,6 +375,27 @@ describe('HttpApi', () => { expect(() => api.apiEndpoint).toThrow(/apiEndpoint is not configured/); }); + test('domainUrl can be retrieved for default stage', () => { + const stack = new Stack(); + const dn = new DomainName(stack, 'DN', { + domainName: 'example.com', + certificate: Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:111111111111:certificate'), + }); + + const api = new HttpApi(stack, 'Api', { + createDefaultStage: true, + defaultDomainMapping: { + domainName: dn, + }, + }); + + expect(stack.resolve(api.defaultStage?.domainUrl)).toEqual({ + 'Fn::Join': ['', [ + 'https://', { Ref: 'DNFDC76583' }, '/', + ]], + }); + }); + describe('default authorization settings', () => { test('can add default authorizer', () => { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index 9f64cfdfbd123..75d744b6b5bcc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -1,8 +1,11 @@ import { Template } from '@aws-cdk/assertions'; import { Stack, App } from '@aws-cdk/core'; import { - HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteAuthorizerBindOptions, - HttpRouteAuthorizerConfig, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, + HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, + HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, + MappingValue, + ParameterMapping, + PayloadFormatVersion, } from '../../lib'; describe('HttpRoute', () => { @@ -174,6 +177,9 @@ describe('HttpRoute', () => { connectionType: HttpConnectionType.VPC_LINK, uri: 'some-target-arn', secureServerName: 'some-server-name', + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), }; } } @@ -201,6 +207,47 @@ describe('HttpRoute', () => { Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::VpcLink', 0); }); + test('configures private integration correctly when parameter mappings are passed', () => { + // GIVEN + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + class PrivateIntegration implements IHttpRouteIntegration { + public bind(): HttpRouteIntegrationConfig { + return { + method: HttpMethod.ANY, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + type: HttpIntegrationType.HTTP_PROXY, + uri: 'some-target-arn', + parameterMapping: new ParameterMapping() + .appendHeader('header2', MappingValue.requestHeader('header1')) + .removeHeader('header1'), + }; + } + } + + // WHEN + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new PrivateIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + IntegrationMethod: 'ANY', + IntegrationUri: 'some-target-arn', + PayloadFormatVersion: '1.0', + RequestParameters: { + 'append:header.header2': '$request.header.header1', + 'remove:header.header1': '', + }, + }); + + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::VpcLink', 0); + }); + test('can create route with an authorizer attached', () => { const stack = new Stack(); const httpApi = new HttpApi(stack, 'HttpApi'); diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index b1779dffe6f8a..54bc79fade4c3 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -35,7 +35,7 @@ After you create your service mesh, you can create virtual services, virtual nod The following example creates the `AppMesh` service mesh with the default egress filter of `DROP_ALL`. See [the AWS CloudFormation `EgressFilter` resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appmesh-mesh-egressfilter.html) for more info on egress filters. ```ts -const mesh = new Mesh(stack, 'AppMesh', { +const mesh = new appmesh.Mesh(this, 'AppMesh', { meshName: 'myAwsMesh', }); ``` @@ -43,9 +43,9 @@ const mesh = new Mesh(stack, 'AppMesh', { The mesh can instead be created with the `ALLOW_ALL` egress filter by providing the `egressFilter` property. ```ts -const mesh = new Mesh(stack, 'AppMesh', { +const mesh = new appmesh.Mesh(this, 'AppMesh', { meshName: 'myAwsMesh', - egressFilter: MeshFilterType.ALLOW_ALL, + egressFilter: appmesh.MeshFilterType.ALLOW_ALL, }); ``` @@ -57,8 +57,9 @@ Virtual routers handle traffic for one or more virtual services within your mesh After you create a virtual router, you can create and associate routes to your virtual router that direct incoming requests to different virtual nodes. ```ts +declare const mesh: appmesh.Mesh; const router = mesh.addVirtualRouter('router', { - listeners: [ VirtualRouterListener.http(8080) ], + listeners: [appmesh.VirtualRouterListener.http(8080)], }); ``` @@ -68,16 +69,19 @@ The router can also be created using the `VirtualRouter` constructor (passing in This is particularly useful when splitting your resources between many stacks: for example, defining the mesh itself as part of an infrastructure stack, but defining the other resources, such as routers, in the application stack: ```ts -const mesh = new Mesh(infraStack, 'AppMesh', { +declare const infraStack: cdk.Stack; +declare const appStack: cdk.Stack; + +const mesh = new appmesh.Mesh(infraStack, 'AppMesh', { meshName: 'myAwsMesh', - egressFilter: MeshFilterType.ALLOW_ALL, + egressFilter: appmesh.MeshFilterType.ALLOW_ALL, }); // the VirtualRouter will belong to 'appStack', // even though the Mesh belongs to 'infraStack' -const router = new VirtualRouter(appStack, 'router', { +const router = new appmesh.VirtualRouter(appStack, 'router', { mesh, // notice that mesh is a required property when creating a router with the 'new' statement - listeners: [VirtualRouterListener.http(8081)], + listeners: [appmesh.VirtualRouterListener.http(8081)], }); ``` @@ -102,18 +106,22 @@ When creating a virtual service: Adding a virtual router as the provider: ```ts -new VirtualService(stack, 'virtual-service', { +declare const router: appmesh.VirtualRouter; + +new appmesh.VirtualService(this, 'virtual-service', { virtualServiceName: 'my-service.default.svc.cluster.local', // optional - virtualServiceProvider: VirtualServiceProvider.virtualRouter(router), + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(router), }); ``` Adding a virtual node as the provider: ```ts -new VirtualService(stack, 'virtual-service', { +declare const node: appmesh.VirtualNode; + +new appmesh.VirtualService(this, 'virtual-service', { virtualServiceName: `my-service.default.svc.cluster.local`, // optional - virtualServiceProvider: VirtualServiceProvider.virtualNode(node), + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), }); ``` @@ -129,18 +137,19 @@ The response metadata for your new virtual node contains the Amazon Resource Nam > If you require your Envoy stats or tracing to use a different name, you can override the `node.cluster` value that is set by `APPMESH_VIRTUAL_NODE_NAME` with the `APPMESH_VIRTUAL_NODE_CLUSTER` environment variable. ```ts -const vpc = new ec2.Vpc(stack, 'vpc'); -const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'test-namespace', { +const vpc = new ec2.Vpc(this, 'vpc'); +const namespace = new cloudmap.PrivateDnsNamespace(this, 'test-namespace', { vpc, name: 'domain.local', }); const service = namespace.createService('Svc'); +declare const mesh: appmesh.Mesh; const node = mesh.addVirtualNode('virtual-node', { - serviceDiscovery: ServiceDiscovery.cloudMap(service), - listeners: [VirtualNodeListener.http({ + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), + listeners: [appmesh.VirtualNodeListener.http({ port: 8081, - healthCheck: HealthCheck.http({ + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, interval: cdk.Duration.seconds(5), // minimum path: '/health-check-path', @@ -148,19 +157,22 @@ const node = mesh.addVirtualNode('virtual-node', { unhealthyThreshold: 2, }), })], - accessLog: AccessLog.fromFilePath('/dev/stdout'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), }); ``` Create a `VirtualNode` with the constructor and add tags. ```ts -const node = new VirtualNode(stack, 'node', { +declare const mesh: appmesh.Mesh; +declare const service: cloudmap.Service; + +const node = new appmesh.VirtualNode(this, 'node', { mesh, - serviceDiscovery: ServiceDiscovery.cloudMap(service), - listeners: [VirtualNodeListener.http({ + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), + listeners: [appmesh.VirtualNodeListener.http({ port: 8080, - healthCheck: HealthCheck.http({ + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, interval: cdk.Duration.seconds(5), path: '/ping', @@ -174,11 +186,11 @@ const node = new VirtualNode(stack, 'node', { backendDefaults: { tlsClientPolicy: { validation: { - trust: TlsValidationTrust.file('/keys/local_cert_chain.pem'), + trust: appmesh.TlsValidationTrust.file('/keys/local_cert_chain.pem'), }, }, }, - accessLog: AccessLog.fromFilePath('/dev/stdout'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), }); cdk.Tags.of(node).add('Environment', 'Dev'); @@ -187,12 +199,16 @@ cdk.Tags.of(node).add('Environment', 'Dev'); Create a `VirtualNode` with the constructor and add backend virtual service. ```ts -const node = new VirtualNode(stack, 'node', { +declare const mesh: appmesh.Mesh; +declare const router: appmesh.VirtualRouter; +declare const service: cloudmap.Service; + +const node = new appmesh.VirtualNode(this, 'node', { mesh, - serviceDiscovery: ServiceDiscovery.cloudMap(service), - listeners: [VirtualNodeListener.http({ + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), + listeners: [appmesh.VirtualNodeListener.http({ port: 8080, - healthCheck: HealthCheck.http({ + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, interval: cdk.Duration.seconds(5), path: '/ping', @@ -203,15 +219,15 @@ const node = new VirtualNode(stack, 'node', { idle: cdk.Duration.seconds(5), }, })], - accessLog: AccessLog.fromFilePath('/dev/stdout'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), }); -const virtualService = new VirtualService(stack, 'service-1', { - virtualServiceProvider: VirtualServiceProvider.virtualRouter(router), +const virtualService = new appmesh.VirtualService(this, 'service-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(router), virtualServiceName: 'service1.domain.local', }); -node.addBackend(Backend.virtualService(virtualService)); +node.addBackend(appmesh.Backend.virtualService(virtualService)); ``` The `listeners` property can be left blank and added later with the `node.addListener()` method. The `serviceDiscovery` property must be specified when specifying a listener. @@ -231,45 +247,44 @@ Provide the TLS certificate to the proxy in one of the following ways: - A certificate provided by a Secrets Discovery Service (SDS) endpoint over local Unix Domain Socket (specify its `secretName`). -```typescript -import * as certificatemanager from '@aws-cdk/aws-certificatemanager'; - +```ts // A Virtual Node with listener TLS from an ACM provided certificate -const cert = new certificatemanager.Certificate(this, 'cert', {...}); +declare const cert: certificatemanager.Certificate; +declare const mesh: appmesh.Mesh; -const node = new VirtualNode(stack, 'node', { +const node = new appmesh.VirtualNode(this, 'node', { mesh, - serviceDiscovery: ServiceDiscovery.dns('node'), - listeners: [VirtualNodeListener.grpc({ + serviceDiscovery: appmesh.ServiceDiscovery.dns('node'), + listeners: [appmesh.VirtualNodeListener.grpc({ port: 80, tls: { - mode: TlsMode.STRICT, - certificate: TlsCertificate.acm(cert), + mode: appmesh.TlsMode.STRICT, + certificate: appmesh.TlsCertificate.acm(cert), }, })], }); // A Virtual Gateway with listener TLS from a customer provided file certificate -const gateway = new VirtualGateway(this, 'gateway', { - mesh: mesh, - listeners: [VirtualGatewayListener.grpc({ +const gateway = new appmesh.VirtualGateway(this, 'gateway', { + mesh, + listeners: [appmesh.VirtualGatewayListener.grpc({ port: 8080, tls: { - mode: TlsMode.STRICT, - certificate: TlsCertificate.file('path/to/certChain', 'path/to/privateKey'), + mode: appmesh.TlsMode.STRICT, + certificate: appmesh.TlsCertificate.file('path/to/certChain', 'path/to/privateKey'), }, })], virtualGatewayName: 'gateway', }); // A Virtual Gateway with listener TLS from a SDS provided certificate -const gateway2 = new VirtualGateway(this, 'gateway2', { - mesh: mesh, - listeners: [VirtualGatewayListener.http2({ +const gateway2 = new appmesh.VirtualGateway(this, 'gateway2', { + mesh, + listeners: [appmesh.VirtualGatewayListener.http2({ port: 8080, tls: { - mode: TlsMode.STRICT, - certificate: TlsCertificate.sds('secrete_certificate'), + mode: appmesh.TlsMode.STRICT, + certificate: appmesh.TlsCertificate.sds('secrete_certificate'), }, })], virtualGatewayName: 'gateway2', @@ -290,38 +305,39 @@ To enable mutual TLS authentication, add the `mutualTlsCertificate` property to > **Note** > Currently, a certificate from AWS Certificate Manager (ACM) cannot be used for mutual TLS authentication. -```typescript -import * as certificatemanager from '@aws-cdk/aws-certificatemanager'; +```ts +declare const mesh: appmesh.Mesh; -const node1 = new VirtualNode(stack, 'node1', { +const node1 = new appmesh.VirtualNode(this, 'node1', { mesh, - serviceDiscovery: ServiceDiscovery.dns('node'), - listeners: [VirtualNodeListener.grpc({ + serviceDiscovery: appmesh.ServiceDiscovery.dns('node'), + listeners: [appmesh.VirtualNodeListener.grpc({ port: 80, tls: { - mode: TlsMode.STRICT, - certificate: TlsCertificate.file('path/to/certChain', 'path/to/privateKey'), + mode: appmesh.TlsMode.STRICT, + certificate: appmesh.TlsCertificate.file('path/to/certChain', 'path/to/privateKey'), // Validate a file client certificates to enable mutual TLS authentication when a client provides a certificate. mutualTlsValidation: { - trust: TlsValidationTrust.file('path-to-certificate'), + trust: appmesh.TlsValidationTrust.file('path-to-certificate'), }, }, })], }); -const node2 = new VirtualNode(stack, 'node2', { +const certificateAuthorityArn = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012'; +const node2 = new appmesh.VirtualNode(this, 'node2', { mesh, - serviceDiscovery: ServiceDiscovery.dns('node2'), + serviceDiscovery: appmesh.ServiceDiscovery.dns('node2'), backendDefaults: { tlsClientPolicy: { ports: [8080, 8081], validation: { - subjectAlternativeNames: SubjectAlternativeNames.matchingExactly('mesh-endpoint.apps.local'), - trust: TlsValidationTrust.acm([ - acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)]), + subjectAlternativeNames: appmesh.SubjectAlternativeNames.matchingExactly('mesh-endpoint.apps.local'), + trust: appmesh.TlsValidationTrust.acm([ + acmpca.CertificateAuthority.fromCertificateAuthorityArn(this, 'certificate', certificateAuthorityArn)]), }, // Provide a SDS client certificate when a server requests it and enable mutual TLS authentication. - mutualTlsCertificate: TlsCertificate.sds('secret_certificate'), + mutualTlsCertificate: appmesh.TlsCertificate.sds('secret_certificate'), }, }, }); @@ -332,18 +348,19 @@ const node2 = new VirtualNode(stack, 'node2', { The `outlierDetection` property adds outlier detection to a Virtual Node listener. The properties `baseEjectionDuration`, `interval`, `maxEjectionPercent`, and `maxServerErrors` are required. -```typescript +```ts // Cloud Map service discovery is currently required for host ejection by outlier detection -const vpc = new ec2.Vpc(stack, 'vpc'); -const namespace = new servicediscovery.PrivateDnsNamespace(this, 'test-namespace', { +const vpc = new ec2.Vpc(this, 'vpc'); +const namespace = new cloudmap.PrivateDnsNamespace(this, 'test-namespace', { vpc, name: 'domain.local', }); const service = namespace.createService('Svc'); +declare const mesh: appmesh.Mesh; const node = mesh.addVirtualNode('virtual-node', { - serviceDiscovery: ServiceDiscovery.cloudMap(service), - listeners: [VirtualNodeListener.http({ + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), + listeners: [appmesh.VirtualNodeListener.http({ outlierDetection: { baseEjectionDuration: cdk.Duration.seconds(10), interval: cdk.Duration.seconds(30), @@ -358,16 +375,17 @@ const node = mesh.addVirtualNode('virtual-node', { The `connectionPool` property can be added to a Virtual Node listener or Virtual Gateway listener to add a request connection pool. Each listener protocol type has its own connection pool properties. -```typescript +```ts // A Virtual Node with a gRPC listener with a connection pool set -const node = new VirtualNode(stack, 'node', { +declare const mesh: appmesh.Mesh; +const node = new appmesh.VirtualNode(this, 'node', { mesh, // DNS service discovery can optionally specify the DNS response type as either LOAD_BALANCER or ENDPOINTS. // LOAD_BALANCER means that the DNS resolver returns a loadbalanced set of endpoints, // whereas ENDPOINTS means that the DNS resolver is returning all the endpoints. // By default, the response type is assumed to be LOAD_BALANCER - serviceDiscovery: ServiceDiscovery.dns('node', DnsResponseType.ENDPOINTS), - listeners: [VirtualNodeListener.http({ + serviceDiscovery: appmesh.ServiceDiscovery.dns('node', appmesh.DnsResponseType.ENDPOINTS), + listeners: [appmesh.VirtualNodeListener.http({ port: 80, connectionPool: { maxConnections: 100, @@ -377,9 +395,9 @@ const node = new VirtualNode(stack, 'node', { }); // A Virtual Gateway with a gRPC listener with a connection pool set -const gateway = new VirtualGateway(stack, 'gateway', { +const gateway = new appmesh.VirtualGateway(this, 'gateway', { mesh, - listeners: [VirtualGatewayListener.grpc({ + listeners: [appmesh.VirtualGatewayListener.grpc({ port: 8080, connectionPool: { maxRequests: 10, @@ -406,8 +424,11 @@ When specifying the method name, the 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 +declare const router: appmesh.VirtualRouter; +declare const node: appmesh.VirtualNode; + router.addRoute('route-http', { - routeSpec: RouteSpec.http({ + routeSpec: appmesh.RouteSpec.http({ weightedTargets: [ { virtualNode: node, @@ -415,7 +436,7 @@ router.addRoute('route-http', { ], match: { // Path that is passed to this method must start with '/'. - path: HttpRoutePathMatch.startsWith('/path-to-app'), + path: appmesh.HttpRoutePathMatch.startsWith('/path-to-app'), }, }), }); @@ -424,25 +445,28 @@ router.addRoute('route-http', { Add an HTTP2 route that matches based on exact path, method, scheme, headers, and query parameters: ```ts +declare const router: appmesh.VirtualRouter; +declare const node: appmesh.VirtualNode; + router.addRoute('route-http2', { - routeSpec: RouteSpec.http2({ + routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [ { virtualNode: node, }, ], match: { - path: HttpRoutePathMatch.exactly('/exact'), - method: HttpRouteMethod.POST, - protocol: HttpRouteProtocol.HTTPS, + path: appmesh.HttpRoutePathMatch.exactly('/exact'), + method: appmesh.HttpRouteMethod.POST, + protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ // All specified headers must match for the route to match. - HeaderMatch.valueIs('Content-Type', 'application/json'), - HeaderMatch.valueIsNot('Content-Type', 'application/json'), + 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. - QueryParameterMatch.valueIs('query-field', 'value') + appmesh.QueryParameterMatch.valueIs('query-field', 'value') ], }, }), @@ -452,8 +476,11 @@ router.addRoute('route-http2', { Add a single route with two targets and split traffic 50/50: ```ts +declare const router: appmesh.VirtualRouter; +declare const node: appmesh.VirtualNode; + router.addRoute('route-http', { - routeSpec: RouteSpec.http({ + routeSpec: appmesh.RouteSpec.http({ weightedTargets: [ { virtualNode: node, @@ -465,7 +492,7 @@ router.addRoute('route-http', { }, ], match: { - path: HttpRoutePathMatch.startsWith('/path-to-app'), + path: appmesh.HttpRoutePathMatch.startsWith('/path-to-app'), }, }), }); @@ -474,14 +501,17 @@ router.addRoute('route-http', { Add an http2 route with retries: ```ts +declare const router: appmesh.VirtualRouter; +declare const node: appmesh.VirtualNode; + router.addRoute('route-http2-retry', { - routeSpec: RouteSpec.http2({ + routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode: node }], retryPolicy: { // Retry if the connection failed - tcpRetryEvents: [TcpRetryEvent.CONNECTION_ERROR], + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], // Retry if HTTP responds with a gateway error (502, 503, 504) - httpRetryEvents: [HttpRetryEvent.GATEWAY_ERROR], + httpRetryEvents: [appmesh.HttpRetryEvent.GATEWAY_ERROR], // Retry five times retryAttempts: 5, // Use a 1 second timeout per retry @@ -494,19 +524,22 @@ router.addRoute('route-http2-retry', { Add a gRPC route with retries: ```ts +declare const router: appmesh.VirtualRouter; +declare const node: appmesh.VirtualNode; + router.addRoute('route-grpc-retry', { - routeSpec: RouteSpec.grpc({ + routeSpec: appmesh.RouteSpec.grpc({ weightedTargets: [{ virtualNode: node }], match: { serviceName: 'servicename' }, retryPolicy: { - tcpRetryEvents: [TcpRetryEvent.CONNECTION_ERROR], - httpRetryEvents: [HttpRetryEvent.GATEWAY_ERROR], + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + httpRetryEvents: [appmesh.HttpRetryEvent.GATEWAY_ERROR], // Retry if gRPC responds that the request was cancelled, a resource // was exhausted, or if the service is unavailable grpcRetryEvents: [ - GrpcRetryEvent.CANCELLED, - GrpcRetryEvent.RESOURCE_EXHAUSTED, - GrpcRetryEvent.UNAVAILABLE, + appmesh.GrpcRetryEvent.CANCELLED, + appmesh.GrpcRetryEvent.RESOURCE_EXHAUSTED, + appmesh.GrpcRetryEvent.UNAVAILABLE, ], retryAttempts: 5, retryTimeout: cdk.Duration.seconds(1), @@ -518,8 +551,11 @@ router.addRoute('route-grpc-retry', { Add an gRPC route that matches based on method name and metadata: ```ts +declare const router: appmesh.VirtualRouter; +declare const node: appmesh.VirtualNode; + router.addRoute('route-grpc-retry', { - routeSpec: RouteSpec.grpc({ + routeSpec: appmesh.RouteSpec.grpc({ weightedTargets: [{ virtualNode: node }], match: { // When method name is specified, service name must be also specified. @@ -527,8 +563,8 @@ router.addRoute('route-grpc-retry', { serviceName: 'servicename', metadata: [ // All specified metadata must match for the route to match. - HeaderMatch.valueStartsWith('Content-Type', 'application/'), - HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), ], }, }), @@ -538,8 +574,11 @@ router.addRoute('route-grpc-retry', { Add a gRPC route with timeout: ```ts +declare const router: appmesh.VirtualRouter; +declare const node: appmesh.VirtualNode; + router.addRoute('route-http', { - routeSpec: RouteSpec.grpc({ + routeSpec: appmesh.RouteSpec.grpc({ weightedTargets: [ { virtualNode: node, @@ -569,13 +608,14 @@ using rules defined in gateway routes which can be added to your virtual gateway Create a virtual gateway with the constructor: ```ts +declare const mesh: appmesh.Mesh; const certificateAuthorityArn = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012'; -const gateway = new VirtualGateway(stack, 'gateway', { +const gateway = new appmesh.VirtualGateway(this, 'gateway', { mesh: mesh, - listeners: [VirtualGatewayListener.http({ + listeners: [appmesh.VirtualGatewayListener.http({ port: 443, - healthCheck: HealthCheck.http({ + healthCheck: appmesh.HealthCheck.http({ interval: cdk.Duration.seconds(10), }), })], @@ -583,12 +623,12 @@ const gateway = new VirtualGateway(stack, 'gateway', { tlsClientPolicy: { ports: [8080, 8081], validation: { - trust: TlsValidationTrust.acm([ - acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)]), + trust: appmesh.TlsValidationTrust.acm([ + acmpca.CertificateAuthority.fromCertificateAuthorityArn(this, 'certificate', certificateAuthorityArn)]), }, }, }, - accessLog: AccessLog.fromFilePath('/dev/stdout'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), virtualGatewayName: 'virtualGateway', }); ``` @@ -596,12 +636,14 @@ const gateway = new VirtualGateway(stack, 'gateway', { Add a virtual gateway directly to the mesh: ```ts +declare const mesh: appmesh.Mesh; + const gateway = mesh.addVirtualGateway('gateway', { - accessLog: AccessLog.fromFilePath('/dev/stdout'), + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), virtualGatewayName: 'virtualGateway', - listeners: [VirtualGatewayListener.http({ + listeners: [appmesh.VirtualGatewayListener.http({ port: 443, - healthCheck: HealthCheck.http({ + healthCheck: appmesh.HealthCheck.http({ interval: cdk.Duration.seconds(10), }), })], @@ -622,11 +664,14 @@ path (prefix, exact, or regex), HTTP method, host name, HTTP headers, and query By default, HTTP-based gateway routes match all requests. ```ts +declare const gateway: appmesh.VirtualGateway; +declare const virtualService: appmesh.VirtualService; + gateway.addGatewayRoute('gateway-route-http', { - routeSpec: GatewayRouteSpec.http({ + routeSpec: appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { - path: HttpGatewayRoutePathMatch.regex('regex'), + path: appmesh.HttpGatewayRoutePathMatch.regex('regex'), }, }), }); @@ -635,11 +680,14 @@ gateway.addGatewayRoute('gateway-route-http', { For gRPC-based gateway routes, the `match` field can be used to match on service name, host name, and metadata. ```ts +declare const gateway: appmesh.VirtualGateway; +declare const virtualService: appmesh.VirtualService; + gateway.addGatewayRoute('gateway-route-grpc', { - routeSpec: GatewayRouteSpec.grpc({ + routeSpec: appmesh.GatewayRouteSpec.grpc({ routeTarget: virtualService, match: { - hostname: GatewayRouteHostnameMatch.endsWith('.example.com'), + hostname: appmesh.GatewayRouteHostnameMatch.endsWith('.example.com'), }, }), }); @@ -649,23 +697,26 @@ For HTTP based gateway routes, App Mesh automatically rewrites the matched prefi This automatic rewrite configuration can be overwritten in following ways: ```ts +declare const gateway: appmesh.VirtualGateway; +declare const virtualService: appmesh.VirtualService; + gateway.addGatewayRoute('gateway-route-http', { - routeSpec: GatewayRouteSpec.http({ + routeSpec: appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { // This disables the default rewrite to '/', and retains original path. - path: HttpGatewayRoutePathMatch.startsWith('/path-to-app/', ''), + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/path-to-app/', ''), }, }), }); gateway.addGatewayRoute('gateway-route-http-1', { - routeSpec: GatewayRouteSpec.http({ + 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: HttpGatewayRoutePathMatch.startsWith('/path-to-app/', '/rewrittenUri/'), + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/path-to-app/', '/rewrittenUri/'), }, }), }); @@ -675,12 +726,15 @@ If matching other path (exact or regex), only specific rewrite path can be speci Unlike `startsWith()` method above, no default rewrite is performed. ```ts +declare const gateway: appmesh.VirtualGateway; +declare const virtualService: appmesh.VirtualService; + gateway.addGatewayRoute('gateway-route-http-2', { - routeSpec: GatewayRouteSpec.http({ + routeSpec: appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { // This rewrites the path from '/test' to '/rewrittenPath'. - path: HttpGatewayRoutePathMatch.exactly('/test', '/rewrittenPath'), + path: appmesh.HttpGatewayRoutePathMatch.exactly('/test', '/rewrittenPath'), }, }), }); @@ -691,11 +745,14 @@ the original request received at the Virtual Gateway to the destination Virtual This default host name rewrite can be configured by specifying the rewrite rule as one of the `match` property: ```ts +declare const gateway: appmesh.VirtualGateway; +declare const virtualService: appmesh.VirtualService; + gateway.addGatewayRoute('gateway-route-grpc', { - routeSpec: GatewayRouteSpec.grpc({ + routeSpec: appmesh.GatewayRouteSpec.grpc({ routeTarget: virtualService, match: { - hostname: GatewayRouteHostnameMatch.exactly('example.com'), + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), // This disables the default rewrite to virtual service name and retain original request. rewriteRequestHostname: false, }, @@ -710,12 +767,13 @@ These imported resources can be used with other resources in your mesh as if the ```ts const arn = 'arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh/virtualNode/testNode'; -VirtualNode.fromVirtualNodeArn(stack, 'importedVirtualNode', arn); +appmesh.VirtualNode.fromVirtualNodeArn(this, 'importedVirtualNode', arn); ``` ```ts -VirtualNode.fromVirtualNodeAttributes(stack, 'imported-virtual-node', { - mesh: Mesh.fromMeshName(stack, 'Mesh', 'testMesh'), +const virtualNodeName = 'my-virtual-node'; +appmesh.VirtualNode.fromVirtualNodeAttributes(this, 'imported-virtual-node', { + mesh: appmesh.Mesh.fromMeshName(this, 'Mesh', 'testMesh'), virtualNodeName: virtualNodeName, }); ``` @@ -724,11 +782,11 @@ To import a mesh, again there are two static methods, `fromMeshArn` and `fromMes ```ts const arn = 'arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh'; -Mesh.fromMeshArn(stack, 'imported-mesh', arn); +appmesh.Mesh.fromMeshArn(this, 'imported-mesh', arn); ``` ```ts -Mesh.fromMeshName(stack, 'imported-mesh', 'abc'); +appmesh.Mesh.fromMeshName(this, 'imported-mesh', 'abc'); ``` ## IAM Grants @@ -737,8 +795,9 @@ Mesh.fromMeshName(stack, 'imported-mesh', 'abc'); Envoy access to stream generated config from App Mesh. ```ts -const gateway = new VirtualGateway(stack, 'testGateway', { mesh: mesh }); -const envoyUser = new iam.User(stack, 'envoyUser'); +declare const mesh: appmesh.Mesh; +const gateway = new appmesh.VirtualGateway(this, 'testGateway', { mesh }); +const envoyUser = new iam.User(this, 'envoyUser'); /** * This will grant `grantStreamAggregatedResources` ONLY for this gateway. @@ -754,10 +813,10 @@ A shared mesh allows resources created by different accounts to communicate with // This is the ARN for the mesh from different AWS IAM account ID. // Ensure mesh is properly shared with your account. For more details, see: https://github.com/aws/aws-cdk/issues/15404 const arn = 'arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh'; -sharedMesh = Mesh.fromMeshArn(stack, 'imported-mesh', arn); +const sharedMesh = appmesh.Mesh.fromMeshArn(this, 'imported-mesh', arn); // This VirtualNode resource can communicate with the resources in the mesh from different AWS IAM account ID. -new VirtualNode(stack, 'test-node', { +new appmesh.VirtualNode(this, 'test-node', { mesh: sharedMesh, }); ``` diff --git a/packages/@aws-cdk/aws-appmesh/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-appmesh/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..09e9b7277edcb --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/rosetta/default.ts-fixture @@ -0,0 +1,16 @@ +// Fixture with packages imported, but nothing else +import cdk = require('@aws-cdk/core'); +import acmpca = require('@aws-cdk/aws-acmpca'); +import appmesh = require('@aws-cdk/aws-appmesh'); +import certificatemanager = require('@aws-cdk/aws-certificatemanager'); +import cloudmap = require('@aws-cdk/aws-servicediscovery'); +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); + +class Fixture extends cdk.Stack { + constructor(scope: cdk.Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 18dbda2cbcd3a..186c58f84138f 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -1656,8 +1656,9 @@ class ArmBuildImage implements IBuildImage { public validate(buildEnvironment: BuildEnvironment): string[] { const ret = []; if (buildEnvironment.computeType && + buildEnvironment.computeType !== ComputeType.SMALL && buildEnvironment.computeType !== ComputeType.LARGE) { - ret.push(`ARM images only support ComputeType '${ComputeType.LARGE}' - ` + + ret.push(`ARM images only support ComputeTypes '${ComputeType.SMALL}' and '${ComputeType.LARGE}' - ` + `'${buildEnvironment.computeType}' was given`); } return ret; diff --git a/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts b/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts index cc8f9309bcb44..f5bb3eacda8d1 100644 --- a/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts +++ b/packages/@aws-cdk/aws-codebuild/test/codebuild.test.ts @@ -1590,17 +1590,21 @@ describe('ARM image', () => { }); }); - test('cannot be used in conjunction with ComputeType SMALL', () => { + test('can be used with ComputeType SMALL', () => { const stack = new cdk.Stack(); + new codebuild.PipelineProject(stack, 'Project', { + environment: { + computeType: codebuild.ComputeType.SMALL, + buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_ARM, + }, + }); - expect(() => { - new codebuild.PipelineProject(stack, 'Project', { - environment: { - buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_ARM, - computeType: codebuild.ComputeType.SMALL, - }, - }); - }).toThrow(/ARM images only support ComputeType 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_SMALL' was given/); + expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + 'Environment': { + 'Type': 'ARM_CONTAINER', + 'ComputeType': 'BUILD_GENERAL1_SMALL', + }, + }); }); test('cannot be used in conjunction with ComputeType MEDIUM', () => { @@ -1613,7 +1617,24 @@ describe('ARM image', () => { computeType: codebuild.ComputeType.MEDIUM, }, }); - }).toThrow(/ARM images only support ComputeType 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_MEDIUM' was given/); + }).toThrow(/ARM images only support ComputeTypes 'BUILD_GENERAL1_SMALL' and 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_MEDIUM' was given/); + }); + + test('can be used with ComputeType LARGE', () => { + const stack = new cdk.Stack(); + new codebuild.PipelineProject(stack, 'Project', { + environment: { + computeType: codebuild.ComputeType.LARGE, + buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_ARM, + }, + }); + + expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { + 'Environment': { + 'Type': 'ARM_CONTAINER', + 'ComputeType': 'BUILD_GENERAL1_LARGE', + }, + }); }); test('cannot be used in conjunction with ComputeType X2_LARGE', () => { @@ -1626,7 +1647,7 @@ describe('ARM image', () => { computeType: codebuild.ComputeType.X2_LARGE, }, }); - }).toThrow(/ARM images only support ComputeType 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_2XLARGE' was given/); + }).toThrow(/ARM images only support ComputeTypes 'BUILD_GENERAL1_SMALL' and 'BUILD_GENERAL1_LARGE' - 'BUILD_GENERAL1_2XLARGE' was given/); }); }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index e57381c517c1f..05860cfe20233 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -11,7 +11,7 @@ This package contains Actions that can be used in a CodePipeline. -```ts +```ts nofixture import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; ``` @@ -23,10 +23,8 @@ import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; To use a CodeCommit Repository in a CodePipeline: ```ts -import * as codecommit from '@aws-cdk/aws-codecommit'; - const repo = new codecommit.Repository(this, 'Repo', { - // ... + repositoryName: 'MyRepo', }); const pipeline = new codepipeline.Pipeline(this, 'MyPipeline', { @@ -49,6 +47,7 @@ You can specify the role object in eventRole property. ```ts const eventRole = iam.Role.fromRoleArn(this, 'Event-role', 'roleArn'); +declare const repo: codecommit.Repository; const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit', repository: repo, @@ -61,6 +60,8 @@ If you want to clone the entire CodeCommit repository (only available for CodeBu you can set the `codeBuildCloneOutput` property to `true`: ```ts +declare const project: codebuild.PipelineProject; +declare const repo: codecommit.Repository; const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit', @@ -80,15 +81,22 @@ const buildAction = new codepipeline_actions.CodeBuildAction({ The CodeCommit source action emits variables: ```ts +declare const project: codebuild.PipelineProject; +declare const repo: codecommit.Repository; +const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ - // ... + actionName: 'CodeCommit', + repository: repo, + output: sourceOutput, variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you }); // later: new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { COMMIT_ID: { value: sourceAction.variables.commitId, @@ -107,20 +115,21 @@ If you want to use a GitHub repository as the source, you must create: with the value of the **GitHub Access Token**. Pick whatever name you want (for example `my-github-token`). This token can be stored either as Plaintext or as a Secret key/value. If you stored the token as Plaintext, - set `cdk.SecretValue.secretsManager('my-github-token')` as the value of `oauthToken`. + set `SecretValue.secretsManager('my-github-token')` as the value of `oauthToken`. If you stored it as a Secret key/value, - you must set `cdk.SecretValue.secretsManager('my-github-token', { jsonField : 'my-github-token' })` as the value of `oauthToken`. + you must set `SecretValue.secretsManager('my-github-token', { jsonField : 'my-github-token' })` as the value of `oauthToken`. To use GitHub as the source of a CodePipeline: ```ts // Read the secret from Secrets Manager +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.GitHubSourceAction({ actionName: 'GitHub_Source', owner: 'awslabs', repo: 'aws-cdk', - oauthToken: cdk.SecretValue.secretsManager('my-github-token'), + oauthToken: SecretValue.secretsManager('my-github-token'), output: sourceOutput, branch: 'develop', // default: 'master' }); @@ -133,15 +142,24 @@ pipeline.addStage({ The GitHub source action emits variables: ```ts +declare const sourceOutput: codepipeline.Artifact; +declare const project: codebuild.PipelineProject; + const sourceAction = new codepipeline_actions.GitHubSourceAction({ - // ... + actionName: 'Github_Source', + output: sourceOutput, + owner: 'my-owner', + repo: 'my-repo', + oauthToken: SecretValue.secretsManager('my-github-token'), variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you }); // later: new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { COMMIT_URL: { value: sourceAction.variables.commitUrl, @@ -185,8 +203,6 @@ You can also use the `CodeStarConnectionsSourceAction` to connect to GitHub, in To use an S3 Bucket as a source in CodePipeline: ```ts -import * as s3 from '@aws-cdk/aws-s3'; - const sourceBucket = new s3.Bucket(this, 'MyBucket', { versioned: true, // a Bucket used as a source in CodePipeline must be versioned }); @@ -229,6 +245,8 @@ You can do it through the CDK: ```ts import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +declare const sourceBucket: s3.Bucket; +const sourceOutput = new codepipeline.Artifact(); const key = 'some/key.zip'; const trail = new cloudtrail.Trail(this, 'CloudTrail'); trail.addS3EventSelector([{ @@ -249,15 +267,23 @@ const sourceAction = new codepipeline_actions.S3SourceAction({ The S3 source action emits variables: ```ts +const key = 'some/key.zip'; +declare const sourceBucket: s3.Bucket; +const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.S3SourceAction({ - // ... + actionName: 'S3Source', + bucketKey: key, + bucket: sourceBucket, + output: sourceOutput, variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you }); // later: - +declare const project: codebuild.PipelineProject; new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { VERSION_ID: { value: sourceAction.variables.versionId, @@ -273,6 +299,7 @@ To use an ECR Repository as a source in a Pipeline: ```ts import * as ecr from '@aws-cdk/aws-ecr'; +declare const ecrRepository: ecr.Repository; const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.EcrSourceAction({ @@ -290,15 +317,23 @@ pipeline.addStage({ The ECR source action emits variables: ```ts +import * as ecr from '@aws-cdk/aws-ecr'; + +const sourceOutput = new codepipeline.Artifact(); +declare const ecrRepository: ecr.Repository; const sourceAction = new codepipeline_actions.EcrSourceAction({ - // ... + actionName: 'Source', + output: sourceOutput, + repository: ecrRepository, variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you }); // later: - +declare const project: codebuild.PipelineProject; new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { IMAGE_URI: { value: sourceAction.variables.imageUri, @@ -314,9 +349,7 @@ new codepipeline_actions.CodeBuildAction({ Example of a CodeBuild Project used in a Pipeline, alongside CodeCommit: ```ts -import * as codebuild from '@aws-cdk/aws-codebuild'; -import * as codecommit from '@aws-cdk/aws-codecommit'; - +declare const project: codebuild.PipelineProject; const repository = new codecommit.Repository(this, 'MyRepository', { repositoryName: 'MyRepository', }); @@ -356,6 +389,8 @@ if you want a `Test` Action instead, override the `type` property: ```ts +declare const project: codebuild.PipelineProject; +const sourceOutput = new codepipeline.Artifact(); const testAction = new codepipeline_actions.CodeBuildAction({ actionName: 'IntegrationTest', project, @@ -373,12 +408,14 @@ properties of the `Project` class, you need to use the `extraInputs` and Actions. Example: ```ts +declare const repository1: codecommit.Repository; const sourceOutput1 = new codepipeline.Artifact(); const sourceAction1 = new codepipeline_actions.CodeCommitSourceAction({ actionName: 'Source1', repository: repository1, output: sourceOutput1, }); +declare const repository2: codecommit.Repository; const sourceOutput2 = new codepipeline.Artifact('source2'); const sourceAction2 = new codepipeline_actions.CodeCommitSourceAction({ actionName: 'Source2', @@ -386,6 +423,7 @@ const sourceAction2 = new codepipeline_actions.CodeCommitSourceAction({ output: sourceOutput2, }); +declare const project: codebuild.PipelineProject; const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: 'Build', project, @@ -447,6 +485,7 @@ in the 'exported-variables' subsection of the 'env' section. Example: ```ts +const sourceOutput = new codepipeline.Artifact(); const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: 'Build1', input: sourceOutput, @@ -469,9 +508,11 @@ const buildAction = new codepipeline_actions.CodeBuildAction({ }); // later: - +declare const project: codebuild.PipelineProject; new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { MyVar: { value: buildAction.variable('MY_VAR'), @@ -498,7 +539,7 @@ or outside the CDK (in the CodePipeline AWS Console, for example), you can import it: ```ts -const jenkinsProvider = codepipeline_actions.JenkinsProvider.import(this, 'JenkinsProvider', { +const jenkinsProvider = codepipeline_actions.JenkinsProvider.fromJenkinsProviderAttributes(this, 'JenkinsProvider', { providerName: 'MyJenkinsProvider', serverUrl: 'http://my-jenkins.com:8080', version: '2', // optional, default: '1' @@ -514,6 +555,7 @@ With a `JenkinsProvider`, we can create a Jenkins Action: ```ts +declare const jenkinsProvider: codepipeline_actions.JenkinsProvider; const buildAction = new codepipeline_actions.JenkinsAction({ actionName: 'JenkinsBuild', jenkinsProvider: jenkinsProvider, @@ -566,8 +608,12 @@ If you want to update stacks in a different account, pass the `account` property when creating the action: ```ts +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.CloudFormationCreateUpdateStackAction({ - // ... + actionName: 'CloudFormationCreateUpdate', + stackName: 'MyStackName', + adminPermissions: true, + templatePath: sourceOutput.atPath('template.yaml'), account: '123456789012', }); ``` @@ -584,15 +630,20 @@ and the action will operate in the same account the role belongs to: import { PhysicalName } from '@aws-cdk/core'; // in stack for account 123456789012... +declare const otherAccountStack: Stack; const actionRole = new iam.Role(otherAccountStack, 'ActionRole', { - assumedBy: new iam.AccountPrincipal(pipelineAccount), + assumedBy: new iam.AccountPrincipal('123456789012'), // the role has to have a physical name set roleName: PhysicalName.GENERATE_IF_NEEDED, }); // in the pipeline stack... +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.CloudFormationCreateUpdateStackAction({ - // ... + actionName: 'CloudFormationCreateUpdate', + stackName: 'MyStackName', + adminPermissions: true, + templatePath: sourceOutput.atPath('template.yaml'), role: actionRole, // this action will be cross-account as well }); ``` @@ -604,14 +655,13 @@ new codepipeline_actions.CloudFormationCreateUpdateStackAction({ To use CodeDeploy for EC2/on-premise deployments in a Pipeline: ```ts -import * as codedeploy from '@aws-cdk/aws-codedeploy'; - const pipeline = new codepipeline.Pipeline(this, 'MyPipeline', { pipelineName: 'MyPipeline', }); // add the source and build Stages to the Pipeline... - +const buildOutput = new codepipeline.Artifact(); +declare const deploymentGroup: codedeploy.ServerDeploymentGroup; const deployAction = new codepipeline_actions.CodeDeployServerDeployAction({ actionName: 'CodeDeploy', input: buildOutput, @@ -629,19 +679,19 @@ To use CodeDeploy for blue-green Lambda deployments in a Pipeline: ```ts const lambdaCode = lambda.Code.fromCfnParameters(); -const func = new lambda.Function(lambdaStack, 'Lambda', { +const func = new lambda.Function(this, 'Lambda', { code: lambdaCode, handler: 'index.handler', runtime: lambda.Runtime.NODEJS_12_X, }); // used to make sure each CDK synthesis produces a different Version const version = func.addVersion('NewVersion'); -const alias = new lambda.Alias(lambdaStack, 'LambdaAlias', { +const alias = new lambda.Alias(this, 'LambdaAlias', { aliasName: 'Prod', version, }); -new codedeploy.LambdaDeploymentGroup(lambdaStack, 'DeploymentGroup', { +new codedeploy.LambdaDeploymentGroup(this, 'DeploymentGroup', { alias, deploymentConfig: codedeploy.LambdaDeploymentConfig.LINEAR_10PERCENT_EVERY_1MINUTE, }); @@ -658,6 +708,11 @@ CodePipeline can deploy an ECS service. The deploy Action receives one input Artifact which contains the [image definition file]: ```ts +import * as ecs from '@aws-cdk/aws-ecs'; + +declare const service: ecs.FargateService; +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const buildOutput = new codepipeline.Artifact(); const deployStage = pipeline.addStage({ stageName: 'Deploy', actions: [ @@ -672,7 +727,7 @@ const deployStage = pipeline.addStage({ // use the `imageFile` property, // and leave out the `input` property imageFile: buildOutput.atPath('imageDef.json'), - deploymentTimeout: cdk.Duration.minutes(60), // optional, default is 60 minutes + deploymentTimeout: Duration.minutes(60), // optional, default is 60 minutes }), ], }); @@ -697,12 +752,12 @@ Here's an example: To use an S3 Bucket as a deployment target in CodePipeline: ```ts -const targetBucket = new s3.Bucket(this, 'MyBucket', {}); +const sourceOutput = new codepipeline.Artifact(); +const targetBucket = new s3.Bucket(this, 'MyBucket'); const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const deployAction = new codepipeline_actions.S3DeployAction({ actionName: 'S3Deploy', - stage: deployStage, bucket: targetBucket, input: sourceOutput, }); @@ -720,9 +775,8 @@ and use the AWS CLI to invalidate the cache: ```ts // Create a Cloudfront Web Distribution -const distribution = new cloudfront.Distribution(this, `Distribution`, { - // ... -}); +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +declare const distribution: cloudfront.Distribution; // Create the build project that will invalidate the cache const invalidateBuildProject = new codebuild.PipelineProject(this, `InvalidateProject`, { @@ -752,19 +806,21 @@ invalidateBuildProject.addToRolePolicy(new iam.PolicyStatement({ })); // Create the pipeline (here only the S3 deploy and Invalidate cache build) +const deployBucket = new s3.Bucket(this, 'DeployBucket'); +const deployInput = new codepipeline.Artifact(); new codepipeline.Pipeline(this, 'Pipeline', { stages: [ // ... { stageName: 'Deploy', actions: [ - new codepipelineActions.S3DeployAction({ + new codepipeline_actions.S3DeployAction({ actionName: 'S3Deploy', bucket: deployBucket, input: deployInput, runOrder: 1, }), - new codepipelineActions.CodeBuildAction({ + new codepipeline_actions.CodeBuildAction({ actionName: 'InvalidateCache', project: invalidateBuildProject, input: deployInput, @@ -782,11 +838,12 @@ You can deploy to Alexa using CodePipeline with the following Action: ```ts // Read the secrets from ParameterStore -const clientId = cdk.SecretValue.secretsManager('AlexaClientId'); -const clientSecret = cdk.SecretValue.secretsManager('AlexaClientSecret'); -const refreshToken = cdk.SecretValue.secretsManager('AlexaRefreshToken'); +const clientId = SecretValue.secretsManager('AlexaClientId'); +const clientSecret = SecretValue.secretsManager('AlexaClientSecret'); +const refreshToken = SecretValue.secretsManager('AlexaRefreshToken'); // Add deploy action +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.AlexaSkillDeployAction({ actionName: 'DeploySkill', runOrder: 1, @@ -801,20 +858,22 @@ new codepipeline_actions.AlexaSkillDeployAction({ If you need manifest overrides you can specify them as `parameterOverridesArtifact` in the action: ```ts -import * as cloudformation from '@aws-cdk/aws-cloudformation'; - // Deploy some CFN change set and store output const executeOutput = new codepipeline.Artifact('CloudFormation'); const executeChangeSetAction = new codepipeline_actions.CloudFormationExecuteChangeSetAction({ actionName: 'ExecuteChangesTest', runOrder: 2, - stackName, - changeSetName, + stackName: 'MyStack', + changeSetName: 'MyChangeSet', outputFileName: 'overrides.json', output: executeOutput, }); // Provide CFN output as manifest overrides +const clientId = SecretValue.secretsManager('AlexaClientId'); +const clientSecret = SecretValue.secretsManager('AlexaClientSecret'); +const refreshToken = SecretValue.secretsManager('AlexaRefreshToken'); +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.AlexaSkillDeployAction({ actionName: 'DeploySkill', runOrder: 1, @@ -832,11 +891,11 @@ new codepipeline_actions.AlexaSkillDeployAction({ You can deploy a CloudFormation template to an existing Service Catalog product with the following Action: ```ts +const cdkBuildOutput = new codepipeline.Artifact(); const serviceCatalogDeployAction = new codepipeline_actions.ServiceCatalogDeployActionBeta1({ actionName: 'ServiceCatalogDeploy', templatePath: cdkBuildOutput.atPath("Sample.template.json"), productVersionName: "Version - " + Date.now.toString, - productType: "CLOUD_FORMATION_TEMPLATE", productVersionDescription: "This is a version from the pipeline with a new description.", productId: "prod-XXXXXXXX", }); @@ -849,6 +908,10 @@ const serviceCatalogDeployAction = new codepipeline_actions.ServiceCatalogDeploy This package contains an Action that stops the Pipeline until someone manually clicks the approve button: ```ts +import * as sns from '@aws-cdk/aws-sns'; + +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const approveStage = pipeline.addStage({ stageName: 'Approve' }); const manualApprovalAction = new codepipeline_actions.ManualApprovalAction({ actionName: 'Approve', notificationTopic: new sns.Topic(this, 'Topic'), // optional @@ -871,12 +934,14 @@ If you want to grant a principal permissions to approve the changes, you can invoke the method `grantManualApproval` passing it a `IGrantable`: ```ts +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const approveStage = pipeline.addStage({ stageName: 'Approve' }); const manualApprovalAction = new codepipeline_actions.ManualApprovalAction({ actionName: 'Approve', }); approveStage.addAction(manualApprovalAction); -const role = iam.Role.fromRoleArn(this, 'Admin', Arn.format({ service: 'iam', resource: 'role', resourceName: 'Admin' }, stack)); +const role = iam.Role.fromRoleArn(this, 'Admin', Arn.format({ service: 'iam', resource: 'role', resourceName: 'Admin' }, this)); manualApprovalAction.grantManualApproval(role); ``` @@ -885,8 +950,7 @@ manualApprovalAction.grantManualApproval(role); This module contains an Action that allows you to invoke a Lambda function in a Pipeline: ```ts -import * as lambda from '@aws-cdk/aws-lambda'; - +declare const fn: lambda.Function; const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ actionName: 'Lambda', @@ -902,7 +966,9 @@ The Lambda Action can have up to 5 inputs, and up to 5 outputs: ```ts - +declare const fn: lambda.Function; +const sourceOutput = new codepipeline.Artifact(); +const buildOutput = new codepipeline.Artifact(); const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ actionName: 'Lambda', inputs: [ @@ -913,7 +979,26 @@ const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ new codepipeline.Artifact('Out1'), new codepipeline.Artifact('Out2'), ], - lambda: fn + lambda: fn, +}); +``` + +The Lambda Action supports custom user parameters that pipeline +will pass to the Lambda function: + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; + +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ + actionName: 'Lambda', + lambda: fn, + userParameters: { + foo: 'bar', + baz: 'qux', + }, + // OR + userParametersString: 'my-parameter-string', }); ``` @@ -924,8 +1009,6 @@ API with the `outputVariables` property filled with the map of variables Example: ```ts -import * as lambda from '@aws-cdk/aws-lambda'; - const lambdaInvokeAction = new codepipeline_actions.LambdaInvokeAction({ actionName: 'Lambda', lambda: new lambda.Function(this, 'Func', { @@ -949,9 +1032,12 @@ const lambdaInvokeAction = new codepipeline_actions.LambdaInvokeAction({ }); // later: - +declare const project: codebuild.PipelineProject; +const sourceOutput = new codepipeline.Artifact(); new codepipeline_actions.CodeBuildAction({ - // ... + actionName: 'CodeBuild', + project, + input: sourceOutput, environmentVariables: { MyVar: { value: lambdaInvokeAction.variable('MY_VAR'), @@ -968,14 +1054,13 @@ on how to write a Lambda function invoked from CodePipeline. This module contains an Action that allows you to invoke a Step Function in a Pipeline: ```ts -import * as stepfunction from '@aws-cdk/aws-stepfunctions'; - +import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); -const startState = new stepfunction.Pass(stack, 'StartState'); -const simpleStateMachine = new stepfunction.StateMachine(stack, 'SimpleStateMachine', { +const startState = new stepfunctions.Pass(this, 'StartState'); +const simpleStateMachine = new stepfunctions.StateMachine(this, 'SimpleStateMachine', { definition: startState, }); -const stepFunctionAction = new codepipeline_actions.StepFunctionsInvokeAction({ +const stepFunctionAction = new codepipeline_actions.StepFunctionInvokeAction({ actionName: 'Invoke', stateMachine: simpleStateMachine, stateMachineInput: codepipeline_actions.StateMachineInput.literal({ IsHelloWorldExample: true }), @@ -990,15 +1075,15 @@ The `StateMachineInput` can be created with one of 2 static factory methods: `literal`, which takes an arbitrary map as its only argument, or `filePath`: ```ts -import * as stepfunction from '@aws-cdk/aws-stepfunctions'; +import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const inputArtifact = new codepipeline.Artifact(); -const startState = new stepfunction.Pass(stack, 'StartState'); -const simpleStateMachine = new stepfunction.StateMachine(stack, 'SimpleStateMachine', { +const startState = new stepfunctions.Pass(this, 'StartState'); +const simpleStateMachine = new stepfunctions.StateMachine(this, 'SimpleStateMachine', { definition: startState, }); -const stepFunctionAction = new codepipeline_actions.StepFunctionsInvokeAction({ +const stepFunctionAction = new codepipeline_actions.StepFunctionInvokeAction({ actionName: 'Invoke', stateMachine: simpleStateMachine, stateMachineInput: codepipeline_actions.StateMachineInput.filePath(inputArtifact.atPath('assets/input.json')), diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts index f8a49d977f6b4..8740d8fafb9ff 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts @@ -38,10 +38,24 @@ export interface LambdaInvokeActionProps extends codepipeline.CommonAwsActionPro * A set of key-value pairs that will be accessible to the invoked Lambda * inside the event that the Pipeline will call it with. * + * Only one of `userParameters` or `userParametersString` can be specified. + * * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html#actions-invoke-lambda-function-json-event-example + * @default - no user parameters will be passed */ readonly userParameters?: { [key: string]: any }; + /** + * The string representation of the user parameters that will be + * accessible to the invoked Lambda inside the event + * that the Pipeline will call it with. + * + * Only one of `userParametersString` or `userParameters` can be specified. + * + * @default - no user parameters will be passed + */ + readonly userParametersString?: string; + /** * The lambda function to invoke. */ @@ -71,6 +85,10 @@ export class LambdaInvokeAction extends Action { }); this.props = props; + + if (props.userParameters && props.userParametersString) { + throw new Error('Only one of userParameters or userParametersString can be specified'); + } } /** @@ -121,7 +139,7 @@ export class LambdaInvokeAction extends Action { return { configuration: { FunctionName: this.props.lambda.functionName, - UserParameters: Stack.of(scope).toJsonString(this.props.userParameters), + UserParameters: this.props.userParametersString ?? Stack.of(scope).toJsonString(this.props.userParameters), }, }; } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-codepipeline-actions/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..aba6086a1683e --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/rosetta/default.ts-fixture @@ -0,0 +1,17 @@ +// Fixture with packages imported, but nothing else +import { Arn, Construct, Duration, SecretValue, Stack } from '@aws-cdk/core'; +import codebuild = require('@aws-cdk/aws-codebuild'); +import codedeploy = require('@aws-cdk/aws-codedeploy'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import codepipeline_actions = require('@aws-cdk/aws-codepipeline-actions'); +import codecommit = require('@aws-cdk/aws-codecommit'); +import iam = require('@aws-cdk/aws-iam'); +import lambda = require('@aws-cdk/aws-lambda'); +import s3 = require('@aws-cdk/aws-s3'); + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts index e631dd6b10c1b..3f9a337333915 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.ts @@ -124,9 +124,7 @@ pipeline.addStage({ templatePath: cdkBuildOutput.atPath('LambdaStack.template.yaml'), stackName: 'LambdaStackDeployedName', adminPermissions: true, - parameterOverrides: { - ...lambdaCode.assign(lambdaBuildOutput.s3Location), - }, + parameterOverrides: lambdaCode.assign(lambdaBuildOutput.s3Location), extraInputs: [ lambdaBuildOutput, ], diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts index c9c694b6cedaf..41d1b13563235 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts @@ -3,9 +3,9 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; +import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import { App, Aws, Lazy, SecretValue, Stack, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ @@ -100,6 +100,36 @@ describe('', () => { }); + test('properly assings userParametersString to UserParameters', () => { + const stack = stackIncludingLambdaInvokeCodePipeline({ + userParamsString: '**/*.template.json', + }); + + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + 'Stages': [ + {}, + { + 'Actions': [ + { + 'Configuration': { + 'UserParameters': '**/*.template.json', + }, + }, + ], + }, + ], + }); + }); + + test('throws if both userParameters and userParametersString are supplied', () => { + expect(() => stackIncludingLambdaInvokeCodePipeline({ + userParams: { + key: Token.asString(null), + }, + userParamsString: '**/*.template.json', + })).toThrow(/Only one of userParameters or userParametersString can be specified/); + }); + test("assigns the Action's Role with read permissions to the Bucket if it has only inputs", () => { const stack = stackIncludingLambdaInvokeCodePipeline({ lambdaInput: new codepipeline.Artifact(), @@ -302,6 +332,7 @@ describe('', () => { interface HelperProps { readonly userParams?: { [key: string]: any }; + readonly userParamsString?: string; readonly lambdaInput?: codepipeline.Artifact; readonly lambdaOutput?: codepipeline.Artifact; } @@ -334,6 +365,7 @@ function stackIncludingLambdaInvokeCodePipeline(props: HelperProps, app?: App) { runtime: lambda.Runtime.NODEJS_10_X, }), userParameters: props.userParams, + userParametersString: props.userParamsString, inputs: props.lambdaInput ? [props.lambdaInput] : undefined, outputs: props.lambdaOutput ? [props.lambdaOutput] : undefined, }), diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts index 1554dcc84004d..38109b1a4a614 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts @@ -49,12 +49,13 @@ export async function isCompleteHandler(event: IsCompleteRequest): Promise r.RegionName === event.ResourceProperties.Region); const replicaActive = !!(regionReplica?.ReplicaStatus === 'ACTIVE'); + const skipReplicationCompletedWait = event.ResourceProperties.SkipReplicationCompletedWait ?? false; switch (event.RequestType) { case 'Create': case 'Update': // Complete when replica is reported as ACTIVE - return { IsComplete: tableActive && replicaActive }; + return { IsComplete: tableActive && (replicaActive || skipReplicationCompletedWait) }; case 'Delete': // Complete when replica is gone return { IsComplete: tableActive && regionReplica === undefined }; diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index a2b1aae5ee2eb..10a7472be3c0d 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -231,6 +231,22 @@ export interface TableOptions extends SchemaOptions { */ readonly replicationTimeout?: Duration; + /** + * Indicates whether CloudFormation stack waits for replication to finish. + * If set to false, the CloudFormation resource will mark the resource as + * created and replication will be completed asynchronously. This property is + * ignored if replicationRegions property is not set. + * + * DO NOT UNSET this property if adding/removing multiple replicationRegions + * in one deployment, as CloudFormation only supports one region replication + * at a time. CDK overcomes this limitation by waiting for replication to + * finish before starting new replicationRegion. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-globaltable.html#cfn-dynamodb-globaltable-replicas + * @default true + */ + readonly waitForReplicationToFinish?: boolean; + /** * Whether CloudWatch contributor insights is enabled. * @@ -1152,7 +1168,7 @@ export class Table extends TableBase { } if (props.replicationRegions && props.replicationRegions.length > 0) { - this.createReplicaTables(props.replicationRegions, props.replicationTimeout); + this.createReplicaTables(props.replicationRegions, props.replicationTimeout, props.waitForReplicationToFinish); } } @@ -1494,7 +1510,7 @@ export class Table extends TableBase { * * @param regions regions where to create tables */ - private createReplicaTables(regions: string[], timeout?: Duration) { + private createReplicaTables(regions: string[], timeout?: Duration, waitForReplicationToFinish?: boolean) { const stack = Stack.of(this); if (!Token.isUnresolved(stack.region) && regions.includes(stack.region)) { @@ -1524,6 +1540,7 @@ export class Table extends TableBase { properties: { TableName: this.tableName, Region: region, + SkipReplicationCompletedWait: waitForReplicationToFinish === undefined ? undefined : !waitForReplicationToFinish, }, }); currentRegion.node.addDependency( diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 56ab48f19f501..a095bff84ca25 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -2438,6 +2438,60 @@ describe('global', () => { }); }); + test('create replicas without waiting to finish replication', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new Table(stack, 'Table', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + replicationRegions: [ + 'eu-west-2', + 'eu-central-1', + ], + waitForReplicationToFinish: false, + }); + + // THEN + expect(stack).toHaveResource('Custom::DynamoDBReplica', { + Properties: { + TableName: { + Ref: 'TableCD117FA1', + }, + Region: 'eu-west-2', + SkipReplicationCompletedWait: true, + }, + Condition: 'TableStackRegionNotEqualseuwest2A03859E7', + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('Custom::DynamoDBReplica', { + Properties: { + TableName: { + Ref: 'TableCD117FA1', + }, + Region: 'eu-central-1', + SkipReplicationCompletedWait: true, + }, + Condition: 'TableStackRegionNotEqualseucentral199D46FC0', + }, ResourcePart.CompleteDefinition); + + expect(SynthUtils.toCloudFormation(stack).Conditions).toEqual({ + TableStackRegionNotEqualseuwest2A03859E7: { + 'Fn::Not': [ + { 'Fn::Equals': ['eu-west-2', { Ref: 'AWS::Region' }] }, + ], + }, + TableStackRegionNotEqualseucentral199D46FC0: { + 'Fn::Not': [ + { 'Fn::Equals': ['eu-central-1', { Ref: 'AWS::Region' }] }, + ], + }, + }); + }); + test('grantReadData', () => { const stack = new Stack(); const table = new Table(stack, 'Table', { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index 2c293dc3173de..67634b3cc95e0 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -320,14 +320,14 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { // Determine the desired task count (minimum) and maximum scaling capacity if (!this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT)) { - this.minCapacity = props.minScalingCapacity || this.desiredCount; + this.minCapacity = props.minScalingCapacity ?? this.desiredCount; this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); } else { if (props.desiredTaskCount != null) { - this.minCapacity = props.minScalingCapacity || this.desiredCount; + this.minCapacity = props.minScalingCapacity ?? this.desiredCount; this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); } else { - this.minCapacity = props.minScalingCapacity || 1; + this.minCapacity = props.minScalingCapacity ?? 1; this.maxCapacity = props.maxScalingCapacity || 2; } } diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json index f43e4e5aa4e0b..48bab449fe610 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json @@ -642,7 +642,7 @@ "Type": "AWS::ApplicationAutoScaling::ScalableTarget", "Properties": { "MaxCapacity": 2, - "MinCapacity": 1, + "MinCapacity": 0, "ResourceId": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.ts index 540b8d2302e9a..a0c679f4afaa8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.ts @@ -15,6 +15,7 @@ new QueueProcessingFargateService(stack, 'QueueProcessingService', { vpc, memoryLimitMiB: 512, image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')), + minScalingCapacity: 0, }); app.synth(); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts b/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts index 80ec042626153..cfaff590b3eb1 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts @@ -54,5 +54,7 @@ export function fromServiceAtrributes(scope: Construct, id: string, attrs: Servi public readonly serviceName = name; public readonly cluster = attrs.cluster; } - return new Import(scope, id); + return new Import(scope, id, { + environmentFromArn: arn, + }); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index 48da04142ff3c..21d4292346974 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -3292,6 +3292,8 @@ describe('ec2 service', () => { expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); expect(service.serviceName).toEqual('my-http-service'); + expect(service.env.account).toEqual('123456789012'); + expect(service.env.region).toEqual('us-west-2'); }); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index ef4ceae4ccedc..71866a116443f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -2124,6 +2124,8 @@ describe('fargate service', () => { expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); expect(service.serviceName).toEqual('my-http-service'); + expect(service.env.account).toEqual('123456789012'); + expect(service.env.region).toEqual('us-west-2'); }); diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 7505cdf463a1b..fffd20decd8d9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -220,6 +220,10 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * Specify the version of CloudWatch Lambda insights to use for monitoring * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights.html * + * When used with `DockerImageFunction` or `DockerImageCode`, the Docker image should have + * the Lambda insights agent installed. + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-Getting-Started-docker.html + * * @default - No Lambda Insights */ readonly insightsVersion?: LambdaInsightsVersion; @@ -782,9 +786,7 @@ export class Function extends FunctionBase { } // Configure Lambda insights - if (props.insightsVersion !== undefined) { - this.configureLambdaInsights(props.insightsVersion); - } + this.configureLambdaInsights(props); } /** @@ -912,8 +914,15 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett * * https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html */ - private configureLambdaInsights(insightsVersion: LambdaInsightsVersion): void { - this.addLayers(LayerVersion.fromLayerVersionArn(this, 'LambdaInsightsLayer', insightsVersion.layerVersionArn)); + private configureLambdaInsights(props: FunctionProps): void { + if (props.insightsVersion === undefined) { + return; + } + if (props.runtime !== Runtime.FROM_IMAGE) { + // Layers cannot be added to Lambda container images. The image should have the insights agent installed. + // See https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-Getting-Started-docker.html + this.addLayers(LayerVersion.fromLayerVersionArn(this, 'LambdaInsightsLayer', props.insightsVersion.layerVersionArn)); + } this.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy')); } diff --git a/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts b/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts index 990c0c7fb7405..762df158da6f4 100644 --- a/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/lambda-insights.test.ts @@ -1,5 +1,6 @@ import '@aws-cdk/assert-internal/jest'; import { MatchStyle } from '@aws-cdk/assert-internal'; +import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; import * as lambda from '../lib'; @@ -313,4 +314,43 @@ describe('lambda-insights', () => { // On synthesis it should not throw an error expect(() => app.synth()).not.toThrow(); }); + + test('insights layer is skipped for container images and the role is updated', () => { + const stack = new cdk.Stack(); + new lambda.DockerImageFunction(stack, 'MyFunction', { + code: lambda.DockerImageCode.fromEcr(ecr.Repository.fromRepositoryArn(stack, 'MyRepo', + 'arn:aws:ecr:us-east-1:0123456789:repository/MyRepo')), + insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_98_0, + }); + + expect(stack).toCountResources('AWS::Lambda::LayerVersion', 0); + + expect(stack).toHaveResourceLike('AWS::IAM::Role', { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Principal': { + 'Service': 'lambda.amazonaws.com', + }, + }, + ], + }, + 'ManagedPolicyArns': [ + { }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy', + ], + ], + }, + ], + }); + }); }); diff --git a/packages/@aws-cdk/aws-msk/README.md b/packages/@aws-cdk/aws-msk/README.md index ce86c6a477c45..664ec4f66c973 100644 --- a/packages/@aws-cdk/aws-msk/README.md +++ b/packages/@aws-cdk/aws-msk/README.md @@ -29,7 +29,7 @@ The following example creates an MSK Cluster. import * as msk from '@aws-cdk/aws-msk'; const cluster = new Cluster(this, 'Cluster', { - kafkaVersion: msk.KafkaVersion.V2_6_1, + kafkaVersion: msk.KafkaVersion.V2_8_1, vpc, }); ``` diff --git a/packages/@aws-cdk/aws-msk/lib/cluster-version.ts b/packages/@aws-cdk/aws-msk/lib/cluster-version.ts index 0d3b511aae59e..b04c154b76054 100644 --- a/packages/@aws-cdk/aws-msk/lib/cluster-version.ts +++ b/packages/@aws-cdk/aws-msk/lib/cluster-version.ts @@ -47,6 +47,11 @@ export class KafkaVersion { */ public static readonly V2_8_0 = KafkaVersion.of('2.8.0'); + /** + * Kafka version 2.8.1 + */ + public static readonly V2_8_1 = KafkaVersion.of('2.8.1'); + /** * Custom cluster version * @param version custom version number diff --git a/packages/@aws-cdk/aws-msk/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-msk/test/integ.cluster.expected.json index 9a0d0db6e2325..2b523706bd3e2 100644 --- a/packages/@aws-cdk/aws-msk/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-msk/test/integ.cluster.expected.json @@ -95,15 +95,15 @@ "VPCPublicSubnet1NATGatewayE0556630": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet1EIP6AD938E8", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet1SubnetB4246D30" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "VPCPublicSubnet2NATGateway3C070193": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet2EIP4947BC00", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet2Subnet74179F39" - }, "Tags": [ { "Key": "Name", @@ -399,7 +399,7 @@ } }, "ClusterName": "integ-test", - "KafkaVersion": "2.6.1", + "KafkaVersion": "2.8.1", "NumberOfBrokerNodes": 2, "EncryptionInfo": { "EncryptionInTransit": { @@ -524,7 +524,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD" + "Ref": "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3Bucket4C71F166" }, "S3Key": { "Fn::Join": [ @@ -537,7 +537,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" + "Ref": "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3VersionKey0124EFC4" } ] } @@ -550,7 +550,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" + "Ref": "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3VersionKey0124EFC4" } ] } @@ -576,17 +576,17 @@ } }, "Parameters": { - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD": { + "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3Bucket4C71F166": { "Type": "String", - "Description": "S3 bucket for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "S3 bucket for asset \"1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2d\"" }, - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A": { + "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dS3VersionKey0124EFC4": { "Type": "String", - "Description": "S3 key for asset version \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "S3 key for asset version \"1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2d\"" }, - "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4ArtifactHash580E429C": { + "AssetParameters1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2dArtifactHash6350D824": { "Type": "String", - "Description": "Artifact hash for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" + "Description": "Artifact hash for asset \"1c4eb88f5a8270f387281dcff6e3493840634113c4d57044f4aff74e3ef94c2d\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-msk/test/integ.cluster.ts b/packages/@aws-cdk/aws-msk/test/integ.cluster.ts index c05fa496d7210..06ad0893d20d7 100644 --- a/packages/@aws-cdk/aws-msk/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-msk/test/integ.cluster.ts @@ -10,7 +10,7 @@ const vpc = new ec2.Vpc(stack, 'VPC', { maxAzs: 2 }); const cluster = new msk.Cluster(stack, 'Cluster', { clusterName: 'integ-test', - kafkaVersion: msk.KafkaVersion.V2_6_1, + kafkaVersion: msk.KafkaVersion.V2_8_1, vpc, removalPolicy: cdk.RemovalPolicy.DESTROY, }); diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter.ts b/packages/@aws-cdk/aws-ssm/lib/parameter.ts index cbec2073cbe1f..29c0eb8a33f03 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter.ts @@ -137,6 +137,13 @@ export interface StringParameterProps extends ParameterOptions { * @default ParameterType.STRING */ readonly type?: ParameterType; + + /** + * The data type of the parameter, such as `text` or `aws:ec2:image`. + * + * @default - undefined + */ + readonly dataType?: ParameterDataType; } /** @@ -217,6 +224,20 @@ export enum ParameterType { AWS_EC2_IMAGE_ID = 'AWS::EC2::Image::Id', } +/** + * SSM parameter data type + */ +export enum ParameterDataType { + /** + * Text + */ + TEXT = 'text', + /** + * Aws Ec2 Image + */ + AWS_EC2_IMAGE = 'aws:ec2:image', +} + /** * SSM parameter tier */ @@ -436,12 +457,17 @@ export class StringParameter extends ParameterBase implements IStringParameter { throw new Error('Description cannot be longer than 1024 characters.'); } + if (props.type && props.type === ParameterType.AWS_EC2_IMAGE_ID) { + throw new Error('The type must either be ParameterType.STRING or ParameterType.STRING_LIST. Did you mean to set dataType: ParameterDataType.AWS_EC2_IMAGE instead?'); + } + const resource = new ssm.CfnParameter(this, 'Resource', { allowedPattern: props.allowedPattern, description: props.description, name: this.physicalName, tier: props.tier, type: props.type || ParameterType.STRING, + dataType: props.dataType, value: props.stringValue, }); diff --git a/packages/@aws-cdk/aws-ssm/test/parameter.test.ts b/packages/@aws-cdk/aws-ssm/test/parameter.test.ts index 17b2af748d9ef..45fbc70f9d926 100644 --- a/packages/@aws-cdk/aws-ssm/test/parameter.test.ts +++ b/packages/@aws-cdk/aws-ssm/test/parameter.test.ts @@ -28,6 +28,34 @@ test('creating a String SSM Parameter', () => { }); }); +test('type cannot be specified as AWS_EC2_IMAGE_ID', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => new ssm.StringParameter(stack, 'myParam', { + stringValue: 'myValue', + type: ssm.ParameterType.AWS_EC2_IMAGE_ID, + })).toThrow('The type must either be ParameterType.STRING or ParameterType.STRING_LIST. Did you mean to set dataType: ParameterDataType.AWS_EC2_IMAGE instead?'); +}); + +test('dataType can be specified', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ssm.StringParameter(stack, 'myParam', { + stringValue: 'myValue', + dataType: ssm.ParameterDataType.AWS_EC2_IMAGE, + }); + + // THEN + expect(stack).toHaveResource('AWS::SSM::Parameter', { + Value: 'myValue', + DataType: 'aws:ec2:image', + }); +}); + test('expect String SSM Parameter to have tier properly set', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/cfnspec/test/filtered-specification.test.ts b/packages/@aws-cdk/cfnspec/test/filtered-specification.test.ts index a4bd359517da1..e9f18346699ea 100644 --- a/packages/@aws-cdk/cfnspec/test/filtered-specification.test.ts +++ b/packages/@aws-cdk/cfnspec/test/filtered-specification.test.ts @@ -14,7 +14,7 @@ test('filteredSpecification(s => s.startsWith("AWS::S3::")', () => { }); for (const name of resourceTypes().sort()) { - test(`filteredSpecification(${JSON.stringify(name)})`, () => { + describe(`filteredSpecification(${JSON.stringify(name)})`, () => { const filteredSpec = filteredSpecification(name); expect(filteredSpec).not.toEqual(specification); expect(filteredSpec.ResourceTypes).not.toEqual({}); diff --git a/packages/@aws-cdk/cfnspec/test/spec-validators.ts b/packages/@aws-cdk/cfnspec/test/spec-validators.ts index 0c49c6bfa1a03..c8993885332fb 100644 --- a/packages/@aws-cdk/cfnspec/test/spec-validators.ts +++ b/packages/@aws-cdk/cfnspec/test/spec-validators.ts @@ -1,3 +1,4 @@ +/* eslint-disable jest/no-export */ import * as schema from '../lib/schema'; export function validateSpecification(specification: schema.Specification) { @@ -7,26 +8,30 @@ export function validateSpecification(specification: schema.Specification) { function validateResourceTypes(specification: schema.Specification) { for (const typeName of Object.keys(specification.ResourceTypes)) { - expect(typeName).toBeTruthy(); - const type = specification.ResourceTypes[typeName]; - expect(type.Documentation).not.toBeNull(); - if (type.ScrutinyType) { - expect(schema.isResourceScrutinyType(type.ScrutinyType)).toBeTruthy(); - } - if (type.Properties) { validateProperties(typeName, type.Properties, specification); } - if (type.Attributes) { validateAttributes(typeName, type.Attributes, specification); } + describe(typeName, () => { + expect(typeName).toBeTruthy(); + const type = specification.ResourceTypes[typeName]; + expect(type.Documentation).not.toBeNull(); + if (type.ScrutinyType) { + expect(schema.isResourceScrutinyType(type.ScrutinyType)).toBeTruthy(); + } + if (type.Properties) { validateProperties(typeName, type.Properties, specification); } + if (type.Attributes) { validateAttributes(typeName, type.Attributes, specification); } + }); } } function validatePropertyTypes(specification: schema.Specification) { for (const typeName of Object.keys(specification.PropertyTypes)) { - expect(typeName).toBeTruthy(); - const type = specification.PropertyTypes[typeName]; - if (schema.isRecordType(type)) { - validateProperties(typeName, type.Properties, specification); - } else { - validateProperties(typeName, { '': type }, specification); - } + describe(`PropertyType ${typeName}`, () => { + expect(typeName).toBeTruthy(); + const type = specification.PropertyTypes[typeName]; + if (schema.isRecordType(type)) { + validateProperties(typeName, type.Properties, specification); + } else { + validateProperties(typeName, { '': type }, specification); + } + }); } } @@ -37,85 +42,87 @@ function validateProperties( const expectedKeys = ['Documentation', 'Required', 'UpdateType', 'ScrutinyType']; for (const name of Object.keys(properties)) { - const property = properties[name]; - expect(property.Documentation).not.toEqual(''); - expect(!property.UpdateType || schema.isUpdateType(property.UpdateType)).toBeTruthy(); - if (property.ScrutinyType !== undefined) { - expect(schema.isPropertyScrutinyType(property.ScrutinyType)).toBeTruthy(); - } + test(`Property ${name}`, () => { + const property = properties[name]; + expect(property.Documentation).not.toEqual(''); + expect(!property.UpdateType || schema.isUpdateType(property.UpdateType)).toBeTruthy(); + if (property.ScrutinyType !== undefined) { + expect(schema.isPropertyScrutinyType(property.ScrutinyType)).toBeTruthy(); + } + + if (schema.isPrimitiveProperty(property)) { + expect(schema.isPrimitiveType(property.PrimitiveType)).toBeTruthy(); + expectedKeys.push('PrimitiveType'); - if (schema.isPrimitiveProperty(property)) { - expect(schema.isPrimitiveType(property.PrimitiveType)).toBeTruthy(); - expectedKeys.push('PrimitiveType'); + } else if (schema.isPrimitiveListProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'PrimitiveItemType'); + expect(schema.isPrimitiveType(property.PrimitiveItemType)).toBeTruthy(); - } else if (schema.isPrimitiveListProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'PrimitiveItemType'); - expect(schema.isPrimitiveType(property.PrimitiveItemType)).toBeTruthy(); + } else if (schema.isPrimitiveMapProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'PrimitiveItemType', 'Type'); + expect(schema.isPrimitiveType(property.PrimitiveItemType)).toBeTruthy(); + expect(property.DuplicatesAllowed).toBeFalsy(); - } else if (schema.isPrimitiveMapProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'PrimitiveItemType', 'Type'); - expect(schema.isPrimitiveType(property.PrimitiveItemType)).toBeTruthy(); - expect(property.DuplicatesAllowed).toBeFalsy(); + } else if (schema.isComplexListProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'Type'); + expect(property.ItemType).toBeTruthy(); + if (property.ItemType !== 'Tag') { + const fqn = `${typeName.split('.')[0]}.${property.ItemType}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); + } - } else if (schema.isComplexListProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'Type'); - expect(property.ItemType).toBeTruthy(); - if (property.ItemType !== 'Tag') { + } else if (schema.isMapOfStructsProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'Type'); + expect(property.ItemType).toBeTruthy(); const fqn = `${typeName.split('.')[0]}.${property.ItemType}`; const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; expect(resolvedType).toBeTruthy(); - } + expect(property.DuplicatesAllowed).toBeFalsy(); + + } else if (schema.isMapOfListsOfPrimitivesProperty(property)) { + expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'PrimitiveItemItemType', 'Type'); + expect(schema.isPrimitiveType(property.PrimitiveItemItemType)).toBeTruthy(); + expect(property.DuplicatesAllowed).toBeFalsy(); + + } else if (schema.isComplexProperty(property)) { + expectedKeys.push('Type'); + expect(property.Type).toBeTruthy(); + const fqn = `${typeName.split('.')[0]}.${property.Type}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); - } else if (schema.isMapOfStructsProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'Type'); - expect(property.ItemType).toBeTruthy(); - const fqn = `${typeName.split('.')[0]}.${property.ItemType}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); - expect(property.DuplicatesAllowed).toBeFalsy(); - - } else if (schema.isMapOfListsOfPrimitivesProperty(property)) { - expectedKeys.push('Type', 'DuplicatesAllowed', 'ItemType', 'PrimitiveItemItemType', 'Type'); - expect(schema.isPrimitiveType(property.PrimitiveItemItemType)).toBeTruthy(); - expect(property.DuplicatesAllowed).toBeFalsy(); - - } else if (schema.isComplexProperty(property)) { - expectedKeys.push('Type'); - expect(property.Type).toBeTruthy(); - const fqn = `${typeName.split('.')[0]}.${property.Type}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); - - } else if (schema.isUnionProperty(property)) { - expectedKeys.push('PrimitiveTypes', 'PrimitiveItemTypes', 'ItemTypes', 'Types'); - if (property.PrimitiveTypes) { - for (const type of property.PrimitiveTypes) { - expect(schema.isPrimitiveType(type)).toBeTruthy(); + } else if (schema.isUnionProperty(property)) { + expectedKeys.push('PrimitiveTypes', 'PrimitiveItemTypes', 'ItemTypes', 'Types'); + if (property.PrimitiveTypes) { + for (const type of property.PrimitiveTypes) { + expect(schema.isPrimitiveType(type)).toBeTruthy(); + } } - } - if (property.ItemTypes) { - for (const type of property.ItemTypes) { - const fqn = `${typeName.split('.')[0]}.${type}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); + if (property.ItemTypes) { + for (const type of property.ItemTypes) { + const fqn = `${typeName.split('.')[0]}.${type}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); + } } - } - if (property.Types) { - for (const type of property.Types) { - const fqn = `${typeName.split('.')[0]}.${type}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); + if (property.Types) { + for (const type of property.Types) { + const fqn = `${typeName.split('.')[0]}.${type}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); + } } - } - } else { - // eslint-disable-next-line no-console - console.error(`${typeName}.Properties.${name} does not declare a type.` + - `Property definition is: ${JSON.stringify(property, undefined, 2)}`); - expect(false).toBeTruthy(); - } + } else { + // eslint-disable-next-line no-console + console.error(`${typeName}.Properties.${name} does not declare a type.` + + `Property definition is: ${JSON.stringify(property, undefined, 2)}`); + expect(false).toBeTruthy(); + } - expect(without(Object.keys(property), expectedKeys)).toEqual([]); + expect(without(Object.keys(property), expectedKeys)).toEqual([]); + }); } } @@ -124,29 +131,31 @@ function validateAttributes( attributes: { [name: string]: schema.Attribute }, specification: schema.Specification) { for (const name of Object.keys(attributes)) { - const attribute = attributes[name]; - expect(('Type' in attribute)).not.toEqual(('PrimitiveType' in attribute)); - if (schema.isPrimitiveAttribute(attribute)) { - expect(schema.isListAttribute(attribute)).toBeFalsy(); - expect(schema.isPrimitiveType(attribute.PrimitiveType)).toBeTruthy(); - expect(('PrimitiveItemType' in attribute)).toBeFalsy(); - expect(('ItemType' in attribute)).toBeFalsy(); - } else if (schema.isPrimitiveListAttribute(attribute)) { - expect(schema.isComplexListAttribute(attribute)).toBeFalsy(); - expect(schema.isPrimitiveType(attribute.PrimitiveItemType)).toBeTruthy(); - expect(('ItemType' in attribute)).toBeFalsy(); - } else if (schema.isComplexListAttribute(attribute)) { - expect(attribute.ItemType).toBeTruthy(); - const fqn = `${typeName.split('.')[0]}.${attribute.ItemType}`; - const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; - expect(resolvedType).toBeTruthy(); - expect(('PrimitiveItemType' in attribute)).toBeFalsy(); - } else if (schema.isPrimitiveMapAttribute(attribute)) { - expect(schema.isPrimitiveType(attribute.PrimitiveItemType)).toBeTruthy(); - expect(('ItemType' in attribute)).toBeFalsy(); - } else { - expect(false).toBeTruthy(); // `${typeName}.Attributes.${name} has a valid type`); - } + test(`Attribute ${name}`, () => { + const attribute = attributes[name]; + expect(('Type' in attribute)).not.toEqual(('PrimitiveType' in attribute)); + if (schema.isPrimitiveAttribute(attribute)) { + expect(schema.isListAttribute(attribute)).toBeFalsy(); + expect(schema.isPrimitiveType(attribute.PrimitiveType)).toBeTruthy(); + expect(('PrimitiveItemType' in attribute)).toBeFalsy(); + expect(('ItemType' in attribute)).toBeFalsy(); + } else if (schema.isPrimitiveListAttribute(attribute)) { + expect(schema.isComplexListAttribute(attribute)).toBeFalsy(); + expect(schema.isPrimitiveType(attribute.PrimitiveItemType)).toBeTruthy(); + expect(('ItemType' in attribute)).toBeFalsy(); + } else if (schema.isComplexListAttribute(attribute)) { + expect(attribute.ItemType).toBeTruthy(); + const fqn = `${typeName.split('.')[0]}.${attribute.ItemType}`; + const resolvedType = specification.PropertyTypes && specification.PropertyTypes[fqn]; + expect(resolvedType).toBeTruthy(); + expect(('PrimitiveItemType' in attribute)).toBeFalsy(); + } else if (schema.isPrimitiveMapAttribute(attribute)) { + expect(schema.isPrimitiveType(attribute.PrimitiveItemType)).toBeTruthy(); + expect(('ItemType' in attribute)).toBeFalsy(); + } else { + expect(false).toBeTruthy(); // `${typeName}.Attributes.${name} has a valid type`); + } + }); } } diff --git a/packages/@aws-cdk/core/lib/fs/fingerprint.ts b/packages/@aws-cdk/core/lib/fs/fingerprint.ts index 4abfbe93c61bc..050240f821601 100644 --- a/packages/@aws-cdk/core/lib/fs/fingerprint.ts +++ b/packages/@aws-cdk/core/lib/fs/fingerprint.ts @@ -53,24 +53,26 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions return hash.digest('hex'); function _processFileOrDirectory(symbolicPath: string, isRootDir: boolean = false, realPath = symbolicPath) { - const relativePath = path.relative(fileOrDirectory, symbolicPath); - if (!isRootDir && ignoreStrategy.ignores(symbolicPath)) { return; } const stat = fs.lstatSync(realPath); + // Use relative path as hash component. Normalize it with forward slashes to ensure + // same hash on Windows and Linux. + const hashComponent = path.relative(fileOrDirectory, symbolicPath).replace(/\\/g, '/'); + if (stat.isSymbolicLink()) { const linkTarget = fs.readlinkSync(realPath); const resolvedLinkTarget = path.resolve(path.dirname(realPath), linkTarget); if (shouldFollow(follow, rootDirectory, resolvedLinkTarget)) { _processFileOrDirectory(symbolicPath, false, resolvedLinkTarget); } else { - _hashField(hash, `link:${relativePath}`, linkTarget); + _hashField(hash, `link:${hashComponent}`, linkTarget); } } else if (stat.isFile()) { - _hashField(hash, `file:${relativePath}`, contentFingerprint(realPath)); + _hashField(hash, `file:${hashComponent}`, contentFingerprint(realPath)); } else if (stat.isDirectory()) { for (const item of fs.readdirSync(realPath).sort()) { _processFileOrDirectory(path.join(symbolicPath, item), false, path.join(realPath, item)); diff --git a/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts b/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts index 8187778de5453..f1d22f891a197 100644 --- a/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts +++ b/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts @@ -178,4 +178,22 @@ describe('fs fingerprint', () => { }); }); + + test('normalizes relative path', () => { + // Simulate a Windows path.relative() + const originalPathRelative = path.relative; + const pathRelativeSpy = jest.spyOn(path, 'relative').mockImplementation((from: string, to: string): string => { + return originalPathRelative(from, to).replace(/\//g, '\\'); + }); + + const hash1 = FileSystem.fingerprint(path.join(__dirname, 'fixtures', 'test1')); + + // Restore Linux behavior + pathRelativeSpy.mockRestore(); + + const hash2 = FileSystem.fingerprint(path.join(__dirname, 'fixtures', 'test1')); + + // Relative paths are normalized + expect(hash1).toEqual(hash2); + }); }); diff --git a/packages/@aws-cdk/pipelines/ORIGINAL_API.md b/packages/@aws-cdk/pipelines/ORIGINAL_API.md index 73ac108d9c67f..447e39bb09bc2 100644 --- a/packages/@aws-cdk/pipelines/ORIGINAL_API.md +++ b/packages/@aws-cdk/pipelines/ORIGINAL_API.md @@ -41,7 +41,9 @@ all commands necessary to do a full CDK build and synth, so do include installing dependencies and running the CDK CLI. For example, the old API: ```ts -SimpleSynthAction.standardNpmSynth({ +const sourceArtifact = new codepipeline.Artifact(); +const cloudAssemblyArtifact = new codepipeline.Artifact(); +pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, @@ -54,8 +56,10 @@ SimpleSynthAction.standardNpmSynth({ Becomes: ```ts -new ShellStep('Synth', { - input: /* source */, +new pipelines.ShellStep('Synth', { + input: pipelines.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', @@ -71,7 +75,7 @@ You can use any of the factory functions on `CodePipelineSource`. For example, for a GitHub source, the following old API: ```ts -sourceAction: new codepipeline_actions.GitHubSourceAction({ +sourceAction: new cpactions.GitHubSourceAction({ actionName: 'GitHub', output: sourceArtifact, // Replace these with your actual GitHub project name @@ -84,8 +88,8 @@ sourceAction: new codepipeline_actions.GitHubSourceAction({ Translates into: ```ts -input: CodePipelineSource.gitHub('OWNER/REPO', 'main', { - authentication: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), +input: pipelines.CodePipelineSource.gitHub('OWNER/REPO', 'main', { + authentication: cdk.SecretValue.secretsManager('GITHUB_TOKEN_NAME'), }), ``` @@ -111,8 +115,9 @@ putting manual approvals in `pre` steps, and automated approvals in `post` steps For example, specifying a manual approval on a stage deployment in old API: ```ts +declare const pipeline: pipelines.CdkPipeline; const stage = pipeline.addApplicationStage(...); -stage.addAction(new ManualApprovalAction({ +stage.addAction(new pipelines.ManualApprovalAction({ actionName: 'ManualApproval', runOrder: testingStage.nextSequentialRunOrder(), })); @@ -121,9 +126,10 @@ stage.addAction(new ManualApprovalAction({ Becomes: ```ts -pipeline.addStage(..., { +const stage = new MyApplicationStage(this, 'MyApplication'); +pipeline.addStage(stage, { pre: [ - new ManualApprovalStep('ManualApproval'), + new pipelines.ManualApprovalStep('ManualApproval'), ], }); ``` @@ -139,7 +145,7 @@ For example, specifying an automated approval after a stage is deployed in the f ```ts const stage = pipeline.addApplicationStage(...); -stage.addActions(new ShellScriptAction({ +stage.addActions(new pipelines.ShellScriptAction({ actionName: 'MyValidation', commands: ['curl -Ssf $VAR'], useOutputs: { @@ -153,10 +159,10 @@ stage.addActions(new ShellScriptAction({ Becomes: ```ts -const stage = new MyStage(...); +const stage = new MyApplicationStage(this, 'MyApplication'); pipeline.addStage(stage, { post: [ - new CodeBuildStep('MyValidation', { + new pipelines.CodeBuildStep('MyValidation', { commands: ['curl -Ssf $VAR'], envFromCfnOutput: { VAR: stage.cfnOutput, @@ -174,7 +180,19 @@ customizations (like `buildEnvironment`). #### Change set approvals In the old API, there were two properties that were used to add actions to the pipeline -in between the `CreateChangeSet` and `ExecuteChangeSet` actions: `manualApprovals` and `extraRunOrderSpace`. These are not supported in the new API. +in between the `CreateChangeSet` and `ExecuteChangeSet` actions: `manualApprovals` and `extraRunOrderSpace`. +This can be achieved in the modern API via the `stackSteps` property, which allows steps to be added +at the stack level: + +```ts +const stage = new MyApplicationStage(this, 'MyApplication'); +pipeline.addStage(stage, { + stackSteps: [{ + stack: stage.stack1, + changeSet: [new pipelines.ManualApprovalStep('ChangeSet Approval')], + }], +}); +``` ### Custom CodePipeline Actions @@ -190,7 +208,6 @@ 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'; /** @@ -203,20 +220,20 @@ class MyPipelineStack extends Stack { const sourceArtifact = new codepipeline.Artifact(); const cloudAssemblyArtifact = new codepipeline.Artifact(); - const pipeline = new CdkPipeline(this, 'Pipeline', { + const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { cloudAssemblyArtifact, - sourceAction: new codepipeline_actions.GitHubSourceAction({ + sourceAction: new cpactions.GitHubSourceAction({ actionName: 'GitHub', output: sourceArtifact, - oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), + oauthToken: cdk.SecretValue.secretsManager('GITHUB_TOKEN_NAME'), // Replace these with your actual GitHub project name owner: 'OWNER', repo: 'REPO', branch: 'main', // default: 'master' }), - synthAction: SimpleSynthAction.standardNpmSynth({ + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, @@ -274,21 +291,21 @@ class MyPipelineStack extends Stack { const sourceArtifact = new codepipeline.Artifact(); const cloudAssemblyArtifact = new codepipeline.Artifact(); - const pipeline = new CdkPipeline(this, 'Pipeline', { + const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { pipelineName: 'MyAppPipeline', cloudAssemblyArtifact, - sourceAction: new codepipeline_actions.GitHubSourceAction({ + sourceAction: new cpactions.GitHubSourceAction({ actionName: 'GitHub', output: sourceArtifact, - oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), + oauthToken: cdk.SecretValue.secretsManager('GITHUB_TOKEN_NAME'), // Replace these with your actual GitHub project name owner: 'OWNER', repo: 'REPO', branch: 'main', // default: 'master' }), - synthAction: SimpleSynthAction.standardNpmSynth({ + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, @@ -316,7 +333,7 @@ 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', { +const codePipeline = new codepipeline.Pipeline(pipelineStack, 'CodePipeline', { stages: [ { stageName: 'CustomSource', @@ -330,7 +347,7 @@ const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { }); const app = new App(); -const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { +const cdkPipeline = new pipelines.CdkPipeline(app, 'CdkPipeline', { codePipeline, cloudAssemblyArtifact, }); @@ -360,9 +377,9 @@ using these, the source repository does not need to have a `buildspec.yml`. An e of using `SimpleSynthAction` to run a Maven build followed by a CDK synth: ```ts -const pipeline = new CdkPipeline(this, 'Pipeline', { +const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { // ... - synthAction: new SimpleSynthAction({ + synthAction: new pipelines.SimpleSynthAction({ sourceArtifact, cloudAssemblyArtifact, installCommands: ['npm install -g aws-cdk'], @@ -396,16 +413,16 @@ from the CA repo instead of NPM. class MyPipelineStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { ... - const pipeline = new CdkPipeline(this, 'Pipeline', { + const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { ... - synthAction: SimpleSynthAction.standardNpmSynth({ + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, // Use this to customize and a permissions required for the build // and synth rolePolicyStatements: [ - new PolicyStatement({ + new iam.PolicyStatement({ actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], resources: ['arn:codeartifact:repo:arn'], }), @@ -477,7 +494,7 @@ const testingStage = pipeline.addApplicationStage(new MyApplication(this, 'Testi // 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({ +testingStage.addAction(new pipelines.ManualApprovalAction({ actionName: 'ManualApproval', runOrder: testingStage.nextSequentialRunOrder(), })); @@ -522,7 +539,7 @@ In its simplest form, adding validation actions looks like this: ```ts const stage = pipeline.addApplicationStage(new MyApplication(/* ... */)); -stage.addActions(new ShellScriptAction({ +stage.addActions(new pipelines.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 @@ -563,7 +580,7 @@ const lbApp = new MyLbApplication(this, 'MyApp', { env: { /* ... */ } }); const stage = pipeline.addApplicationStage(lbApp); -stage.addActions(new ShellScriptAction({ +stage.addActions(new pipelines.ShellScriptAction({ // ... useOutputs: { // When the test is executed, this will make $URL contain the @@ -594,7 +611,7 @@ two ways. Either pass additional policy statements in the `rolePolicyStatements` property: ```ts -new ShellScriptAction({ +new pipelines.ShellScriptAction({ // ... rolePolicyStatements: [ new iam.PolicyStatement({ @@ -608,7 +625,7 @@ new ShellScriptAction({ The Action can also be used as a Grantable after having been added to a Pipeline: ```ts -const action = new ShellScriptAction({ /* ... */ }); +const action = new pipelines.ShellScriptAction({ /* ... */ }); pipeline.addStage('Test').addActions(action); bucket.grantRead(action); @@ -623,11 +640,11 @@ if they are executable shell scripts themselves). Pass the `sourceArtifact`: ```ts const sourceArtifact = new codepipeline.Artifact(); -const pipeline = new CdkPipeline(this, 'Pipeline', { +const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { // ... }); -const validationAction = new ShellScriptAction({ +const validationAction = new pipelines.ShellScriptAction({ actionName: 'TestUsingSourceArtifact', additionalArtifacts: [sourceArtifact], @@ -651,8 +668,8 @@ in the `ShellScriptAction`'s `additionalArtifacts`: const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); const integTestsArtifact = new codepipeline.Artifact('IntegTests'); -const pipeline = new CdkPipeline(this, 'Pipeline', { - synthAction: SimpleSynthAction.standardNpmSynth({ +const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact, buildCommands: ['npm run build'], @@ -666,7 +683,7 @@ const pipeline = new CdkPipeline(this, 'Pipeline', { // ... }); -const validationAction = new ShellScriptAction({ +const validationAction = new pipelines.ShellScriptAction({ actionName: 'TestUsingBuildArtifact', additionalArtifacts: [integTestsArtifact], // 'test.js' was produced from 'test/test.ts' during the synth step @@ -715,12 +732,11 @@ create an SNS Topic, subscribe your own email address, and pass it in via ```ts import * as sns from '@aws-cdk/aws-sns'; import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; -import * as pipelines from '@aws-cdk/pipelines'; const topic = new sns.Topic(this, 'SecurityChangesTopic'); topic.addSubscription(new subscriptions.EmailSubscription('test@email.com')); -const pipeline = new CdkPipeline(app, 'Pipeline', { /* ... */ }); +const pipeline = new pipelines.CdkPipeline(app, 'Pipeline', { /* ... */ }); const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), { confirmBroadeningPermissions: true, securityNotificationTopic: topic, diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 3013140344bef..bd6525542920b 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -43,14 +43,31 @@ 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 +/** The stacks for our app are minimally defined here. The internals of these * stacks aren't important, except that DatabaseStack exposes an attribute * "table" for a database table it defines, and ComputeStack accepts a reference * to this table in its properties. */ -import { DatabaseStack, ComputeStack } from '../lib/my-stacks'; -import { Construct, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; -import { CodePipeline, CodePipelineSource, ShellStep } from '@aws-cdk/pipelines'; +class DatabaseStack extends Stack { + public readonly table: dynamodb.Table; + + constructor(scope: Construct, id: string) { + super(scope, id); + this.table = new dynamodb.Table(this, 'Table', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } + }); + } +} + +interface ComputeProps { + readonly table: dynamodb.Table; +} + +class ComputeStack extends Stack { + constructor(scope: Construct, id: string, props: ComputeProps) { + super(scope, id); + } +} /** * Stack to hold the pipeline @@ -59,11 +76,11 @@ 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', { + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.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', { + input: pipelines.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: [ @@ -81,7 +98,7 @@ class MyPipelineStack extends Stack { env: { account: '123456789012', region: 'eu-west-1', - } + }, })); } } @@ -106,7 +123,7 @@ class MyApplication extends Stage { } // In your main file -new MyPipelineStack(app, 'PipelineStack', { +new MyPipelineStack(this, 'PipelineStack', { env: { account: '123456789012', region: 'eu-west-1', @@ -172,15 +189,25 @@ off temporarily, by passing `selfMutation: false` property, example: ```ts // Modern API -const pipeline = new CodePipeline(this, 'Pipeline', { +const modernPipeline = new pipelines.CodePipeline(this, 'Pipeline', { selfMutation: false, - ... + synth: new pipelines.ShellStep('Synth', { + input: pipelines.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', + ], + }), }); // Original API -const pipeline = new CdkPipeline(this, 'Pipeline', { +const cloudAssemblyArtifact = new codepipeline.Artifact(); +const originalPipeline = new pipelines.CdkPipeline(this, 'Pipeline', { selfMutating: false, - ... + cloudAssemblyArtifact, }); ``` @@ -204,10 +231,10 @@ 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 source = /* the repository source */; +declare const source: pipelines.IFileSetProducer; // the repository source -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'npm ci', @@ -224,8 +251,10 @@ 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', { +declare const source: pipelines.IFileSetProducer; // the repository source + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'cd mysubdir', @@ -254,8 +283,10 @@ look like in a number of different situations. For Yarn, the install commands are different: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +declare const source: pipelines.IFileSetProducer; // the repository source + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'yarn install --frozen-lockfile', @@ -270,8 +301,10 @@ For Python projects, remember to install the CDK CLI globally (as there is no `package.json` to automatically install it for you): ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +declare const source: pipelines.IFileSetProducer; // the repository source + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'pip install -r requirements.txt', @@ -288,8 +321,10 @@ and the Maven compilation step is automatically executed for you as you run `cdk synth`: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { +declare const source: pipelines.IFileSetProducer; // the repository source + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { input: source, commands: [ 'npm install -g aws-cdk', @@ -314,7 +349,7 @@ You will first use the AWS Console to authenticate to the source control provider, and then use the connection ARN in your pipeline definition: ```ts -CodePipelineSource.connection('org/repo', 'branch', { +pipelines.CodePipelineSource.connection('org/repo', 'branch', { connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', }); ``` @@ -328,9 +363,9 @@ you can change the name. The token should have the **repo** and **admin:repo_hoo scopes. ```ts -CodePipelineSource.gitHub('org/repo', 'branch', { +pipelines.CodePipelineSource.gitHub('org/repo', 'branch', { // This is optional - authentication: SecretValue.secretsManager('my-token'), + authentication: cdk.SecretValue.secretsManager('my-token'), }); ``` @@ -341,8 +376,8 @@ that the CodeCommit repository and then use `CodePipelineSource.codeCommit` to reference it: ```ts -const repository = codecommit.fromRepositoryName(this, 'Repository', 'my-repository'); -CodePipelineSource.codeCommit(repository); +const repository = codecommit.Repository.fromRepositoryName(this, 'Repository', 'my-repository'); +pipelines.CodePipelineSource.codeCommit(repository, 'main'); ``` ##### S3 @@ -352,7 +387,7 @@ triggered every time the file in S3 is changed: ```ts const bucket = s3.Bucket.fromBucketName(this, 'Bucket', 'my-bucket'); -CodePipelineSource.s3(bucket, 'my/source.zip'); +pipelines.CodePipelineSource.s3(bucket, 'my/source.zip'); ``` #### Additional inputs @@ -363,17 +398,17 @@ output file set can be used as an input, such as a `CodePipelineSource`, but also other `ShellStep`: ```ts -const prebuild = new ShellStep('Prebuild', { - input: CodePipelineSource.gitHub('myorg/repo1'), +const prebuild = new pipelines.ShellStep('Prebuild', { + input: pipelines.CodePipelineSource.gitHub('myorg/repo1', 'main'), primaryOutputDirectory: './build', commands: ['./build.sh'], }); -const pipeline = new CodePipeline(this, 'Pipeline', { - synth: new ShellStep('Synth', { - input: CodePipelineSource.gitHub('myorg/repo2'), +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub('myorg/repo2', 'main'), additionalInputs: { - 'subdir': CodePipelineSource.gitHub('myorg/repo3'), + 'subdir': pipelines.CodePipelineSource.gitHub('myorg/repo3', 'main'), '../siblingdir': prebuild, }, @@ -389,6 +424,7 @@ more CDK `Stages` which will be deployed to their target environments. To do so, call `pipeline.addStage()` on the Stage object: ```ts +declare const pipeline: pipelines.CodePipeline; // 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', { @@ -421,6 +457,7 @@ 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 +declare const pipeline: pipelines.CodePipeline; const europeWave = pipeline.addWave('Europe'); europeWave.addStage(new MyApplicationStage(this, 'Ireland', { env: { region: 'eu-west-1' } @@ -445,9 +482,19 @@ KMS key. Example: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { // Encrypt artifacts, required for cross-account deployments crossAccountKeys: true, + synth: new pipelines.ShellStep('Synth', { + input: pipelines.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', + ], + }), }); ``` @@ -464,19 +511,20 @@ a manual approval in the form of a `ManualApprovalStep` added to the pipeline. B pass in order to promote from the `PreProd` to the `Prod` environment: ```ts -const preprod = new MyApplicationStage(this, 'PreProd', { ... }); -const prod = new MyApplicationStage(this, 'Prod', { ... }); +declare const pipeline: pipelines.CodePipeline; +const preprod = new MyApplicationStage(this, 'PreProd'); +const prod = new MyApplicationStage(this, 'Prod'); pipeline.addStage(preprod, { post: [ - new ShellStep('Validate Endpoint', { + new pipelines.ShellStep('Validate Endpoint', { commands: ['curl -Ssf https://my.webservice.com/'], }), ], }); pipeline.addStage(prod, { pre: [ - new ManualApprovalStep('PromoteToProd'), + new pipelines.ManualApprovalStep('PromoteToProd'), ], }); ``` @@ -484,15 +532,29 @@ pipeline.addStage(prod, { You can also specify steps to be executed at the stack level. To achieve this, you can specify the stack and step via the `stackSteps` property: ```ts +class MyStacksStage extends Stage { + public readonly stack1: Stack; + public readonly stack2: Stack; + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + this.stack1 = new Stack(this, 'stack1'); + this.stack2 = new Stack(this, 'stack2'); + } +} + +declare const pipeline: pipelines.CodePipeline; +const prod = new MyStacksStage(this, 'Prod'); + pipeline.addStage(prod, { stackSteps: [{ stack: prod.stack1, - pre: [new ManualApprovalStep('Pre-Stack Check')], // Executed before stack is prepared - changeSet: [new ManualApprovalStep('ChangeSet Approval')], // Executed after stack is prepared but before the stack is deployed - post: [new ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed + pre: [new pipelines.ManualApprovalStep('Pre-Stack Check')], // Executed before stack is prepared + changeSet: [new pipelines.ManualApprovalStep('ChangeSet Approval')], // Executed after stack is prepared but before the stack is deployed + post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed }, { stack: prod.stack2, - post: [new ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed + post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed }], }); ``` @@ -507,21 +569,26 @@ To use Stack Outputs, expose the `CfnOutput` object you're interested in, and pass it to `envFromCfnOutputs` of the `ShellStep`: ```ts -class MyApplicationStage extends Stage { +class MyOutputStage extends Stage { public readonly loadBalancerAddress: CfnOutput; - // ... + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + this.loadBalancerAddress = new CfnOutput(this, 'Output', {value: 'value'}); + } } -const lbApp = new MyApplicationStage(this, 'MyApp', { /* ... */ }); +const lbApp = new MyOutputStage(this, 'MyApp'); +declare const pipeline: pipelines.CodePipeline; pipeline.addStage(lbApp, { post: [ - new ShellStep('HitEndpoint', { + new pipelines.ShellStep('HitEndpoint', { envFromCfnOutputs: { // Make the load balancer address available as $URL inside the commands URL: lbApp.loadBalancerAddress, }, commands: ['curl -Ssf $URL'], - }); + }), ], }); ``` @@ -539,12 +606,13 @@ Here's an example that captures an additional output directory in the synth step and runs tests from there: ```ts -const synth = new ShellStep('Synth', { /* ... */ }); -const pipeline = new CodePipeline(this, 'Pipeline', { synth }); +declare const synth: pipelines.ShellStep; +const stage = new MyApplicationStage(this, 'MyApplication'); +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { synth }); -pipeline.addStage(/* ... */, { +pipeline.addStage(stage, { post: [ - new ShellStep('Approve', { + new pipelines.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'], @@ -562,7 +630,9 @@ generated, use a `CodeBuildStep` instead of a `ShellStep`. This class has a numb of properties that allow you to customize various aspects of the projects: ```ts -new CodeBuildStep('Synth', { +declare const vpc: ec2.Vpc; +declare const mySecurityGroup: ec2.SecurityGroup; +new pipelines.CodeBuildStep('Synth', { // ...standard ShellStep props... commands: [/* ... */], env: { /* ... */ }, @@ -602,8 +672,20 @@ or just for the synth, asset publishing, and self-mutation projects by passing ` `assetPublishingCodeBuildDefaults`, or `selfMutationCodeBuildDefaults`: ```ts -new CodePipeline(this, 'Pipeline', { - // ... +declare const vpc: ec2.Vpc; +declare const mySecurityGroup: ec2.SecurityGroup; +new pipelines.CodePipeline(this, 'Pipeline', { + // Standard CodePipeline properties + synth: new pipelines.ShellStep('Synth', { + input: pipelines.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', + ], + }), // Defaults for all CodeBuild projects codeBuildDefaults: { @@ -644,15 +726,19 @@ doesn't have a matching class yet, you can define your own step class that exten Here's an example that adds a Jenkins step: ```ts -class MyJenkinsStep extends Step implements ICodePipelineActionFactory { - constructor(private readonly provider: codepipeline_actions.JenkinsProvider, private readonly input: FileSet) { +class MyJenkinsStep extends pipelines.Step implements pipelines.ICodePipelineActionFactory { + constructor( + private readonly provider: cpactions.JenkinsProvider, + private readonly input: pipelines.FileSet, + ) { + super('MyJenkinsStep'); } - public produceAction(stage: codepipeline.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { + public produceAction(stage: codepipeline.IStage, options: pipelines.ProduceActionOptions): pipelines.CodePipelineActionFactoryResult { // This is where you control what type of Action gets added to the // CodePipeline - stage.addAction(new codepipeline_actions.JenkinsAction({ + stage.addAction(new cpactions.JenkinsAction({ // Copy 'actionName' and 'runOrder' from the options actionName: options.actionName, runOrder: options.runOrder, @@ -700,8 +786,13 @@ stacks the pipeline is deploying), for example by the use of `LinuxBuildImage.fr you need to pass `dockerEnabledForSelfMutation: true` to the pipeline. For example: ```ts -const pipeline = new CodePipeline(this, 'Pipeline', { - // ... +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.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'], + }), // Turn this on because the pipeline uses Docker image assets dockerEnabledForSelfMutation: true, @@ -709,16 +800,16 @@ const pipeline = new CodePipeline(this, 'Pipeline', { pipeline.addWave('MyWave', { post: [ - new CodeBuildStep('RunApproval', { + new pipelines.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', { + buildImage: codebuild.LinuxBuildImage.fromAsset(this, 'Image', { directory: './docker-image', - }) + }), }, - }) + }), ], }); ``` @@ -734,8 +825,13 @@ 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 CodePipeline(this, 'Pipeline', { - // ... +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.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'], + }), // Turn this on because the application uses bundled file assets dockerEnabledForSynth: true, @@ -756,16 +852,21 @@ different environment (e.g., ECR repo) or to avoid throttling (e.g., DockerHub). ```ts const dockerHubSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'DHSecret', 'arn:aws:...'); const customRegSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'CRSecret', 'arn:aws:...'); -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 repo1 = ecr.Repository.fromRepositoryArn(this, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo1'); +const repo2 = ecr.Repository.fromRepositoryArn(this, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo2'); -const pipeline = new CodePipeline(this, 'Pipeline', { +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { dockerCredentials: [ - DockerCredential.dockerHub(dockerHubSecret), - DockerCredential.customRegistry('dockerregistry.example.com', customRegSecret), - DockerCredential.ecr([repo1, repo2]); + pipelines.DockerCredential.dockerHub(dockerHubSecret), + pipelines.DockerCredential.customRegistry('dockerregistry.example.com', customRegSecret), + pipelines.DockerCredential.ecr([repo1, repo2]), ], - // ... + synth: new pipelines.ShellStep('Synth', { + input: pipelines.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'], + }), }); ``` @@ -784,7 +885,7 @@ the **Synth**, **Self-Update**, and **Asset Publishing** actions within the ```ts const dockerHubSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'DHSecret', 'arn:aws:...'); // Only the image asset publishing actions will be granted read access to the secret. -const creds = DockerCredential.dockerHub(dockerHubSecret, { usages: [DockerCredentialUsage.ASSET_PUBLISHING] }); +const creds = pipelines.DockerCredential.dockerHub(dockerHubSecret, { usages: [pipelines.DockerCredentialUsage.ASSET_PUBLISHING] }); ``` ## CDK Environment Bootstrapping @@ -953,9 +1054,11 @@ give the synth CodeBuild execution role permissions to assume the bootstrapped lookup roles. As an example, doing so would look like this: ```ts -new CodePipeline(this, 'Pipeline', { - synth: new CodeBuildStep('Synth', { - input: // ...input... +new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.CodeBuildStep('Synth', { + input: pipelines.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: [ // Commands to load cdk.context.json from somewhere here '...', @@ -1034,10 +1137,11 @@ Pipeline You can insert the security check by using a `ConfirmPermissionsBroadening` step: ```ts +declare const pipeline: pipelines.CodePipeline; const stage = new MyApplicationStage(this, 'MyApplication'); pipeline.addStage(stage, { pre: [ - new ConfirmPermissionsBroadening('Check', { stage }), + new pipelines.ConfirmPermissionsBroadening('Check', { stage }), ], }); ``` @@ -1047,17 +1151,14 @@ create an SNS Topic, subscribe your own email address, and pass it in as as the `notificationTopic` property: ```ts -import * as sns from '@aws-cdk/aws-sns'; -import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; -import * as pipelines from '@aws-cdk/pipelines'; - +declare const pipeline: pipelines.CodePipeline; const topic = new sns.Topic(this, 'SecurityChangesTopic'); topic.addSubscription(new subscriptions.EmailSubscription('test@email.com')); const stage = new MyApplicationStage(this, 'MyApplication'); pipeline.addStage(stage, { pre: [ - new ConfirmPermissionsBroadening('Check', { + new pipelines.ConfirmPermissionsBroadening('Check', { stage, notificationTopic: topic, }), @@ -1162,19 +1263,19 @@ that bundles asset using tools run via Docker, like `aws-lambda-nodejs`, `aws-la Make sure you set the `privileged` environment variable to `true` in the synth definition: -```typescript - const pipeline = new CdkPipeline(this, 'MyPipeline', { - ... - - synthAction: SimpleSynthAction.standardNpmSynth({ - sourceArtifact: ..., - cloudAssemblyArtifact: ..., - - environment: { - privileged: true, - }, - }), - }); +```ts +const sourceArtifact = new codepipeline.Artifact(); +const cloudAssemblyArtifact = new codepipeline.Artifact(); +const pipeline = new pipelines.CdkPipeline(this, 'MyPipeline', { + cloudAssemblyArtifact, + synthAction: pipelines.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + environment: { + privileged: true, + }, + }), +}); ``` After turning on `privilegedMode: true`, you will need to do a one-time manual cdk deploy of your @@ -1201,10 +1302,11 @@ This happens because the pipeline is not self-mutating and, as a consequence, th build projects get out-of-sync with the generated templates. To fix this, make sure the `selfMutating` property is set to `true`: -```typescript -const pipeline = new CdkPipeline(this, 'MyPipeline', { +```ts +const cloudAssemblyArtifact = new codepipeline.Artifact(); +const pipeline = new pipelines.CdkPipeline(this, 'MyPipeline', { selfMutating: true, - ... + cloudAssemblyArtifact, }); ``` @@ -1240,9 +1342,9 @@ $ env CDK_NEW_BOOTSTRAP=1 npx cdk bootstrap \ See https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html for more info. ```ts -new MyStack(this, 'MyStack', { +new Stack(this, 'MyStack', { // Update this qualifier to match the one used above. - synthesizer: new DefaultStackSynthesizer({ + synthesizer: new cdk.DefaultStackSynthesizer({ qualifier: 'randchars1234', }), }); diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts index 75c1883d92419..1f03105c78ee9 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts @@ -65,11 +65,11 @@ export interface ShellStepProps { * following configuration: * * ```ts - * const script = new ShellStep('MainScript', { - * // ... - * input: MyEngineSource.gitHub('org/source1'), + * const script = new pipelines.ShellStep('MainScript', { + * commands: ['npm ci','npm run build','npx cdk synth'], + * input: pipelines.CodePipelineSource.gitHub('org/source1', 'main'), * additionalInputs: { - * '../siblingdir': MyEngineSource.gitHub('org/source2'), + * '../siblingdir': pipelines.CodePipelineSource.gitHub('org/source2', 'main'), * } * }); * ``` diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts index b6d10b03f2f67..382bed08028ff 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -26,7 +26,7 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc * Pass in the owner and repository in a single string, like this: * * ```ts - * CodePipelineSource.gitHub('owner/repo', 'main'); + * pipelines.CodePipelineSource.gitHub('owner/repo', 'main'); * ``` * * Authentication will be done by a secret called `github-token` in AWS @@ -51,8 +51,8 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc * Example: * * ```ts - * const bucket: IBucket = ... - * CodePipelineSource.s3(bucket, { + * declare const bucket: s3.Bucket; + * pipelines.CodePipelineSource.s3(bucket, { * key: 'path/to/file.zip', * }); * ``` @@ -74,7 +74,7 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc * Example: * * ```ts - * CodePipelineSource.connection('owner/repo', 'main', { + * pipelines.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 * }); * ``` @@ -131,7 +131,6 @@ export interface GitHubSourceOptions { * * ```ts * const oauth = cdk.SecretValue.secretsManager('my-github-token'); - * new GitHubSource(this, 'GitHubSource', { authentication: oauth, ... }); * ``` * * The GitHub Personal Access Token should have these scopes: diff --git a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts index d69c2a6c89ab1..2f90df9de6f1a 100644 --- a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts +++ b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts @@ -95,9 +95,11 @@ export abstract class PipelineBase extends CoreConstruct { * Example: * * ```ts + * declare const pipeline: pipelines.CodePipeline; + * * const wave = pipeline.addWave('MyWave'); - * wave.addStage(new MyStage('Stage1', ...)); - * wave.addStage(new MyStage('Stage2', ...)); + * wave.addStage(new MyApplicationStage(this, 'Stage1')); + * wave.addStage(new MyApplicationStage(this, 'Stage2')); * ``` */ public addWave(id: string, options?: WaveOptions) { diff --git a/packages/@aws-cdk/pipelines/rosetta/default.ts-fixture b/packages/@aws-cdk/pipelines/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..61a973840f007 --- /dev/null +++ b/packages/@aws-cdk/pipelines/rosetta/default.ts-fixture @@ -0,0 +1,29 @@ +// Fixture with packages imported, but nothing else +import { Construct, CfnOutput, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; +import cdk = require('@aws-cdk/core'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import cpactions = require('@aws-cdk/aws-codepipeline-actions'); +import codebuild = require('@aws-cdk/aws-codebuild'); +import codecommit = require('@aws-cdk/aws-codecommit'); +import dynamodb = require('@aws-cdk/aws-dynamodb'); +import ecr = require('@aws-cdk/aws-ecr'); +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); +import pipelines = require('@aws-cdk/pipelines'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); +import sns = require('@aws-cdk/aws-sns'); +import subscriptions = require('@aws-cdk/aws-sns-subscriptions'); +import s3 = require('@aws-cdk/aws-s3'); + +class MyApplicationStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + } +} + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/scripts/bump.js b/scripts/bump.js index 14bb70fb0a5c8..3daca6711605f 100755 --- a/scripts/bump.js +++ b/scripts/bump.js @@ -74,7 +74,7 @@ async function main() { console.error("🎉 Calling our 'cdk-release' package to make the bump"); console.error("ℹī¸ Set the LEGACY_BUMP env variable to use the old 'standard-version' bump instead"); const cdkRelease = require('@aws-cdk/cdk-release'); - cdkRelease(opts); + cdkRelease.createRelease(opts); } } diff --git a/scripts/create-release-notes.js b/scripts/create-release-notes.js new file mode 100755 index 0000000000000..1e6096c38bc00 --- /dev/null +++ b/scripts/create-release-notes.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +const cdkRelease = require('@aws-cdk/cdk-release'); +const ver = require('./resolve-version'); + +async function main() { + await cdkRelease.createReleaseNotes({ + versionFile: ver.versionFile, + changelogFile: ver.changelogFile, + alphaChangelogFile: ver.alphaChangelogFile, + releaseNotesFile: 'RELEASE_NOTES.md', + }); +} + +main().catch(err => { + console.error(err.stack); + process.exit(1); +}); diff --git a/tools/@aws-cdk/cdk-release/lib/index.ts b/tools/@aws-cdk/cdk-release/lib/index.ts index b908e4cccdc7d..d058aec7e98ab 100644 --- a/tools/@aws-cdk/cdk-release/lib/index.ts +++ b/tools/@aws-cdk/cdk-release/lib/index.ts @@ -1,16 +1,17 @@ -import * as path from 'path'; -import * as fs from 'fs-extra'; import { getConventionalCommitsFromGitHistory } from './conventional-commits'; import { defaults } from './defaults'; import { bump } from './lifecycles/bump'; import { writeChangelogs } from './lifecycles/changelog'; import { commit } from './lifecycles/commit'; import { debug, debugObject } from './private/print'; -import { PackageInfo, ReleaseOptions, Versions } from './types'; +import { PackageInfo, ReleaseOptions } from './types'; +import { readVersion } from './versions'; // eslint-disable-next-line @typescript-eslint/no-require-imports const lerna_project = require('@lerna/project'); -module.exports = async function main(opts: ReleaseOptions): Promise { +export * from './release-notes'; + +export async function createRelease(opts: ReleaseOptions): Promise { // handle the default options const args: ReleaseOptions = { ...defaults, @@ -34,15 +35,6 @@ module.exports = async function main(opts: ReleaseOptions): Promise { await commit(args, newVersion.stableVersion, [args.versionFile, ...changelogResults.map(r => r.filePath)]); }; -function readVersion(versionFile: string): Versions { - const versionPath = path.resolve(process.cwd(), versionFile); - const contents = JSON.parse(fs.readFileSync(versionPath, { encoding: 'utf-8' })); - return { - stableVersion: contents.version, - alphaVersion: contents.alphaVersion, - }; -} - function getProjectPackageInfos(): PackageInfo[] { const packages = lerna_project.Project.getPackagesSync(); diff --git a/tools/@aws-cdk/cdk-release/lib/private/files.ts b/tools/@aws-cdk/cdk-release/lib/private/files.ts index d9a68dd894c87..6009633bc72b2 100644 --- a/tools/@aws-cdk/cdk-release/lib/private/files.ts +++ b/tools/@aws-cdk/cdk-release/lib/private/files.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; -interface WriteFileOpts { +export interface WriteFileOpts { readonly dryRun?: boolean; } diff --git a/tools/@aws-cdk/cdk-release/lib/release-notes.ts b/tools/@aws-cdk/cdk-release/lib/release-notes.ts new file mode 100644 index 0000000000000..4647b4db71533 --- /dev/null +++ b/tools/@aws-cdk/cdk-release/lib/release-notes.ts @@ -0,0 +1,67 @@ +// eslint-disable-next-line @typescript-eslint/no-require-imports +import parseChangelog = require('changelog-parser'); +import { WriteFileOpts, writeFile } from './private/files'; +import { debugObject, LoggingOptions } from './private/print'; +import { Versions } from './types'; +import { readVersion } from './versions'; + +export interface ReleaseNotesOpts { + /** path to the version file for the current branch (e.g., version.v2.json) */ + versionFile: string; + /** path to the primary changelog file (e.g., 'CHANGELOG.v2.md') */ + changelogFile: string; + /** (optional) path to the independent alpha changelog file (e.g., 'CHANGELOG.v2.alpha.md') */ + alphaChangelogFile?: string; + /** path to write out the final release notes (e.g., 'RELEASE_NOTES.md'). */ + releaseNotesFile: string; +} + +/** + * Creates a release notes file from one (or more) changelog files for the current version. + * If an alpha version and alpha changelog file aren't present, this is identical to the contents + * of the (main) changelog for the current version. Otherwise, a combined release is put together + * from the contents of the stable and alpha changelogs. + */ +export async function createReleaseNotes(opts: ReleaseNotesOpts & LoggingOptions & WriteFileOpts) { + const currentVersion = readVersion(opts.versionFile); + debugObject(opts, 'Current version info', currentVersion); + + writeFile(opts, opts.releaseNotesFile, await releaseNoteContents(currentVersion, opts)); +} + +async function releaseNoteContents(currentVersion: Versions, opts: ReleaseNotesOpts) { + const stableChangelogContents = await readChangelogSection(opts.changelogFile, currentVersion.stableVersion); + // If we don't have an alpha version and distinct alpha changelog, the release notes are just the main changelog section. + if (!opts.alphaChangelogFile || !currentVersion.alphaVersion) { return stableChangelogContents; } + + const alphaChangelogContents = await readChangelogSection(opts.alphaChangelogFile, currentVersion.alphaVersion); + + // See https://github.com/aws/aws-cdk-rfcs/blob/master/text/0249-v2-experiments.md#changelog--release-notes for format + return [ + stableChangelogContents, + '---', + `## Alpha modules (${currentVersion.alphaVersion})`, + alphaChangelogContents, + ].join('\n'); +} + +async function readChangelogSection(changelogFile: string, version: string) { + const changelog = await parseChangelog(changelogFile) as Changelog; + const entry = (changelog.versions || []).find(section => section.version === version); + if (!entry) { + throw new Error(`No changelog entry found for version ${version} in ${changelogFile}`); + } + return entry.body; +} + +/** @types/changelog-parser only returns `object`; this is slightly more helpful */ +interface Changelog { + title: string; + description: string; + versions?: ChangelogVersion[]; +} +interface ChangelogVersion { + version: string; + title: string; + body: string; +} diff --git a/tools/@aws-cdk/cdk-release/lib/versions.ts b/tools/@aws-cdk/cdk-release/lib/versions.ts new file mode 100644 index 0000000000000..d92ed61a5f0e5 --- /dev/null +++ b/tools/@aws-cdk/cdk-release/lib/versions.ts @@ -0,0 +1,12 @@ +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { Versions } from './types'; + +export function readVersion(versionFile: string): Versions { + const versionPath = path.resolve(process.cwd(), versionFile); + const contents = JSON.parse(fs.readFileSync(versionPath, { encoding: 'utf-8' })); + return { + stableVersion: contents.version, + alphaVersion: contents.alphaVersion, + }; +} diff --git a/tools/@aws-cdk/cdk-release/package.json b/tools/@aws-cdk/cdk-release/package.json index 9e06c857907db..ed0b5705e94df 100644 --- a/tools/@aws-cdk/cdk-release/package.json +++ b/tools/@aws-cdk/cdk-release/package.json @@ -30,6 +30,7 @@ "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/pkglint": "0.0.0", + "@types/changelog-parser": "^2.7.1", "@types/fs-extra": "^8.1.2", "@types/jest": "^26.0.24", "@types/yargs": "^15.0.14", @@ -37,17 +38,18 @@ }, "dependencies": { "@lerna/project": "^4.0.0", + "changelog-parser": "^2.8.0", "conventional-changelog": "^3.1.24", "conventional-changelog-config-spec": "^2.1.0", "conventional-changelog-preset-loader": "^2.3.4", - "conventional-commits-parser": "^3.2.2", "conventional-changelog-writer": "^4.1.0", + "conventional-commits-parser": "^3.2.2", + "detect-indent": "^6.1.0", + "detect-newline": "^3.1.0", "fs-extra": "^9.1.0", "git-raw-commits": "^2.0.10", "semver": "^7.3.5", - "stringify-package": "^1.0.1", - "detect-indent": "^6.1.0", - "detect-newline": "^3.1.0" + "stringify-package": "^1.0.1" }, "keywords": [ "aws", diff --git a/tools/@aws-cdk/cdk-release/test/release-notes.test.ts b/tools/@aws-cdk/cdk-release/test/release-notes.test.ts new file mode 100644 index 0000000000000..f190b6b84a332 --- /dev/null +++ b/tools/@aws-cdk/cdk-release/test/release-notes.test.ts @@ -0,0 +1,61 @@ +import * as files from '../lib/private/files'; +import { createReleaseNotes } from '../lib/release-notes'; +import * as versions from '../lib/versions'; + +/** MOCKS */ +const mockWriteFile = jest.spyOn(files, 'writeFile').mockImplementation(() => jest.fn()); +const mockReadVersion = jest.spyOn(versions, 'readVersion'); +jest.mock('changelog-parser', () => { return jest.fn(); }); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const changelogParser = require('changelog-parser'); +/** MOCKS */ + +beforeEach(() => { jest.resetAllMocks(); }); + +const DEFAULT_OPTS = { + changelogFile: 'CHANGELOG.md', + releaseNotesFile: 'RELEASE_NOTES.md', + versionFile: 'versions.json', +}; + +test('without alpha releases, only the stable changelog is returned', async () => { + mockReadVersion.mockImplementation((_) => { return { stableVersion: '1.2.3' }; }); + mockChangelogOnceForVersion('1.2.3', 'foo'); + + await createReleaseNotes(DEFAULT_OPTS); + + expectReleaseNotes('foo'); +}); + +test('with alpha releases the contents of both are returned as separate sections', async () => { + mockReadVersion.mockImplementation((_) => { return { stableVersion: '1.2.3', alphaVersion: '1.2.3-alpha' }; }); + mockChangelogOnceForVersion('1.2.3', 'foo'); // stable + mockChangelogOnceForVersion('1.2.3-alpha', 'bar'); // alpha + + await createReleaseNotes({ ...DEFAULT_OPTS, alphaChangelogFile: 'CHANGELOG.alpha.md' }); + + expectReleaseNotes([ + 'foo', + '---', + '## Alpha modules (1.2.3-alpha)', + 'bar', + ]); +}); + +test('throws if no matching version is found in the changelog', async () => { + mockReadVersion.mockImplementation((_) => { return { stableVersion: '1.2.3' }; }); + mockChangelogOnceForVersion('4.5.6', 'foo'); + + await expect(createReleaseNotes(DEFAULT_OPTS)) + .rejects + .toThrow(/No changelog entry found for version 1.2.3 in CHANGELOG.md/); +}); + +function mockChangelogOnceForVersion(version: string, body: string) { + changelogParser.mockImplementationOnce((_: string) => { return { versions: [{ version, body }] }; }); +} + +function expectReleaseNotes(contents: string | string[]) { + const data = (typeof contents === 'string') ? contents : contents.join('\n'); + expect(mockWriteFile).toBeCalledWith(expect.any(Object), 'RELEASE_NOTES.md', data); +} diff --git a/tools/@aws-cdk/individual-pkg-gen/transform-packages.ts b/tools/@aws-cdk/individual-pkg-gen/transform-packages.ts index ece10f8262a86..a5cbcb556ea1b 100644 --- a/tools/@aws-cdk/individual-pkg-gen/transform-packages.ts +++ b/tools/@aws-cdk/individual-pkg-gen/transform-packages.ts @@ -3,6 +3,8 @@ import * as awsCdkMigration from 'aws-cdk-migration'; import * as fs from 'fs-extra'; // eslint-disable-next-line @typescript-eslint/no-require-imports const lerna_project = require('@lerna/project'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const ver = require('../../../scripts/resolve-version'); /** * @aws-cdk/ scoped packages that may be present in devDependencies and need to @@ -128,6 +130,9 @@ function transformPackageJson(pkg: any, source: string, destination: string, alp const pkgUnscopedName = `${pkg.name.substring('@aws-cdk/'.length)}`; packageJson.name += '-alpha'; + if (ver.alphaVersion) { + packageJson.version = ver.alphaVersion; + } packageJson.repository.directory = `packages/individual-packages/${pkgUnscopedName}`; // All individual packages are public by default on v1, and private by default on v2. @@ -201,7 +206,7 @@ function transformPackageJsonDependencies(packageJson: any, pkg: any, alphaPacka break; default: if (alphaPackages[dependency]) { - alphaDependencies[alphaPackages[dependency]] = pkg.version; + alphaDependencies[alphaPackages[dependency]] = packageJson.version; } else if (v1BundledDependencies.indexOf(dependency) !== -1) { // ...other than third-party dependencies, which are in bundledDependencies bundledDependencies[dependency] = packageJson.dependencies[dependency]; @@ -221,7 +226,7 @@ function transformPackageJsonDependencies(packageJson: any, pkg: any, alphaPacka break; default: if (alphaPackages[v1DevDependency]) { - alphaDevDependencies[alphaPackages[v1DevDependency]] = pkg.version; + alphaDevDependencies[alphaPackages[v1DevDependency]] = packageJson.version; } else if (!v1DevDependency.startsWith('@aws-cdk/') || isRequiredTool(v1DevDependency)) { devDependencies[v1DevDependency] = packageJson.devDependencies[v1DevDependency]; } diff --git a/version.v1.json b/version.v1.json index 030861767e22a..c24ba0065c588 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.127.0" + "version": "1.128.0" } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index c6815c2021b43..3fc7477ad872b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1621,6 +1621,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/changelog-parser@^2.7.1": + version "2.7.1" + resolved "https://registry.npmjs.org/@types/changelog-parser/-/changelog-parser-2.7.1.tgz#da124373fc8abfb6951fef83718ea5f041fea527" + integrity sha512-OFZB7OlG6nrkcnvJhcyV2Zm/PUGk40oHyfaEBRjlm+ghrKxbFQI+xao/IzYL0G72fpLCTGGs3USrhe38/FF6QQ== + "@types/eslint@^7.28.1": version "7.28.1" resolved "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.1.tgz#50b07747f1f84c2ba8cd394cf0fe0ba07afce320" @@ -2705,6 +2710,14 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +changelog-parser@^2.8.0: + version "2.8.0" + resolved "https://registry.npmjs.org/changelog-parser/-/changelog-parser-2.8.0.tgz#c14293e3e8fab797913c722de965480198650108" + integrity sha512-ZtSwN0hY7t+WpvaXqqXz98RHCNhWX9HsvCRAv1aBLlqJ7BpKtqdM6Nu6JOiUhRAWR7Gov0aN0fUnmflTz0WgZg== + dependencies: + line-reader "^0.2.4" + remove-markdown "^0.2.2" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -6334,6 +6347,11 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" +line-reader@^0.2.4: + version "0.2.4" + resolved "https://registry.npmjs.org/line-reader/-/line-reader-0.2.4.tgz#c4392b587dea38580c9678570e6e8e49fce52622" + integrity sha1-xDkrWH3qOFgMlnhXDm6OSfzlJiI= + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -8211,6 +8229,11 @@ release-zalgo@^1.0.0: dependencies: es6-error "^4.0.1" +remove-markdown@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.2.2.tgz#66b0ceeba9fb77ca9636bb1b0307ce21a32a12a6" + integrity sha1-ZrDO66n7d8qWNrsbAwfOIaMqEqY= + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"