From eba5fdd44424e6524f4cc581d443c9d3941418d4 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Fri, 2 Jul 2021 17:17:16 +0100 Subject: [PATCH 001/105] docs(pipeline): document work-arounds for when bootstrap stack has been recreated (#15405) If bootstrap stack roles have been deleted and recreated, it results in cryptic errors in the pipeline (AccessDeniedException). An elegant and robust fix to this is involved and not likely to be implemented in the near-term. In the meantime, what we can do is document ways to work around this issue, and heavily advise people not to get themselves into this scenario in the first place. This README update intends to accomplish the former, if not the latter. Tested and verified the proposed "automated" solution (re-creating bootstrap stacks). related #11934 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 112 ++++++++++++++++++++------ 1 file changed, 89 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 515c3eb5c5e9e..6e4f73c895b1f 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -205,8 +205,8 @@ const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { }); ``` -If you use assets for files or Docker images, every asset will get its own upload action during the asset stage. -By setting the value `singlePublisherPerType` to `true`, only one action for files and one action for +If you use assets for files or Docker images, every asset will get its own upload action during the asset stage. +By setting the value `singlePublisherPerType` to `true`, only one action for files and one action for Docker images is created that handles all assets of the respective type. If you need to run commands to setup proxies, mirrors, etc you can supply them using the `assetPreInstallCommands`. @@ -673,7 +673,7 @@ $ env CDK_NEW_BOOTSTRAP=1 npx cdk bootstrap \ aws://222222222222/us-east-2 ``` -If you only want to trust an account to do lookups (e.g, when your CDK application has a +If you only want to trust an account to do lookups (e.g, when your CDK application has a `Vpc.fromLookup()` call), use the option `--trust-for-lookup`: ```console @@ -699,9 +699,9 @@ These command lines explained: CDK applications into this account. In this case we indicate the Pipeline's account, but you could also use this for developer accounts (don't do that for production application accounts though!). -* `--trust-for-lookup`: similar to `--trust`, but gives a more limited set of permissions to the - trusted account, allowing it to only look up values, such as availability zones, EC2 images and - VPCs. Note that if you provide an account using `--trust`, that account can also do lookups. +* `--trust-for-lookup`: similar to `--trust`, but gives a more limited set of permissions to the + trusted account, allowing it to only look up values, such as availability zones, EC2 images and + VPCs. Note that if you provide an account using `--trust`, that account can also do lookups. So you only need to pass `--trust-for-lookup` if you need to use a different account. * `aws://222222222222/us-east-2`: the account and region we're bootstrapping. @@ -717,10 +717,10 @@ These command lines explained: > Check with the appropriate department within your organization to decide on the > proper policy to use. > -> If your policy includes permissions to create on attach permission to a role, -> developers can escalate their privilege with more permissive permission. -> Thus, we recommend implementing [permissions boundary](https://aws.amazon.com/premiumsupport/knowledge-center/iam-permission-boundaries/) -> in the CDK Execution role. To do this, you can bootstrap with the `--template` option with +> If your policy includes permissions to create on attach permission to a role, +> developers can escalate their privilege with more permissive permission. +> Thus, we recommend implementing [permissions boundary](https://aws.amazon.com/premiumsupport/knowledge-center/iam-permission-boundaries/) +> in the CDK Execution role. To do this, you can bootstrap with the `--template` option with > [a customized template](https://github.com/aws-samples/aws-bootstrap-kit-examples/blob/ba28a97d289128281bc9483bcba12c1793f2c27a/source/1-SDLC-organization/lib/cdk-bootstrap-template.yml#L395) that contains a permission boundary. ### Migrating from old bootstrap stack @@ -826,21 +826,21 @@ workstation's version or upgrade the CodeBuild version. If, in the 'Synth' action (inside the 'Build' stage) of your pipeline, you get an error like this: ```console -stderr: docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?. -See 'docker run --help'. +stderr: docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?. +See 'docker run --help'. ``` -It means that the AWS CodeBuild project for 'Synth' is not configured to run in privileged mode, -which prevents Docker builds from happening. This typically happens if you use a CDK construct -that bundles asset using tools run via Docker, like `aws-lambda-nodejs`, `aws-lambda-python`, -`aws-lambda-go` and others. +It means that the AWS CodeBuild project for 'Synth' is not configured to run in privileged mode, +which prevents Docker builds from happening. This typically happens if you use a CDK construct +that bundles asset using tools run via Docker, like `aws-lambda-nodejs`, `aws-lambda-python`, +`aws-lambda-go` and others. 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: ..., @@ -852,21 +852,21 @@ Make sure you set the `privileged` environment variable to `true` in the synth d }); ``` -After turning on `privilegedMode: true`, you will need to do a one-time manual cdk deploy of your -pipeline to get it going again (as with a broken 'synth' the pipeline will not be able to self -update to the right state). +After turning on `privilegedMode: true`, you will need to do a one-time manual cdk deploy of your +pipeline to get it going again (as with a broken 'synth' the pipeline will not be able to self +update to the right state). ### S3 error: Access Denied -Some constructs, such as EKS clusters, generate nested stacks. When CloudFormation tries +Some constructs, such as EKS clusters, generate nested stacks. When CloudFormation tries to deploy those stacks, it may fail with this error: ```console S3 error: Access Denied For more information check http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html ``` -This happens because the pipeline is not self-mutating and, as a consequence, the `FileAssetX` -build projects get out-of-sync with the generated templates. To fix this, make sure the +This happens because the pipeline is not self-mutating and, as a consequence, the `FileAssetX` +build projects get out-of-sync with the generated templates. To fix this, make sure the `selfMutating` property is set to `true`: ```typescript @@ -876,6 +876,72 @@ const pipeline = new CdkPipeline(this, 'MyPipeline', { }); ``` +### Action Execution Denied + +While attempting to deploy an application stage, the "Prepare" or "Deploy" stage may fail with a cryptic error like: + +`Action execution failed +Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied; Request ID: 0123456ABCDEFGH; +S3 Extended Request ID: 3hWcrVkhFGxfiMb/rTJO0Bk7Qn95x5ll4gyHiFsX6Pmk/NT+uX9+Z1moEcfkL7H3cjH7sWZfeD0=; Proxy: null)` + +This generally indicates that the roles necessary to deploy have been deleted (or deleted and re-created); +for example, if the bootstrap stack has been deleted and re-created, this scenario will happen. Under the hood, +the resources that rely on these roles (e.g., `cdk-$qualifier-deploy-role-$account-$region`) point to different +canonical IDs than the recreated versions of these roles, which causes the errors. There are no simple solutions +to this issue, and for that reason we **strongly recommend** that bootstrap stacks not be deleted and re-created +once created. + +The most automated way to solve the issue is to introduce a secondary bootstrap stack. By changing the qualifier +that the pipeline stack looks for, a change will be detected and the impacted policies and resources will be updated. +A hypothetical recovery workflow would look something like this: + +* First, for all impacted environments, create a secondary bootstrap stack: + +```sh +$ env CDK_NEW_BOOTSTRAP=1 npx cdk bootstrap \ + --qualifier randchars1234 + --toolkit-stack-name CDKToolkitTemp + aws://111111111111/us-east-1 +``` + +* Update all impacted stacks in the pipeline to use this new qualifier. +See https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html for more info. + +```ts +new MyStack(this, 'MyStack', { + // Update this qualifier to match the one used above. + synthesizer: new DefaultStackSynthesizer({ + qualifier: 'randchars1234', + }), +}); +``` + +* Deploy the updated stacks. This will update the stacks to use the roles created in the new bootstrap stack. +* (Optional) Restore back to the original state: + * Revert the change made in step #2 above + * Re-deploy the pipeline to use the original qualifier. + * Delete the temporary bootstrap stack(s) + +#### Manual Alternative + +Alternatively, the errors can be resolved by finding each impacted resource and policy, and correcting the policies +by replacing the canonical IDs (e.g., `AROAYBRETNYCYV6ZF2R93`) with the appropriate ARNs. As an example, the KMS +encryption key policy for the artifacts bucket may have a statement that looks like the following: + +```json +{ + "Effect" : "Allow", + "Principal" : { + // "AWS" : "AROAYBRETNYCYV6ZF2R93" // Indicates this issue; replace this value + "AWS": "arn:aws:iam::0123456789012:role/cdk-hnb659fds-deploy-role-0123456789012-eu-west-1", // Correct value + }, + "Action" : [ "kms:Decrypt", "kms:DescribeKey" ], + "Resource" : "*" +} +``` + +Any resource or policy that references the qualifier (`hnb659fds` by default) will need to be updated. + ## Current Limitations Limitations that we are aware of and will address: From 6a68873f51545e394efa30cd855aae1fc4484d62 Mon Sep 17 00:00:00 2001 From: Seiya6329 Date: Fri, 2 Jul 2021 10:32:46 -0700 Subject: [PATCH 002/105] feat(appmesh): add support for shared Meshes (#15353) #### Collaborator Shout out to @dfezzie #### REV - Adding new `meshOwner` attribute to `IMesh` - Adding `meshOwner` property for Cfn resources #### Design Note - Allowing to provide Mesh ARN from different AWS IAM account ID for IMesh.fromMeshArn() to enable working with shared mesh. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 17 ++++++++ .../@aws-cdk/aws-appmesh/lib/gateway-route.ts | 2 + packages/@aws-cdk/aws-appmesh/lib/mesh.ts | 4 +- .../@aws-cdk/aws-appmesh/lib/private/utils.ts | 11 +++++ packages/@aws-cdk/aws-appmesh/lib/route.ts | 2 + .../aws-appmesh/lib/virtual-gateway.ts | 3 +- .../@aws-cdk/aws-appmesh/lib/virtual-node.ts | 3 +- .../aws-appmesh/lib/virtual-router.ts | 2 + .../aws-appmesh/lib/virtual-service.ts | 2 + .../aws-appmesh/test/integ.mesh.expected.json | 6 +-- .../aws-appmesh/test/test.gateway-route.ts | 43 ++++++++++++++++++- .../@aws-cdk/aws-appmesh/test/test.route.ts | 39 +++++++++++++++++ .../aws-appmesh/test/test.virtual-gateway.ts | 30 ++++++++++++- .../aws-appmesh/test/test.virtual-node.ts | 34 ++++++++++++++- .../aws-appmesh/test/test.virtual-router.ts | 30 ++++++++++++- .../aws-appmesh/test/test.virtual-service.ts | 39 ++++++++++++++++- 16 files changed, 255 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 61be382c3313e..f4f9930f4676c 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -674,3 +674,20 @@ const envoyUser = new iam.User(stack, 'envoyUser'); */ gateway.grantStreamAggregatedResources(envoyUser) ``` + +## Adding Resources to shared meshes + +A shared mesh allows resources created by different accounts to communicate with each other in the same mesh: + +```ts +// 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 = appmesh.Mesh.fromMeshArn(stack, 'imported-mesh', arn); + +// This VirtualNode resource can communicate with the resources in the mesh from different AWS IAM account ID. +new appmesh.VirtualNode(stack, 'test-node', { + mesh: sharedMesh, +}); +``` + diff --git a/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts b/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts index 5bfb3b0ca21cd..2c34c1e3f6f46 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts @@ -2,6 +2,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnGatewayRoute } from './appmesh.generated'; import { GatewayRouteSpec } from './gateway-route-spec'; +import { renderMeshOwner } from './private/utils'; import { IVirtualGateway, VirtualGateway } from './virtual-gateway'; /** @@ -113,6 +114,7 @@ export class GatewayRoute extends cdk.Resource implements IGatewayRoute { const gatewayRoute = new CfnGatewayRoute(this, 'Resource', { gatewayRouteName: this.physicalName, meshName: props.virtualGateway.mesh.meshName, + meshOwner: renderMeshOwner(this.env.account, this.virtualGateway.mesh.env.account), spec: { httpRoute: routeSpecConfig.httpSpecConfig, http2Route: routeSpecConfig.http2SpecConfig, diff --git a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts index c3d768660117e..30bb9cee8243a 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts @@ -143,7 +143,9 @@ export class Mesh extends MeshBase { public meshArn = meshArn; } - return new Import(scope, id); + return new Import(scope, id, { + environmentFromArn: meshArn, + }); } /** diff --git a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts index f14bc1413d707..ad277985250cb 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -1,6 +1,7 @@ import { CfnVirtualNode } from '../appmesh.generated'; import { ListenerTlsOptions } from '../listener-tls-options'; import { TlsClientPolicy } from '../tls-client-policy'; +import { Token, TokenComparison } from '@aws-cdk/core'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -81,3 +82,13 @@ export function renderListenerTlsOptions(scope: Construct, listenerTls: Listener } : undefined; } + +/** + * This is the helper method to populate mesh owner when it is a shared mesh scenario + */ +export function renderMeshOwner(resourceAccount: string, meshAccount: string) : string | undefined { + const comparison = Token.compareStrings(resourceAccount, meshAccount); + return comparison === TokenComparison.DIFFERENT || comparison === TokenComparison.ONE_UNRESOLVED + ? meshAccount + : undefined; +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/route.ts b/packages/@aws-cdk/aws-appmesh/lib/route.ts index 7800e3e08e53f..49b101acfdf9f 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route.ts @@ -2,6 +2,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnRoute } from './appmesh.generated'; import { IMesh } from './mesh'; +import { renderMeshOwner } from './private/utils'; import { RouteSpec } from './route-spec'; import { IVirtualRouter, VirtualRouter } from './virtual-router'; @@ -120,6 +121,7 @@ export class Route extends cdk.Resource implements IRoute { const route = new CfnRoute(this, 'Resource', { routeName: this.physicalName, meshName: this.virtualRouter.mesh.meshName, + meshOwner: renderMeshOwner(this.env.account, this.virtualRouter.mesh.env.account), virtualRouterName: this.virtualRouter.virtualRouterName, spec: { tcpRoute: spec.tcpRouteSpec, diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts index f2286784a10ed..e287027c7e946 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts @@ -4,7 +4,7 @@ import { Construct } from 'constructs'; import { CfnVirtualGateway } from './appmesh.generated'; import { GatewayRoute, GatewayRouteBaseProps } from './gateway-route'; import { IMesh, Mesh } from './mesh'; -import { renderTlsClientPolicy } from './private/utils'; +import { renderTlsClientPolicy, renderMeshOwner } from './private/utils'; import { AccessLog, BackendDefaults } from './shared-interfaces'; import { VirtualGatewayListener, VirtualGatewayListenerConfig } from './virtual-gateway-listener'; @@ -192,6 +192,7 @@ export class VirtualGateway extends VirtualGatewayBase { const node = new CfnVirtualGateway(this, 'Resource', { virtualGatewayName: this.physicalName, meshName: this.mesh.meshName, + meshOwner: renderMeshOwner(this.env.account, this.mesh.env.account), spec: { listeners: this.listeners.map(listener => listener.listener), backendDefaults: props.backendDefaults !== undefined diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts index 2668609353874..ffc56239c47f0 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts @@ -3,7 +3,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualNode } from './appmesh.generated'; import { IMesh, Mesh } from './mesh'; -import { renderTlsClientPolicy } from './private/utils'; +import { renderMeshOwner, renderTlsClientPolicy } from './private/utils'; import { ServiceDiscovery } from './service-discovery'; import { AccessLog, BackendDefaults, Backend } from './shared-interfaces'; import { VirtualNodeListener, VirtualNodeListenerConfig } from './virtual-node-listener'; @@ -194,6 +194,7 @@ export class VirtualNode extends VirtualNodeBase { const node = new CfnVirtualNode(this, 'Resource', { virtualNodeName: this.physicalName, meshName: this.mesh.meshName, + meshOwner: renderMeshOwner(this.env.account, this.mesh.env.account), spec: { backends: cdk.Lazy.any({ produce: () => this.backends }, { omitEmptyArray: true }), listeners: cdk.Lazy.any({ produce: () => this.listeners.map(listener => listener.listener) }, { omitEmptyArray: true }), diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts index b202c4c1ca3ae..bc2023f19d028 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts @@ -2,6 +2,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualRouter } from './appmesh.generated'; import { IMesh, Mesh } from './mesh'; +import { renderMeshOwner } from './private/utils'; import { Route, RouteBaseProps } from './route'; import { VirtualRouterListener } from './virtual-router-listener'; @@ -154,6 +155,7 @@ export class VirtualRouter extends VirtualRouterBase { const router = new CfnVirtualRouter(this, 'Resource', { virtualRouterName: this.physicalName, meshName: this.mesh.meshName, + meshOwner: renderMeshOwner(this.env.account, this.mesh.env.account), spec: { listeners: this.listeners, }, diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts index d41b47d554178..cb561eff19308 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts @@ -2,6 +2,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualService } from './appmesh.generated'; import { IMesh, Mesh } from './mesh'; +import { renderMeshOwner } from './private/utils'; import { IVirtualNode } from './virtual-node'; import { IVirtualRouter } from './virtual-router'; @@ -110,6 +111,7 @@ export class VirtualService extends cdk.Resource implements IVirtualService { const svc = new CfnVirtualService(this, 'Resource', { meshName: this.mesh.meshName, + meshOwner: renderMeshOwner(this.env.account, this.mesh.env.account), virtualServiceName: this.physicalName, spec: { provider: providerConfig.virtualNodeProvider || providerConfig.virtualRouterProvider diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index be76cbb71b7d6..34333711b8488 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -95,15 +95,15 @@ "vpcPublicSubnet1NATGateway9C16659E": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + }, "AllocationId": { "Fn::GetAtt": [ "vpcPublicSubnet1EIPDA49DCBE", "AllocationId" ] }, - "SubnetId": { - "Ref": "vpcPublicSubnet1Subnet2E65531E" - }, "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts index 6b636507e718c..76f325e99dd10 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -53,6 +53,7 @@ export = { // THEN expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { GatewayRouteName: 'gateway-http-route', + MeshOwner: ABSENT, Spec: { HttpRoute: { Action: { @@ -133,6 +134,44 @@ export = { /Prefix Path must start with \'\/\', got: wrong/); test.done(); }, + + 'with shared service mesh': { + 'Mesh Owner is the AWS account ID of the account that shared the mesh with your account'(test:Test) { + // GIVEN + const app = new cdk.App(); + const meshEnv = { account: '1234567899', region: 'us-west-2' }; + const gatewayRouteEnv = { account: '9987654321', region: 'us-west-2' }; + + // Creating stack in Account 9987654321 + const stack = new cdk.Stack(app, 'mySharedStack', { env: gatewayRouteEnv }); + // Mesh is in Account 1234567899 + const sharedMesh = appmesh.Mesh.fromMeshArn(stack, 'shared-mesh', + `arn:aws:appmesh:${meshEnv.region}:${meshEnv.account}:mesh/shared-mesh`); + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: sharedMesh, + }); + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(sharedMesh), + virtualServiceName: 'target.local', + }); + + // WHEN + new appmesh.GatewayRoute(stack, 'test-node', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + }), + virtualGateway: virtualGateway, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + MeshOwner: meshEnv.account, + })); + + test.done(); + }, + }, }, 'Can import Gateway Routes using an ARN'(test: Test) { @@ -178,4 +217,4 @@ export = { test.equal(gatewayRoute.virtualGateway.mesh.meshName, meshName); test.done(); }, -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index 05cbd1dd04f4c..588c784d7ba71 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -112,6 +112,7 @@ export = { }, }, }, + MeshOwner: ABSENT, RouteName: 'test-http-route', })); @@ -574,6 +575,44 @@ export = { test.done(); }, + + 'with shared service mesh': { + 'Mesh Owner is the AWS account ID of the account that shared the mesh with your account'(test: Test) { + // GIVEN + const app = new cdk.App(); + const meshEnv = { account: '1234567899', region: 'us-west-2' }; + const routeEnv = { account: '9987654321', region: 'us-west-2' }; + // Creating stack in Account 9987654321 + const stack = new cdk.Stack(app, 'mySharedStack', { env: routeEnv }); + // Mesh is in Account 1234567899 + const sharedMesh = appmesh.Mesh.fromMeshArn(stack, 'shared-mesh', + `arn:aws:appmesh:${meshEnv.region}:${meshEnv.account}:mesh/shared-mesh`); + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh: sharedMesh, + }); + const virtualNode = sharedMesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + new appmesh.Route(stack, 'test-route', { + mesh: sharedMesh, + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode }], + match: { serviceName: 'example' }, + }), + virtualRouter: router, + + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + MeshOwner: meshEnv.account, + })); + + test.done(); + }, + }, }, 'should match routes based on headers'(test: Test) { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts index 8ce071ccc7ee2..4f3a8717b827a 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; @@ -74,6 +74,7 @@ export = { ], }, VirtualGatewayName: 'httpGateway', + MeshOwner: ABSENT, })); expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { @@ -434,6 +435,33 @@ export = { test.done(); }, + + 'with shared service mesh': { + 'Mesh Owner is the AWS account ID of the account that shared the mesh with your account'(test:Test) { + // GIVEN + const app = new cdk.App(); + const meshEnv = { account: '1234567899', region: 'us-west-2' }; + const virtualGatewayEnv = { account: '9987654321', region: 'us-west-2' }; + + // Creating stack in Account 9987654321 + const stack = new cdk.Stack(app, 'mySharedStack', { env: virtualGatewayEnv }); + // Mesh is in Account 1234567899 + const sharedMesh = appmesh.Mesh.fromMeshArn(stack, 'shared-mesh', + `arn:aws:appmesh:${meshEnv.region}:${meshEnv.account}:mesh/shared-mesh`); + + // WHEN + new appmesh.VirtualGateway(stack, 'test-node', { + mesh: sharedMesh, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + MeshOwner: meshEnv.account, + })); + + test.done(); + }, + }, }, 'When adding a gateway route to existing VirtualGateway ': { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts index 31589df364b67..171c754f09954 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as acmpca from '@aws-cdk/aws-acmpca'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as iam from '@aws-cdk/aws-iam'; @@ -55,6 +55,7 @@ export = { }, ], }, + MeshOwner: ABSENT, })); test.done(); @@ -894,4 +895,35 @@ export = { test.done(); }, + + 'When creating a VirtualNode': { + 'with shared service mesh': { + 'Mesh Owner is the AWS account ID of the account that shared the mesh with your account'(test:Test) { + // GIVEN + const app = new cdk.App(); + const meshEnv = { account: '1234567899', region: 'us-west-2' }; + const virtualNodeEnv = { account: '9987654321', region: 'us-west-2' }; + + // Creating stack in Account 9987654321 + const stack = new cdk.Stack(app, 'mySharedStack', { env: virtualNodeEnv }); + // Mesh is in Account 1234567899 + const sharedMesh = appmesh.Mesh.fromMeshArn(stack, 'shared-mesh', + `arn:aws:appmesh:${meshEnv.region}:${meshEnv.account}:mesh/shared-mesh`); + + // WHEN + new appmesh.VirtualNode(stack, 'test-node', { + mesh: sharedMesh, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualNode', { + MeshOwner: meshEnv.account, + })); + + test.done(); + }, + }, + }, }; + + diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts index 2fc959ad57769..4999b4425a1c7 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -80,11 +80,39 @@ export = { }, ], }, + MeshOwner: ABSENT, })); }); test.done(); }, + + 'with shared service mesh': { + 'Mesh Owner is the AWS account ID of the account that shared the mesh with your account'(test:Test) { + // GIVEN + const app = new cdk.App(); + const meshEnv = { account: '1234567899', region: 'us-west-2' }; + const virtualRouterEnv = { account: '9987654321', region: 'us-west-2' }; + + // Creating stack in Account B + const stack = new cdk.Stack(app, 'mySharedStack', { env: virtualRouterEnv }); + // Mesh is in Account A + const sharedMesh = appmesh.Mesh.fromMeshArn(stack, 'shared-mesh', + `arn:aws:appmesh:${meshEnv.region}:${meshEnv.account}:mesh/shared-mesh`); + + // WHEN + new appmesh.VirtualRouter(stack, 'test-node', { + mesh: sharedMesh, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualRouter', { + MeshOwner: meshEnv.account, + })); + + test.done(); + }, + }, }, 'When adding route to existing VirtualRouter': { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts index 404cb5481141e..ea2990dd9f418 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -74,6 +74,7 @@ export = { }, }, }, + MeshOwner: ABSENT, }), ); @@ -122,4 +123,40 @@ export = { }, }, }, + + 'When creating a VirtualService': { + 'with shared service mesh': { + 'Mesh Owner is the AWS account ID of the account that shared the mesh with your account'(test:Test) { + // GIVEN + const app = new cdk.App(); + const meshEnv = { account: '1234567899', region: 'us-west-2' }; + const virtualServiceEnv = { account: '9987654321', region: 'us-west-2' }; + + // Creating stack in Account B + const stack = new cdk.Stack(app, 'mySharedStack', { env: virtualServiceEnv }); + // Mesh is in Account A + const sharedMesh = appmesh.Mesh.fromMeshArn(stack, 'shared-mesh', + `arn:aws:appmesh:${meshEnv.region}:${meshEnv.account}:mesh/shared-mesh`); + + const node = sharedMesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test.domain.local'), + listeners: [appmesh.VirtualNodeListener.http({ + port: 8080, + })], + }); + + // WHEN + new appmesh.VirtualService(stack, 'test-node', { + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualService', { + MeshOwner: meshEnv.account, + })); + + test.done(); + }, + }, + }, }; From 86a44f93bf718e20e32f9968ec75bee28dbc2198 Mon Sep 17 00:00:00 2001 From: Madeline Kusters <80541297+madeline-k@users.noreply.github.com> Date: Fri, 2 Jul 2021 12:04:10 -0700 Subject: [PATCH 003/105] feat(cloudwatch): allow arbitrary statistics in Metric and Alarm (#15387) This change maintains the existing logic which parses statistic strings provided by the user for Simple statistics or Percentile statistics, but it also allows for any string to be passed. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cloudwatch/lib/private/statistic.ts | 13 +++++++++++-- .../@aws-cdk/aws-cloudwatch/test/test.metrics.ts | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts b/packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts index 562631738f293..8f1aae4ff99e6 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts @@ -9,10 +9,16 @@ export interface PercentileStatistic { type: 'percentile'; percentile: number; } + +export interface GenericStatistic { + type: 'generic'; + statistic: string; +} + /** * Parse a statistic, returning the type of metric that was used (simple or percentile) */ -export function parseStatistic(stat: string): SimpleStatistic | PercentileStatistic { +export function parseStatistic(stat: string): SimpleStatistic | PercentileStatistic | GenericStatistic { const lowerStat = stat.toLowerCase(); // Simple statistics @@ -45,7 +51,10 @@ export function parseStatistic(stat: string): SimpleStatistic | PercentileStatis }; } - throw new Error(`Not a valid statistic: '${stat}', must be one of Average | Minimum | Maximum | SampleCount | Sum | pNN.NN`); + return { + type: 'generic', + statistic: lowerStat, + }; } export function normalizeStatistic(stat: string): string { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts index 39f39d4476bc1..dc55f7a2bb0e7 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts @@ -231,4 +231,14 @@ export = { test.done(); }, + + 'metric accepts a variety of statistics'(test: Test) { + new Metric({ + namespace: 'Test', + metricName: 'Metric', + statistic: 'myCustomStatistic', + }); + + test.done(); + }, }; From c432d481aa9e26bae9092a3084eca55fdc7038ce Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Fri, 2 Jul 2021 18:56:47 -0700 Subject: [PATCH 004/105] fix(cfnspec): .npmignore generated by cfnspec does not pass pkglint (#15409) A recent change to pkglint (#15064) to always include .lit.ts files in .npmignore was not also applied to the .npmignore generator in cfnspec which is created whenever a new CFN module is added. This will lead to a failed build whenever the CFN spec is next updated and forces a new module to be created. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts b/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts index 35360473b8a4e..8a62868591119 100644 --- a/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts +++ b/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts @@ -254,6 +254,7 @@ async function main() { '**/cdk.out', 'junit.xml', 'test/', + '!*.lit.ts', ]); await write('lib/index.ts', [ From b010239ee0a027c25ec90c5ed8784a36315536d2 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Mon, 5 Jul 2021 10:08:59 +0100 Subject: [PATCH 005/105] fix(pipelines): unable to add assets stage to existing VPC pipeline (#15401) The VPC Policy for the asset publishing roles is created as a separate policy, which is guaranteed to be created after the role, but not necessarily before it's needed. CodeBuild -- at CloudFormation deploy time -- actually verifies the roles have appropriate permissions, leading to a potential race condition and failure if VPC permissions are not present when the CodeBuild jobs are created. Add an explicit dependency on the separate policy to guarantee creation order during deployment. fixes #14343 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/actions/publish-assets-action.ts | 13 ++++++++++- packages/@aws-cdk/pipelines/lib/pipeline.ts | 8 +++++-- .../pipelines/test/pipeline-assets.test.ts | 23 ++++++++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts index 3187889316414..83e7629b91b3a 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts @@ -6,7 +6,7 @@ import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; -import { Lazy, ISynthesisSession, Stack, attachCustomSynthesis } from '@aws-cdk/core'; +import { IDependable, ISynthesisSession, Lazy, Stack, attachCustomSynthesis } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { toPosixPath } from '../private/fs'; @@ -69,6 +69,13 @@ export interface PublishAssetsActionProps { */ readonly role?: iam.IRole; + /** + * Any Dependable construct that the CodeBuild project needs to take a dependency on. + * + * @default - none + */ + readonly dependable?: IDependable; + /** * The VPC where to execute the PublishAssetsAction. * @@ -147,6 +154,10 @@ export class PublishAssetsAction extends CoreConstruct implements codepipeline.I role: props.role, }); + if (props.dependable) { + project.node.addDependency(props.dependable); + } + this.action = new codepipeline_actions.CodeBuildAction({ actionName: props.actionName, project, diff --git a/packages/@aws-cdk/pipelines/lib/pipeline.ts b/packages/@aws-cdk/pipelines/lib/pipeline.ts index 2d6d9e33c3ef9..a3084d9f489d7 100644 --- a/packages/@aws-cdk/pipelines/lib/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/pipeline.ts @@ -428,6 +428,7 @@ class AssetPublishing extends CoreConstruct { private readonly publishers: Record = {}; private readonly assetRoles: Record = {}; + private readonly assetAttachedPolicies: Record = {}; private readonly assetPublishingRoles: Record> = {}; private readonly myCxAsmRoot: string; @@ -518,6 +519,7 @@ class AssetPublishing extends CoreConstruct { cdkCliVersion: this.props.cdkCliVersion, assetType: command.assetType, role: this.assetRoles[command.assetType], + dependable: this.assetAttachedPolicies[command.assetType], vpc: this.props.vpc, subnetSelection: this.props.subnetSelection, createBuildspecFile: this.props.singlePublisherPerType, @@ -603,7 +605,7 @@ class AssetPublishing extends CoreConstruct { // Normally CodeBuild itself takes care of this but we're creating a singleton role so now // we need to do this. if (this.props.vpc) { - assetRole.attachInlinePolicy(new iam.Policy(assetRole, 'VpcPolicy', { + const vpcPolicy = new iam.Policy(assetRole, 'VpcPolicy', { statements: [ new iam.PolicyStatement({ resources: [`arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:network-interface/*`], @@ -630,7 +632,9 @@ class AssetPublishing extends CoreConstruct { ], }), ], - })); + }); + assetRole.attachInlinePolicy(vpcPolicy); + this.assetAttachedPolicies[assetType] = vpcPolicy; } this.assetRoles[assetType] = assetRole.withoutPolicyUpdates(); diff --git a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts index 970e097733530..23f8815a3dc47 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { arrayWith, deepObjectLike, encodedJson, notMatching, objectLike, stringLike, SynthUtils } from '@aws-cdk/assert-internal'; +import { arrayWith, deepObjectLike, encodedJson, notMatching, objectLike, ResourcePart, stringLike, SynthUtils } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; import * as cb from '@aws-cdk/aws-codebuild'; import * as cp from '@aws-cdk/aws-codepipeline'; @@ -482,6 +482,27 @@ describe('pipeline with VPC', () => { }); }); }); + + behavior('Asset publishing CodeBuild Projects have a dependency on attached policies to the role', (suite) => { + suite.legacy(() => { + pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); + + // Assets Project + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Properties: { + ServiceRole: { + 'Fn::GetAtt': [ + 'CdkAssetsDockerRole484B6DD3', + 'Arn', + ], + }, + }, + DependsOn: [ + 'CdkAssetsDockerRoleVpcPolicy86CA024B', + ], + }, ResourcePart.CompleteDefinition); + }); + }); }); describe('pipeline with single asset publisher', () => { From f3c1b6d29416bdb19828cff9a4facd690c416d5f Mon Sep 17 00:00:00 2001 From: Ryan Parker Date: Mon, 5 Jul 2021 08:11:20 -0500 Subject: [PATCH 006/105] fix(cli): prevent 'Failed resources:' message when no failures and report all progress steps (#15207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This PR changes the CLI output to: - Prevent output of "Failed resources:" message when all resources were deployed successfully. - Fixes an issue where some steps were not being reported. See related issue: https://github.com/aws/aws-cdk/issues/15196 Before: ```shell CDK_NEW_BOOTSTRAP set, using new-style bootstrapping ⏳ Bootstrapping environment aws://##########/eu-west-1... Trusted accounts for deployment: (none) Trusted accounts for lookup: (none) Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize. AN_AWS_STACK: creating CloudFormation changeset... 0/4 |15:20:04 | UPDATE_IN_PROGRESS | AWS::CloudFormation::Stack | AN_AWS_STACK User Initiated 0/4 |15:20:11 | UPDATE_IN_PROGRESS | AWS::IAM::Role | DeploymentActionRole 0/4 |15:20:11 | UPDATE_IN_PROGRESS | AWS::IAM::Role | FilePublishingRole 0/4 |15:20:11 | UPDATE_IN_PROGRESS | AWS::IAM::Role | ImagePublishingRole 3/4 |15:20:28 | UPDATE_COMPLETE | AWS::IAM::Role | FilePublishingRole 3/4 |15:20:28 | UPDATE_COMPLETE | AWS::IAM::Role | FilePublishingRole 3/4 |15:20:28 | UPDATE_COMPLETE_CLEA | AWS::IAM::Role | ImagePublishingRole Failed resources: ✅ Environment aws://##########/eu-west-1 bootstrapped. ``` After: ```shell CDK_NEW_BOOTSTRAP set, using new-style bootstrapping ⏳ Bootstrapping environment aws://##########/eu-west-1... Trusted accounts for deployment: (none) Trusted accounts for lookup: (none) Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize. AN_AWS_STACK: creating CloudFormation changeset... 0/4 |15:20:04 | UPDATE_IN_PROGRESS | AWS::CloudFormation::Stack | AN_AWS_STACK User Initiated 0/4 |15:20:11 | UPDATE_IN_PROGRESS | AWS::IAM::Role | DeploymentActionRole 0/4 |15:20:11 | UPDATE_IN_PROGRESS | AWS::IAM::Role | FilePublishingRole 0/4 |15:20:11 | UPDATE_IN_PROGRESS | AWS::IAM::Role | ImagePublishingRole 1/4 |15:20:28 | UPDATE_COMPLETE | AWS::IAM::Role | FilePublishingRole 2/4 |15:20:28 | UPDATE_COMPLETE | AWS::IAM::Role | FilePublishingRole 3/4 |15:20:28 | UPDATE_COMPLETE_CLEA | AWS::IAM::Role | ImagePublishingRole 4/4 |15:20:28 | UPDATE_COMPLETE | AWS::IAM::Role | DeploymentActionRole ✅ Environment aws://##########/eu-west-1 bootstrapped. ``` ### TODO - [x] Prevent output of "Failed resources:" if there are no failures present. - [x] Report all steps before exiting. - [x] Add tests for changes. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cloudformation/stack-activity-monitor.ts | 21 +- packages/aws-cdk/test/api/console-listener.ts | 65 +++++ .../test/api/stack-activity-monitor.test.ts | 224 ++++++++++++++++++ 3 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 packages/aws-cdk/test/api/console-listener.ts create mode 100644 packages/aws-cdk/test/api/stack-activity-monitor.test.ts diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts index 723f7bd8c36f7..98277d3339fa1 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts @@ -420,6 +420,10 @@ abstract class ActivityPrinterBase implements IActivityPrinter { delete this.resourcesInProgress[activity.event.LogicalResourceId]; } + if (status.endsWith('_COMPLETE_CLEANUP_IN_PROGRESS')) { + this.resourcesDone++; + } + if (status.endsWith('_COMPLETE')) { const prevState = this.resourcesPrevCompleteState[activity.event.LogicalResourceId]; if (!prevState) { @@ -475,6 +479,7 @@ export class HistoryActivityPrinter extends ActivityPrinterBase { public addActivity(activity: StackActivity) { super.addActivity(activity); this.printable.push(activity); + this.print(); } public print() { @@ -486,15 +491,17 @@ export class HistoryActivityPrinter extends ActivityPrinterBase { } public stop() { - this.stream.write('\nFailed resources:\n'); // Print failures at the end - for (const failure of this.failures) { - // Root stack failures are not interesting - if (failure.event.StackName === failure.event.LogicalResourceId) { - continue; - } + if (this.failures.length > 0) { + this.stream.write('\nFailed resources:\n'); + for (const failure of this.failures) { + // Root stack failures are not interesting + if (failure.event.StackName === failure.event.LogicalResourceId) { + continue; + } - this.printOne(failure, false); + this.printOne(failure, false); + } } } diff --git a/packages/aws-cdk/test/api/console-listener.ts b/packages/aws-cdk/test/api/console-listener.ts new file mode 100644 index 0000000000000..5a43b662ba217 --- /dev/null +++ b/packages/aws-cdk/test/api/console-listener.ts @@ -0,0 +1,65 @@ +import { EventEmitter } from 'events'; + +export type Output = ReadonlyArray; + +export interface Options { + isTTY?: boolean; +} + +export interface Inspector { + output: Output; + restore: () => void; +} + +class ConsoleListener { + private _stream: NodeJS.WriteStream; + private _options?: Options + + constructor(stream: NodeJS.WriteStream, options?: Options) { + this._stream = stream; + this._options = options; + } + + inspect(): Inspector { + let isTTY; + if (this._options && this._options.isTTY !== undefined) { + isTTY = this._options.isTTY; + } + + const output: string[] = []; + const stream = this._stream; + const res: EventEmitter & Partial = new EventEmitter(); + + const originalWrite = stream.write; + stream.write = (string: string) => { + output.push(string); + return res.emit('data', string); + }; + + const originalIsTTY = stream.isTTY; + if (isTTY === true) { + stream.isTTY = isTTY; + } + + res.output = output; + res.restore = () => { + stream.write = originalWrite; + stream.isTTY = originalIsTTY; + }; + return (res as Inspector); + } + + inspectSync(fn: (output: Output) => void): Output { + const inspect = this.inspect(); + try { + fn(inspect.output); + } finally { + inspect.restore(); + } + return inspect.output; + } + +} + +export const stdout = new ConsoleListener(process.stdout); +export const stderr = new ConsoleListener(process.stderr); diff --git a/packages/aws-cdk/test/api/stack-activity-monitor.test.ts b/packages/aws-cdk/test/api/stack-activity-monitor.test.ts new file mode 100644 index 0000000000000..7a1037581c94e --- /dev/null +++ b/packages/aws-cdk/test/api/stack-activity-monitor.test.ts @@ -0,0 +1,224 @@ +import { bold, reset, green, yellow, red } from 'colors/safe'; +import { HistoryActivityPrinter } from '../../lib/api/util/cloudformation/stack-activity-monitor'; +import { stderr } from './console-listener'; + +let TIMESTAMP: number; +let HUMAN_TIME: string; + +beforeAll(() => { + TIMESTAMP = new Date().getTime(); + HUMAN_TIME = new Date(TIMESTAMP).toLocaleTimeString(); +}); + + +test('prints 0/4 progress report, when addActivity is called with an "IN_PROGRESS" ResourceStatus', () => { + const historyActivityPrinter = new HistoryActivityPrinter({ + resourceTypeColumnWidth: 23, + resourcesTotal: 3, + stream: process.stderr, + }); + + const output = stderr.inspectSync(() => { + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'IN_PROGRESS', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + }); + + expect(output[0].trim()).toStrictEqual(`0/4 |${HUMAN_TIME} | ${reset('IN_PROGRESS ')} | AWS::CloudFormation::Stack | ${reset(bold('stack1'))}`); +}); + +test('prints 1/4 progress report, when addActivity is called with an "UPDATE_COMPLETE" ResourceStatus', () => { + const historyActivityPrinter = new HistoryActivityPrinter({ + resourceTypeColumnWidth: 23, + resourcesTotal: 3, + stream: process.stderr, + }); + + const output = stderr.inspectSync(() => { + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'UPDATE_COMPLETE', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + }); + + expect(output[0].trim()).toStrictEqual(`1/4 |${HUMAN_TIME} | ${green('UPDATE_COMPLETE ')} | AWS::CloudFormation::Stack | ${green(bold('stack1'))}`); +}); + +test('prints 1/4 progress report, when addActivity is called with an "UPDATE_COMPLETE_CLEAN_IN_PROGRESS" ResourceStatus', () => { + const historyActivityPrinter = new HistoryActivityPrinter({ + resourceTypeColumnWidth: 23, + resourcesTotal: 3, + stream: process.stderr, + }); + + const output = stderr.inspectSync(() => { + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + }); + + expect(output[0].trim()).toStrictEqual(`1/4 |${HUMAN_TIME} | ${green('UPDATE_COMPLETE_CLEA')} | AWS::CloudFormation::Stack | ${green(bold('stack1'))}`); +}); + + +test('prints 1/4 progress report, when addActivity is called with an "ROLLBACK_COMPLETE_CLEAN_IN_PROGRESS" ResourceStatus', () => { + const historyActivityPrinter = new HistoryActivityPrinter({ + resourceTypeColumnWidth: 23, + resourcesTotal: 3, + stream: process.stderr, + }); + + const output = stderr.inspectSync(() => { + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + }); + + expect(output[0].trim()).toStrictEqual(`1/4 |${HUMAN_TIME} | ${yellow('ROLLBACK_COMPLETE_CL')} | AWS::CloudFormation::Stack | ${yellow(bold('stack1'))}`); +}); + +test('prints 0/4 progress report, when addActivity is called with an "UPDATE_FAILED" ResourceStatus', () => { + const historyActivityPrinter = new HistoryActivityPrinter({ + resourceTypeColumnWidth: 23, + resourcesTotal: 3, + stream: process.stderr, + }); + + const output = stderr.inspectSync(() => { + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'UPDATE_FAILED', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + }); + + expect(output[0].trim()).toStrictEqual(`0/4 |${HUMAN_TIME} | ${red('UPDATE_FAILED ')} | AWS::CloudFormation::Stack | ${red(bold('stack1'))}`); +}); + + +test('does not print "Failed Resources:" list, when all deployments are successful', () => { + const historyActivityPrinter = new HistoryActivityPrinter({ + resourceTypeColumnWidth: 23, + resourcesTotal: 1, + stream: process.stderr, + }); + + const output = stderr.inspectSync(() => { + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'IN_PROGRESS', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'UPDATE_COMPLETE', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack2', + ResourceStatus: 'UPDATE_COMPLETE', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + historyActivityPrinter.stop(); + }); + + expect(output.length).toStrictEqual(3); + expect(output[0].trim()).toStrictEqual(`0/2 |${HUMAN_TIME} | ${reset('IN_PROGRESS ')} | AWS::CloudFormation::Stack | ${reset(bold('stack1'))}`); + expect(output[1].trim()).toStrictEqual(`1/2 |${HUMAN_TIME} | ${green('UPDATE_COMPLETE ')} | AWS::CloudFormation::Stack | ${green(bold('stack1'))}`); + expect(output[2].trim()).toStrictEqual(`2/2 |${HUMAN_TIME} | ${green('UPDATE_COMPLETE ')} | AWS::CloudFormation::Stack | ${green(bold('stack2'))}`); +}); + +test('prints "Failed Resources:" list, when at least one deployment fails', () => { + const historyActivityPrinter = new HistoryActivityPrinter({ + resourceTypeColumnWidth: 23, + resourcesTotal: 1, + stream: process.stderr, + }); + + const output = stderr.inspectSync(() => { + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'IN_PROGRESS', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + historyActivityPrinter.addActivity({ + event: { + LogicalResourceId: 'stack1', + ResourceStatus: 'UPDATE_FAILED', + Timestamp: new Date(TIMESTAMP), + ResourceType: 'AWS::CloudFormation::Stack', + StackId: '', + EventId: '', + StackName: '', + }, + }); + historyActivityPrinter.stop(); + }); + + expect(output.length).toStrictEqual(4); + expect(output[0].trim()).toStrictEqual(`0/2 |${HUMAN_TIME} | ${reset('IN_PROGRESS ')} | AWS::CloudFormation::Stack | ${reset(bold('stack1'))}`); + expect(output[1].trim()).toStrictEqual(`0/2 |${HUMAN_TIME} | ${red('UPDATE_FAILED ')} | AWS::CloudFormation::Stack | ${red(bold('stack1'))}`); + expect(output[2].trim()).toStrictEqual('Failed resources:'); + expect(output[3].trim()).toStrictEqual(`${HUMAN_TIME} | ${red('UPDATE_FAILED ')} | AWS::CloudFormation::Stack | ${red(bold('stack1'))}`); +}); From 04193565178405dae9a9980c439e22e2924b239c Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 5 Jul 2021 15:50:31 +0200 Subject: [PATCH 007/105] chore(build): build no longer fails if there are multiple dotnet versions installed (#15201) I was using yarn build and it gave an error on my local computer because I have multiple dotnet versions installed ( and the most recent one is 5.x ). With this pull request multiple dotnet versions are supported, as long as 3.1.x is installed. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- scripts/check-build-prerequisites.sh | 2 +- scripts/check-pack-prerequisites.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/check-build-prerequisites.sh b/scripts/check-build-prerequisites.sh index b48e67f363edf..a13a6b65c1c0a 100755 --- a/scripts/check-build-prerequisites.sh +++ b/scripts/check-build-prerequisites.sh @@ -93,7 +93,7 @@ fi app="dotnet" app_min="3.1.0" check_which $app $app_min -app_v=$(${app} --version) +app_v=$(${app} --list-sdks) echo -e "Checking dotnet version... \c" if [ $(echo $app_v | grep -c -E "(3\.1\.[0-9]+|5\.[0-9]+\.[0-9]+)") -eq 1 ] then diff --git a/scripts/check-pack-prerequisites.sh b/scripts/check-pack-prerequisites.sh index b60d23f963d5b..27a8ac925e8ef 100755 --- a/scripts/check-pack-prerequisites.sh +++ b/scripts/check-pack-prerequisites.sh @@ -83,7 +83,7 @@ fi app="dotnet" app_min="3.1.0" check_which $app $app_min -app_v=$(${app} --version) +app_v=$(${app} --list-sdks) echo -e "Checking $app version... \c" if [ $(echo $app_v | grep -c -E "3\.1\.[0-9].*|[4-9]\..*") -eq 1 ] then From cbee18acf750319488238dd926ae7b86392c8356 Mon Sep 17 00:00:00 2001 From: maafk Date: Mon, 5 Jul 2021 10:29:43 -0400 Subject: [PATCH 008/105] feat(core): add docker security option to asset bundling (#15204) Allow users to add [Docker security option](https://docs.docker.com/engine/reference/run/#security-configuration) when setting their [BundlingOptions](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.BundlingOptions.html). Improvement on PR [14682](https://github.com/aws/aws-cdk/pull/14682), related to issue #14681 Last PR [14682](https://github.com/aws/aws-cdk/pull/14682) only addressed [DockerRunOptions](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.DockerRunOptions.html) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/asset-staging.ts | 1 + packages/@aws-cdk/core/lib/bundling.ts | 10 +++++++- packages/@aws-cdk/core/test/staging.test.ts | 28 +++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 44ab0de0bdd5d..89f7257ae522a 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -463,6 +463,7 @@ export class AssetStaging extends CoreConstruct { volumes, environment: options.environment, workingDirectory: options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR, + securityOpt: options.securityOpt ?? '', }); } } catch (err) { diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index 81315ee94be0d..c6b6b66881771 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -86,6 +86,14 @@ export interface BundlingOptions { * */ readonly outputType?: BundlingOutput; + + /** + * [Security configuration](https://docs.docker.com/engine/reference/run/#security-configuration) + * when running the docker container. + * + * @default - no security options + */ + readonly securityOpt?: string; } /** @@ -413,7 +421,7 @@ export interface DockerRunOptions { * [Security configuration](https://docs.docker.com/engine/reference/run/#security-configuration) * when running the docker container. * - * @default - no secutiy options + * @default - no security options */ readonly securityOpt?: string; } diff --git a/packages/@aws-cdk/core/test/staging.test.ts b/packages/@aws-cdk/core/test/staging.test.ts index e492f0a2dce88..c55e7a9286326 100644 --- a/packages/@aws-cdk/core/test/staging.test.ts +++ b/packages/@aws-cdk/core/test/staging.test.ts @@ -597,6 +597,34 @@ nodeunitShim({ test.done(); }, + + 'bundling with docker security option'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const asset = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SUCCESS], + securityOpt: 'no-new-privileges', + }, + assetHashType: AssetHashType.BUNDLE, + }); + + // THEN + test.equal( + readDockerStubInput(), + `run --rm --security-opt no-new-privileges ${USER_ARG} -v /input:/asset-input:delegated -v /output:/asset-output:delegated -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); + test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); + + test.done(); + }, + 'bundling with OUTPUT asset hash type'(test: Test) { // GIVEN const app = new App(); From e61a5b80fb19270a0ed21938b777390ce5d835cc Mon Sep 17 00:00:00 2001 From: Neil Ferreira Date: Mon, 5 Jul 2021 23:09:02 +0800 Subject: [PATCH 009/105] feat(ec2): add rds-data vpc endpoint (#15240) Fixes https://github.com/aws/aws-cdk/issues/15239 (ec2): Add rds-data VPC interface endpoints ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts | 1 + packages/@aws-cdk/aws-ec2/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 2250bdd37c6c3..2face39e7ccd5 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -288,6 +288,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly CLOUDWATCH_LOGS = new InterfaceVpcEndpointAwsService('logs'); public static readonly CLOUDWATCH = new InterfaceVpcEndpointAwsService('monitoring'); public static readonly RDS = new InterfaceVpcEndpointAwsService('rds'); + public static readonly RDS_DATA = new InterfaceVpcEndpointAwsService('rds-data'); public static readonly SAGEMAKER_API = new InterfaceVpcEndpointAwsService('sagemaker.api'); public static readonly SAGEMAKER_RUNTIME = new InterfaceVpcEndpointAwsService('sagemaker.runtime'); public static readonly SAGEMAKER_RUNTIME_FIPS = new InterfaceVpcEndpointAwsService('sagemaker.runtime-fips'); diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index b19cb54bd0b85..83ce458230cb0 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -243,6 +243,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_FIREHOSE", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KMS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.RDS", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.RDS_DATA", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_API", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_NOTEBOOK", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_RUNTIME", From ff044eddccfe1e4812e686343ca1a614b73a1a1f Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Tue, 6 Jul 2021 10:35:22 +0200 Subject: [PATCH 010/105] feat(acm): DaysToExpiry metric (#15424) Adds a convenient method to obtain the `DaysToExpiry` metric for an AWS Certificates Manager Certificate, without having to craft it yourself. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-certificatemanager/README.md | 18 +++++++++++ .../lib/certificate-base.ts | 32 +++++++++++++++++++ .../aws-certificatemanager/lib/certificate.ts | 21 +++++++++--- .../lib/dns-validated-certificate.ts | 6 +++- .../aws-certificatemanager/package.json | 4 ++- .../test/certificate.test.ts | 21 +++++++++++- yarn.lock | 2 +- 7 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 packages/@aws-cdk/aws-certificatemanager/lib/certificate-base.ts diff --git a/packages/@aws-cdk/aws-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index 24f1e72a3cb20..7068591d9c076 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -126,3 +126,21 @@ const certificate = Certificate.fromCertificateArn(this, 'Certificate', arn); To share the certificate between stacks in the same CDK application, simply pass the `Certificate` object between the stacks. + +## Metrics + +The `DaysToExpiry` metric is available via the `metricDaysToExpiry` method for +all certificates. This metric is emitted by AWS Certificates Manager once per +day until the certificate has effectively expired. + +An alarm can be created to determine whether a certificate is soon due for +renewal ussing the following code: + +```ts +const certificate = new Certificate(this, 'Certificate', { /* ... */ }); +certificate.metricDaysToExpiry().createAlarm({ + comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, + evaluationPeriods: 1, + threshold: 45, // Automatic rotation happens between 60 and 45 days before expiry +}); +``` diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate-base.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate-base.ts new file mode 100644 index 0000000000000..b66a845429abf --- /dev/null +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate-base.ts @@ -0,0 +1,32 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Statistic } from '@aws-cdk/aws-cloudwatch'; +import { Duration, Resource } from '@aws-cdk/core'; +import { ICertificate } from './certificate'; + +/** + * Shared implementation details of ICertificate implementations. + * + * @internal + */ +export abstract class CertificateBase extends Resource implements ICertificate { + public abstract readonly certificateArn: string; + + /** + * If the certificate is provisionned in a different region than the + * containing stack, this should be the region in which the certificate lives + * so we can correctly create `Metric` instances. + */ + protected readonly region?: string; + + public metricDaysToExpiry(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + period: Duration.days(1), + ...props, + dimensions: { CertificateArn: this.certificateArn }, + metricName: 'DaysToExpiry', + namespace: 'AWS/CertificateManager', + region: this.region, + statistic: Statistic.MINIMUM, + }); + } +} diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts index eac04dcb6df05..5c5182390bf95 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts @@ -1,6 +1,8 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as route53 from '@aws-cdk/aws-route53'; -import { IResource, Resource, Token } from '@aws-cdk/core'; +import { IResource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { CertificateBase } from './certificate-base'; import { CfnCertificate } from './certificatemanager.generated'; import { apexDomain } from './util'; @@ -14,6 +16,16 @@ export interface ICertificate extends IResource { * @attribute */ readonly certificateArn: string; + + /** + * Return the DaysToExpiry metric for this AWS Certificate Manager + * Certificate. By default, this is the minimum value over 1 day. + * + * This metric is no longer emitted once the certificate has effectively + * expired, so alarms configured on this metric should probably treat missing + * data as "breaching". + */ + metricDaysToExpiry(props?: cloudwatch.MetricOptions): cloudwatch.Metric; } /** @@ -169,14 +181,13 @@ export class CertificateValidation { /** * A certificate managed by AWS Certificate Manager */ -export class Certificate extends Resource implements ICertificate { - +export class Certificate extends CertificateBase implements ICertificate { /** * Import a certificate */ public static fromCertificateArn(scope: Construct, id: string, certificateArn: string): ICertificate { - class Import extends Resource implements ICertificate { - public certificateArn = certificateArn; + class Import extends CertificateBase { + public readonly certificateArn = certificateArn; } return new Import(scope, id); diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts index 2c47ad9c49d9e..fff0ef7df7d7d 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -5,6 +5,7 @@ import * as route53 from '@aws-cdk/aws-route53'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CertificateProps, ICertificate } from './certificate'; +import { CertificateBase } from './certificate-base'; /** * Properties to create a DNS validated certificate managed by AWS Certificate Manager @@ -54,7 +55,7 @@ export interface DnsValidatedCertificateProps extends CertificateProps { * * @resource AWS::CertificateManager::Certificate */ -export class DnsValidatedCertificate extends cdk.Resource implements ICertificate, cdk.ITaggable { +export class DnsValidatedCertificate extends CertificateBase implements ICertificate, cdk.ITaggable { public readonly certificateArn: string; /** @@ -63,6 +64,7 @@ export class DnsValidatedCertificate extends cdk.Resource implements ICertificat */ public readonly tags: cdk.TagManager; + protected readonly region?: string; private normalizedZoneName: string; private hostedZoneId: string; private domainName: string; @@ -70,6 +72,8 @@ export class DnsValidatedCertificate extends cdk.Resource implements ICertificat constructor(scope: Construct, id: string, props: DnsValidatedCertificateProps) { super(scope, id); + this.region = props.region; + this.domainName = props.domainName; this.normalizedZoneName = props.hostedZone.zoneName; // Remove trailing `.` from zone name diff --git a/packages/@aws-cdk/aws-certificatemanager/package.json b/packages/@aws-cdk/aws-certificatemanager/package.json index 0519341a876d3..c903bb46c9181 100644 --- a/packages/@aws-cdk/aws-certificatemanager/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/package.json @@ -80,6 +80,7 @@ "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-route53": "0.0.0", @@ -92,7 +93,8 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/core": "0.0.0", - "constructs": "^3.3.69" + "constructs": "^3.3.69", + "@aws-cdk/aws-cloudwatch": "0.0.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts index 1fd22c2175e26..22ebc72917b2b 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert-internal/jest'; import * as route53 from '@aws-cdk/aws-route53'; -import { Lazy, Stack } from '@aws-cdk/core'; +import { Duration, Lazy, Stack } from '@aws-cdk/core'; import { Certificate, CertificateValidation, ValidationMethod } from '../lib'; test('apex domain selection by default', () => { @@ -19,6 +19,25 @@ test('apex domain selection by default', () => { }); }); +test('metricDaysToExpiry', () => { + const stack = new Stack(); + + const certificate = new Certificate(stack, 'Certificate', { + domainName: 'test.example.com', + }); + + expect(stack.resolve(certificate.metricDaysToExpiry().toMetricConfig())).toEqual({ + metricStat: { + dimensions: [{ name: 'CertificateArn', value: stack.resolve(certificate.certificateArn) }], + metricName: 'DaysToExpiry', + namespace: 'AWS/CertificateManager', + period: Duration.days(1), + statistic: 'Minimum', + }, + renderingProperties: expect.anything(), + }); +}); + test('validation domain can be overridden', () => { const stack = new Stack(); diff --git a/yarn.lock b/yarn.lock index e43c8d14ff3ea..c1e70ddfc3533 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1609,7 +1609,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^26.0.23": +"@types/jest@^26.0.22", "@types/jest@^26.0.23": version "26.0.23" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7" integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA== From 31e6b1a88aaafb28e3916fbba918894435495906 Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Tue, 6 Jul 2021 10:30:50 -0700 Subject: [PATCH 011/105] fix(build): explicit non-private package not respected in packaging (#15435) The cdk-package build tool skips packaging in private packages by looking at the value of the "private" entry in package.json. Currently, this entry must be missing for cdk-package to treat the package as public, but we should also allow an explicit `false` value. fixes #15203 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- tools/cdk-build-tools/lib/package-info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cdk-build-tools/lib/package-info.ts b/tools/cdk-build-tools/lib/package-info.ts index 2ed8abff6171b..56e1fcacd3410 100644 --- a/tools/cdk-build-tools/lib/package-info.ts +++ b/tools/cdk-build-tools/lib/package-info.ts @@ -42,7 +42,7 @@ export function isJsii(): boolean { * Whether this is a private package */ export function isPrivate(): boolean { - return currentPackageJson().private !== undefined; + return currentPackageJson().private; } export interface File { From 647acfa3fdca6013614dfb9ebf0a2d55ea74e828 Mon Sep 17 00:00:00 2001 From: Seiya6329 Date: Tue, 6 Jul 2021 11:33:28 -0700 Subject: [PATCH 012/105] feat(appmesh): allow setting the DnsResponseType in DNS ServiceDiscovery (#15388) #### REV: - Adding new property `responseType` to DNS `ServiceDiscovery` - Changing `.cloudMap()` factory method to accept positional argument - Adding a runtime-error to check if the service discovery is defined when listener is specified BREAKING CHANGE: `ServiceDiscovery.cloudMap()` method has been changed to accept positional arguments ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/extensions/appmesh.ts | 4 +- packages/@aws-cdk/aws-appmesh/README.md | 23 +++--- .../@aws-cdk/aws-appmesh/lib/private/utils.ts | 2 +- .../aws-appmesh/lib/service-discovery.ts | 67 ++++++++++------- .../@aws-cdk/aws-appmesh/lib/virtual-node.ts | 23 ++++-- .../aws-appmesh/test/integ.mesh.expected.json | 3 +- .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 2 +- .../@aws-cdk/aws-appmesh/test/test.mesh.ts | 47 +++++++++++- .../aws-appmesh/test/test.virtual-node.ts | 75 +++++++++++++++++++ 9 files changed, 188 insertions(+), 58 deletions(-) diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts index 5442cb4fd6d15..ccd9a2ece2bc2 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts @@ -285,9 +285,7 @@ export class AppMeshExtension extends ServiceExtension { mesh: this.mesh, virtualNodeName: this.parentService.id, serviceDiscovery: service.cloudMapService - ? appmesh.ServiceDiscovery.cloudMap({ - service: service.cloudMapService, - }) + ? appmesh.ServiceDiscovery.cloudMap(service.cloudMapService) : undefined, listeners: [addListener(this.protocol, containerextension.trafficPort)], }); diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index f4f9930f4676c..e2436bed89295 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -149,9 +149,7 @@ const namespace = new servicediscovery.PrivateDnsNamespace(this, 'test-namespace const service = namespace.createService('Svc'); const node = mesh.addVirtualNode('virtual-node', { - serviceDiscovery: appmesh.ServiceDiscovery.cloudMap({ - service: service, - }), + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), listeners: [appmesh.VirtualNodeListener.httpNodeListener({ port: 8081, healthCheck: appmesh.HealthCheck.http({ @@ -171,9 +169,7 @@ Create a `VirtualNode` with the constructor and add tags. ```ts const node = new VirtualNode(this, 'node', { mesh, - serviceDiscovery: appmesh.ServiceDiscovery.cloudMap({ - service: service, - }), + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), listeners: [appmesh.VirtualNodeListener.http({ port: 8080, healthCheck: appmesh.HealthCheck.http({ @@ -205,9 +201,7 @@ Create a `VirtualNode` with the constructor and add backend virtual service. ```ts const node = new VirtualNode(this, 'node', { mesh, - serviceDiscovery: appmesh.ServiceDiscovery.cloudMap({ - service: service, - }), + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), listeners: [appmesh.VirtualNodeListener.httpNodeListener({ port: 8080, healthCheck: appmesh.HealthCheck.http({ @@ -360,9 +354,7 @@ const namespace = new servicediscovery.PrivateDnsNamespace(this, 'test-namespace const service = namespace.createService('Svc'); const node = mesh.addVirtualNode('virtual-node', { - serviceDiscovery: appmesh.ServiceDiscovery.cloudMap({ - service: service, - }), + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), outlierDetection: { baseEjectionDuration: cdk.Duration.seconds(10), interval: cdk.Duration.seconds(30), @@ -381,7 +373,11 @@ connection pool properties per listener protocol types. // A Virtual Node with a gRPC listener with a connection pool set const node = new appmesh.VirtualNode(stack, 'node', { mesh, - serviceDiscovery: appmesh.ServiceDiscovery.dns('node'), + // 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: appmesh.ServiceDiscovery.dns('node', appmesh.ResponseType.ENDPOINTS), listeners: [appmesh.VirtualNodeListener.http({ port: 80, connectionPool: { @@ -690,4 +686,3 @@ new appmesh.VirtualNode(stack, 'test-node', { mesh: sharedMesh, }); ``` - diff --git a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts index ad277985250cb..daa95f18bc410 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -1,7 +1,7 @@ +import { Token, TokenComparison } from '@aws-cdk/core'; import { CfnVirtualNode } from '../appmesh.generated'; import { ListenerTlsOptions } from '../listener-tls-options'; import { TlsClientPolicy } from '../tls-client-policy'; -import { Token, TokenComparison } from '@aws-cdk/core'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order diff --git a/packages/@aws-cdk/aws-appmesh/lib/service-discovery.ts b/packages/@aws-cdk/aws-appmesh/lib/service-discovery.ts index 961357945a16b..013eefec5b8af 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/service-discovery.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/service-discovery.ts @@ -5,27 +5,6 @@ import { CfnVirtualNode } from './appmesh.generated'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct } from '@aws-cdk/core'; - -/** - * Represents the properties needed to define CloudMap Service Discovery - */ -export interface CloudMapServiceDiscoveryOptions { - /** - * The AWS Cloud Map Service to use for service discovery - */ - readonly service: cloudmap.IService; - - /** - * A string map that contains attributes with values that you can use to - * filter instances by any custom attribute that you specified when you - * registered the instance. Only instances that match all of the specified - * key/value pairs will be returned. - * - * @default - no instance attributes - */ - readonly instanceAttributes?: {[key: string]: string}; -} - /** * Properties for VirtualNode Service Discovery */ @@ -45,22 +24,49 @@ export interface ServiceDiscoveryConfig { readonly cloudmap?: CfnVirtualNode.AwsCloudMapServiceDiscoveryProperty; } +/** + * Enum of DNS service discovery response type + */ +export enum DnsResponseType { + /** + * DNS resolver returns a loadbalanced set of endpoints and the traffic would be sent to the given endpoints. + * It would not drain existing connections to other endpoints that are not part of this list. + */ + LOAD_BALANCER = 'LOADBALANCER', + + /** + * DNS resolver is returning all the endpoints. + * This also means that if an endpoint is missing, it would drain the current connections to the missing endpoint. + */ + ENDPOINTS = 'ENDPOINTS', +} + /** * Provides the Service Discovery method a VirtualNode uses */ export abstract class ServiceDiscovery { /** * Returns DNS based service discovery + * + * @param hostname + * @param responseType Specifies the DNS response type for the virtual node. + * The default is `DnsResponseType.LOAD_BALANCER`. */ - public static dns(hostname: string): ServiceDiscovery { - return new DnsServiceDiscovery(hostname); + public static dns(hostname: string, responseType?: DnsResponseType): ServiceDiscovery { + return new DnsServiceDiscovery(hostname, responseType); } /** * Returns Cloud Map based service discovery + * + * @param service The AWS Cloud Map Service to use for service discovery + * @param instanceAttributes A string map that contains attributes with values that you can use to + * filter instances by any custom attribute that you specified when you + * registered the instance. Only instances that match all of the specified + * key/value pairs will be returned. */ - public static cloudMap(options: CloudMapServiceDiscoveryOptions): ServiceDiscovery { - return new CloudMapServiceDiscovery(options); + public static cloudMap(service: cloudmap.IService, instanceAttributes?: {[key: string]: string}): ServiceDiscovery { + return new CloudMapServiceDiscovery(service, instanceAttributes); } /** @@ -71,16 +77,19 @@ export abstract class ServiceDiscovery { class DnsServiceDiscovery extends ServiceDiscovery { private readonly hostname: string; + private readonly responseType?: DnsResponseType; - constructor(hostname: string) { + constructor(hostname: string, responseType?: DnsResponseType) { super(); this.hostname = hostname; + this.responseType = responseType; } public bind(_scope: Construct): ServiceDiscoveryConfig { return { dns: { hostname: this.hostname, + responseType: this.responseType, }, }; } @@ -90,10 +99,10 @@ class CloudMapServiceDiscovery extends ServiceDiscovery { private readonly service: cloudmap.IService; private readonly instanceAttributes?: {[key: string]: string}; - constructor(options: CloudMapServiceDiscoveryOptions) { + constructor(service: cloudmap.IService, instanceAttributes?: {[key: string]: string}) { super(); - this.service = options.service; - this.instanceAttributes = options.instanceAttributes; + this.service = service; + this.instanceAttributes = instanceAttributes; } public bind(_scope: Construct): ServiceDiscoveryConfig { diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts index ffc56239c47f0..bba83feb0b3d1 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts @@ -4,7 +4,7 @@ import { Construct } from 'constructs'; import { CfnVirtualNode } from './appmesh.generated'; import { IMesh, Mesh } from './mesh'; import { renderMeshOwner, renderTlsClientPolicy } from './private/utils'; -import { ServiceDiscovery } from './service-discovery'; +import { ServiceDiscovery, ServiceDiscoveryConfig } from './service-discovery'; import { AccessLog, BackendDefaults, Backend } from './shared-interfaces'; import { VirtualNodeListener, VirtualNodeListenerConfig } from './virtual-node-listener'; @@ -176,6 +176,8 @@ export class VirtualNode extends VirtualNodeBase { */ public readonly mesh: IMesh; + private readonly serviceDiscoveryConfig?: ServiceDiscoveryConfig; + private readonly backends = new Array(); private readonly listeners = new Array(); @@ -185,11 +187,11 @@ export class VirtualNode extends VirtualNodeBase { }); this.mesh = props.mesh; + this.serviceDiscoveryConfig = props.serviceDiscovery?.bind(this); props.backends?.forEach(backend => this.addBackend(backend)); props.listeners?.forEach(listener => this.addListener(listener)); const accessLogging = props.accessLog?.bind(this); - const serviceDiscovery = props.serviceDiscovery?.bind(this); const node = new CfnVirtualNode(this, 'Resource', { virtualNodeName: this.physicalName, @@ -205,10 +207,7 @@ export class VirtualNode extends VirtualNodeBase { }, } : undefined, - serviceDiscovery: { - dns: serviceDiscovery?.dns, - awsCloudMap: serviceDiscovery?.cloudmap, - }, + serviceDiscovery: renderServiceDiscovery(this.serviceDiscoveryConfig), logging: accessLogging !== undefined ? { accessLog: accessLogging.virtualNodeAccessLog, } : undefined, @@ -234,6 +233,9 @@ export class VirtualNode extends VirtualNodeBase { * @see https://github.com/aws/aws-app-mesh-roadmap/issues/120 */ public addListener(listener: VirtualNodeListener) { + if (!this.serviceDiscoveryConfig) { + throw new Error('Service discovery information is required for a VirtualNode with a listener.'); + } this.listeners.push(listener.bind(this)); } @@ -259,3 +261,12 @@ export interface VirtualNodeAttributes { */ readonly mesh: IMesh; } + +function renderServiceDiscovery(config?: ServiceDiscoveryConfig): CfnVirtualNode.ServiceDiscoveryProperty | undefined { + return config + ? { + dns: config?.dns, + awsCloudMap: config?.cloudmap, + } + : undefined; +} diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index 34333711b8488..3744fe7583cf1 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -1202,7 +1202,8 @@ }, "ServiceDiscovery": { "DNS": { - "Hostname": "node4.domain.local" + "Hostname": "node4.domain.local", + "ResponseType": "ENDPOINTS" } } }, diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 3520b49331f05..53a539204ef36 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -112,7 +112,7 @@ const node3 = mesh.addVirtualNode('node3', { }); const node4 = mesh.addVirtualNode('node4', { - serviceDiscovery: appmesh.ServiceDiscovery.dns(`node4.${namespace.namespaceName}`), + serviceDiscovery: appmesh.ServiceDiscovery.dns(`node4.${namespace.namespaceName}`, appmesh.DnsResponseType.ENDPOINTS), listeners: [appmesh.VirtualNodeListener.http({ tls: { mode: appmesh.TlsMode.STRICT, diff --git a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts index dfecbc4292c88..61a51b9578431 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts @@ -105,9 +105,7 @@ export = { // WHEN new appmesh.VirtualNode(stack, 'test-node', { mesh, - serviceDiscovery: appmesh.ServiceDiscovery.cloudMap({ - service: service, - }), + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service), }); // THEN @@ -125,6 +123,49 @@ export = { test.done(); }, + 'VirtualService can use CloudMap service with instanceAttributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + const vpc = new ec2.Vpc(stack, 'vpc'); + const namespace = new cloudmap.PrivateDnsNamespace(stack, 'test-namespace', { + vpc, + name: 'domain.local', + }); + const service = namespace.createService('Svc'); + + const instanceAttribute : { [key: string]: string} = {}; + instanceAttribute.testKey = 'testValue'; + + // WHEN + new appmesh.VirtualNode(stack, 'test-node', { + mesh, + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap(service, instanceAttribute), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualNode', { + Spec: { + ServiceDiscovery: { + AWSCloudMap: { + NamespaceName: 'domain.local', + ServiceName: { 'Fn::GetAtt': ['testnamespaceSvcB55702EC', 'Name'] }, + Attributes: [ + { + Key: 'testKey', + Value: 'testValue', + }, + ], + }, + }, + }, + })); + + test.done(); + }, + 'When adding a VirtualNode to a mesh': { 'with empty default listeners and backends': { 'should create default resource'(test: Test) { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts index 171c754f09954..19d36126380fb 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -507,6 +507,7 @@ export = { }, }, )], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // THEN @@ -552,6 +553,7 @@ export = { certificate: appmesh.TlsCertificate.file('path/to/certChain', 'path/to/privateKey'), }, })], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // THEN @@ -595,6 +597,7 @@ export = { certificate: appmesh.TlsCertificate.sds('secret_certificate'), }, })], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // THEN @@ -638,6 +641,7 @@ export = { certificate: appmesh.TlsCertificate.file('path/to/certChain', 'path/to/privateKey'), }, })], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // THEN @@ -683,6 +687,7 @@ export = { }, }), ], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // THEN @@ -723,6 +728,7 @@ export = { }, }), ], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // THEN @@ -762,6 +768,7 @@ export = { }, }), ], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // THEN @@ -801,6 +808,7 @@ export = { }, }), ], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // THEN @@ -872,6 +880,7 @@ export = { certificate: appmesh.TlsCertificate.file('path/to/certChain', 'path/to/privateKey'), }, })], + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); // WHEN @@ -923,6 +932,72 @@ export = { test.done(); }, }, + + 'with DNS service discovery': { + 'should allow set response type'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + // WHEN + new appmesh.VirtualNode(stack, 'test-node', { + mesh, + serviceDiscovery: appmesh.ServiceDiscovery.dns('test', appmesh.DnsResponseType.LOAD_BALANCER), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualNode', { + Spec: { + ServiceDiscovery: { + DNS: { + Hostname: 'test', + ResponseType: 'LOADBALANCER', + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with listener and without service discovery': { + 'should throw an error'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = new appmesh.VirtualNode(stack, 'test-node', { + mesh, + }); + + // WHEN + THEN + test.throws(() => { + new appmesh.VirtualNode(stack, 'test-node-2', { + mesh, + listeners: [appmesh.VirtualNodeListener.http()], + }); + }, /Service discovery information is required for a VirtualNode with a listener/); + + test.throws(() => { + mesh.addVirtualNode('test-node-3', { + listeners: [appmesh.VirtualNodeListener.http()], + }); + }, /Service discovery information is required for a VirtualNode with a listener/); + + test.throws(() => { + node.addListener(appmesh.VirtualNodeListener.http()); + }, /Service discovery information is required for a VirtualNode with a listener/); + + test.done(); + }, + }, }, }; From d779be08c1fb20febb7dd9a695686be20b68b707 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Tue, 6 Jul 2021 12:11:30 -0700 Subject: [PATCH 013/105] chore: new private 'cdk-release' tool for performing releases (#15331) Introduce a new repo-private tool called `cdk-release` that replaces `standard-version` in our 'bump' process. It's responsible for updating the version file with the new version, generating the Changelog based on on the commit history, and performing the commit that includes the two above files. It allows us to correctly handle edge cases that we had to work around previously with `standard-version`: * For patch releases, `standard-version` always generated the main header as H3, while we want H2. We always had to correct that manually, and we sometimes forgot. This tool does the correct thing for patch releases too. * We had to hack around when we wanted to change the heading 'BREAKING CHANGES' to 'BREAKING CHANGES TO EXPERIMENTAL FEATURES'. This tool now does it natively, no hacks needed. * In V2, `standard-version` couldn't figure out the tag to compare to, because we have tags for 2 major versions present in the repo. This tool handles this without the hacks of locally removing and then re-fetching the tags. * Also in V2, we want to strip the changes to experimental packages (as those are not included in `aws-cdk-lib`). With `standard-version`, we had to grep in the resulting Changelog, which was very fragile (for example, in `2.0.0-rc.7`, our Changelog includes breaking changes to `appmesh`, which is an experimental module). This tool handles this case natively, by filtering out commits, without the need for fragile Changelog grepping. To make sure we don't break our release process, allow passing the environment variable `LEGACY_BUMP` as truthy to fall back on `standard-version`. Once we make at least one successful release, in both major versions, using this new tool, I'll remove the old `standard-version` based code in a separate PR. In a subsequent PR, the tool will be enhanced with the capability to generate separate version bumps and Changelogs for experimental packages in V2. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- bump.sh | 6 +- scripts/bump.js | 51 +++-- tools/cdk-release/.eslintrc.js | 3 + tools/cdk-release/.gitignore | 15 ++ tools/cdk-release/.npmignore | 16 ++ tools/cdk-release/LICENSE | 201 ++++++++++++++++++ tools/cdk-release/NOTICE | 2 + tools/cdk-release/README.md | 21 ++ tools/cdk-release/jest.config.js | 10 + tools/cdk-release/lib/conventional-commits.ts | 138 ++++++++++++ tools/cdk-release/lib/defaults.ts | 32 +++ tools/cdk-release/lib/index.ts | 57 +++++ tools/cdk-release/lib/lifecycles/bump.ts | 122 +++++++++++ tools/cdk-release/lib/lifecycles/changelog.ts | 123 +++++++++++ tools/cdk-release/lib/lifecycles/commit.ts | 45 ++++ tools/cdk-release/lib/private/files.ts | 9 + tools/cdk-release/lib/private/print.ts | 23 ++ .../cdk-release/lib/private/run-exec-file.ts | 19 ++ tools/cdk-release/lib/types.ts | 64 ++++++ tools/cdk-release/lib/updaters/index.ts | 59 +++++ tools/cdk-release/lib/updaters/types/json.ts | 28 +++ .../lib/updaters/types/plain-text.ts | 12 ++ tools/cdk-release/package.json | 70 ++++++ tools/cdk-release/test/changelog.test.ts | 110 ++++++++++ tools/cdk-release/tsconfig.json | 21 ++ 25 files changed, 1239 insertions(+), 18 deletions(-) create mode 100644 tools/cdk-release/.eslintrc.js create mode 100644 tools/cdk-release/.gitignore create mode 100644 tools/cdk-release/.npmignore create mode 100644 tools/cdk-release/LICENSE create mode 100644 tools/cdk-release/NOTICE create mode 100644 tools/cdk-release/README.md create mode 100644 tools/cdk-release/jest.config.js create mode 100644 tools/cdk-release/lib/conventional-commits.ts create mode 100644 tools/cdk-release/lib/defaults.ts create mode 100644 tools/cdk-release/lib/index.ts create mode 100644 tools/cdk-release/lib/lifecycles/bump.ts create mode 100644 tools/cdk-release/lib/lifecycles/changelog.ts create mode 100644 tools/cdk-release/lib/lifecycles/commit.ts create mode 100644 tools/cdk-release/lib/private/files.ts create mode 100644 tools/cdk-release/lib/private/print.ts create mode 100644 tools/cdk-release/lib/private/run-exec-file.ts create mode 100644 tools/cdk-release/lib/types.ts create mode 100644 tools/cdk-release/lib/updaters/index.ts create mode 100644 tools/cdk-release/lib/updaters/types/json.ts create mode 100644 tools/cdk-release/lib/updaters/types/plain-text.ts create mode 100644 tools/cdk-release/package.json create mode 100644 tools/cdk-release/test/changelog.test.ts create mode 100644 tools/cdk-release/tsconfig.json diff --git a/bump.sh b/bump.sh index 73954148de7c6..b3c04198b50d3 100755 --- a/bump.sh +++ b/bump.sh @@ -15,5 +15,9 @@ set -euo pipefail scriptdir=$(cd $(dirname $0) && pwd) cd ${scriptdir} -yarn --frozen-lockfile +yarn install --frozen-lockfile +if [[ "${LEGACY_BUMP:-}" == "" ]]; then + # if we're using 'cdk-release' for the bump, build that package, including all of its dependencies + npx lerna run build --include-dependencies --scope cdk-release +fi ${scriptdir}/scripts/bump.js ${1:-minor} diff --git a/scripts/bump.js b/scripts/bump.js index dee713472c944..a0a05f398669f 100755 --- a/scripts/bump.js +++ b/scripts/bump.js @@ -1,15 +1,13 @@ #!/usr/bin/env node + const fs = require('fs'); const path = require('path'); const semver = require('semver'); const ver = require('./resolve-version'); const { exec } = require('child_process'); -const repoRoot = path.join(__dirname, '..'); - -const releaseAs = process.argv[2] || 'minor'; -const forTesting = process.env.BUMP_CANDIDATE || false; async function main() { + const releaseAs = process.argv[2] || 'minor'; if (releaseAs !== 'minor' && releaseAs !== 'patch') { throw new Error(`invalid bump type "${releaseAs}". only "minor" (the default) and "patch" are allowed. major version bumps require *slightly* more intention`); } @@ -17,6 +15,7 @@ async function main() { console.error(`Starting ${releaseAs} version bump`); console.error('Current version information:', JSON.stringify(ver, undefined, 2)); + const repoRoot = path.join(__dirname, '..'); const changelogPath = path.join(repoRoot, ver.changelogFile); const opts = { releaseAs: releaseAs, @@ -30,6 +29,12 @@ async function main() { } }; + const majorVersion = semver.major(ver.version); + if (majorVersion > 1) { + opts.stripExperimentalChanges = true; + } + + const forTesting = process.env.BUMP_CANDIDATE || false; if (forTesting) { opts.skip.commit = true; opts.skip.changelog = true; @@ -37,24 +42,36 @@ async function main() { // if we are on a "stable" branch, add a pre-release tag ("rc") to the // version number as a safety in case this version will accidentally be // published. - opts.prerelease = ver.prerelease || 'rc' + opts.prerelease = ver.prerelease || 'rc'; console.error(`BUMP_CANDIDATE is set, so bumping version for testing (with the "${opts.prerelease}" prerelease tag)`); } - // `standard-release` will -- among other things -- create the changelog. - // However, on the v2 branch, `conventional-changelog` (which `standard-release` uses) gets confused - // and creates really muddled changelogs with both v1 and v2 releases intermingled, and lots of missing data. - // A super HACK here is to locally remove all version tags that don't match this major version prior - // to doing the bump, and then later fetching to restore those tags. - const majorVersion = semver.major(ver.version); - await exec(`git tag -d $(git tag -l | grep -v '^v${majorVersion}.')`); + const useLegacyBump = process.env.LEGACY_BUMP || false; + if (useLegacyBump) { + console.error("ℹ️ Using the third-party 'standard-version' package to perform the bump"); - // Delay loading standard-version until the git tags have been pruned. - const standardVersion = require('standard-version'); - await standardVersion(opts); + // `standard-release` will -- among other things -- create the changelog. + // However, on the v2 branch, `conventional-changelog` (which `standard-release` uses) gets confused + // and creates really muddled changelogs with both v1 and v2 releases intermingled, and lots of missing data. + // A super HACK here is to locally remove all version tags that don't match this major version prior + // to doing the bump, and then later fetching to restore those tags. + await exec(`git tag -d $(git tag -l | grep -v '^v${majorVersion}.')`); - // fetch back the tags, and only the tags, removed locally above - await exec('git fetch origin "refs/tags/*:refs/tags/*"'); + // Delay loading standard-version until the git tags have been pruned. + const standardVersion = require('standard-version'); + await standardVersion(opts); + + // fetch back the tags, and only the tags, removed locally above + await exec('git fetch origin "refs/tags/*:refs/tags/*"'); + } else { + // this is incredible, but passing this option to standard-version actually makes it crash! + // good thing we're getting rid of it... + opts.verbose = !!process.env.VERBOSE; + 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('cdk-release'); + cdkRelease(opts); + } } main().catch(err => { diff --git a/tools/cdk-release/.eslintrc.js b/tools/cdk-release/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/tools/cdk-release/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/tools/cdk-release/.gitignore b/tools/cdk-release/.gitignore new file mode 100644 index 0000000000000..acdfee7f84c04 --- /dev/null +++ b/tools/cdk-release/.gitignore @@ -0,0 +1,15 @@ +*.js +node_modules +*.js.map +*.d.ts + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +*.snk +!.eslintrc.js + +junit.xml + +!jest.config.js \ No newline at end of file diff --git a/tools/cdk-release/.npmignore b/tools/cdk-release/.npmignore new file mode 100644 index 0000000000000..c480a1570dbe3 --- /dev/null +++ b/tools/cdk-release/.npmignore @@ -0,0 +1,16 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +.LAST_BUILD +*.snk +.eslintrc.js + +# exclude cdk artifacts +**/cdk.out +junit.xml + +jest.config.js \ No newline at end of file diff --git a/tools/cdk-release/LICENSE b/tools/cdk-release/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/tools/cdk-release/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tools/cdk-release/NOTICE b/tools/cdk-release/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/tools/cdk-release/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/tools/cdk-release/README.md b/tools/cdk-release/README.md new file mode 100644 index 0000000000000..16997e2fe72dd --- /dev/null +++ b/tools/cdk-release/README.md @@ -0,0 +1,21 @@ +# cdk-release + +This is a repo-private tool that we use for performing a release: +bumping the version of the package(s), +generating the Changelog file(s), +creating a commit, etc. + +We used to rely on [standard-version](https://www.npmjs.com/package/standard-version) +for this purpose, but our case is so (haha) non-standard, +with `aws-cdk-lib` excluding experimental modules, +and the need for separate Changelog files for V2 experimental modules, +that we decided we need a tool that we have full control over +(plus, `standard-version` has some problems too, +like messing up the headings, +and having problems with both V1 and V2 tags in the same repo). + +This library is called from the +[`bump.js` file](../../scripts/bump.js), +which is called from the [`bump.sh` script](../../bump.sh), +which is called by a CodeBuild job that creates the 'bump' +PR every time we perform a CDK release. diff --git a/tools/cdk-release/jest.config.js b/tools/cdk-release/jest.config.js new file mode 100644 index 0000000000000..07f5f6c432bb6 --- /dev/null +++ b/tools/cdk-release/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 60, + }, + }, +}; diff --git a/tools/cdk-release/lib/conventional-commits.ts b/tools/cdk-release/lib/conventional-commits.ts new file mode 100644 index 0000000000000..ddf8b82f22050 --- /dev/null +++ b/tools/cdk-release/lib/conventional-commits.ts @@ -0,0 +1,138 @@ +import * as fs from 'fs-extra'; +import { ReleaseOptions } from './types'; +// 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 conventionalCommitsParser = require('conventional-commits-parser'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const gitRawCommits = require('git-raw-commits'); + +/** + * The optional notes in the commit message. + * Today, the only notes are 'BREAKING CHANGES'. + */ +export interface ConventionalCommitNote { + /** Today, always 'BREAKING CHANGE'. */ + readonly title: string; + + /** The body of the note. */ + readonly text: string; +} + +/** For now, only needed for unit tests. */ +export interface ConventionalCommitReference { +} + +export interface ConventionalCommit { + /** The type of the commit ('feat', 'fix', etc.). */ + readonly type: string; + + /** The optional scope of the change ('core', 'aws-s3', 's3', etc.). */ + readonly scope?: string; + + /** The subject is the remaining part of the first line without 'type' and 'scope'. */ + readonly subject: string; + + /** + * The header is the entire first line of the commit + * ((): ). + */ + readonly header: string; + + /** + * The optional notes in the commit message. + * Today, the only notes are 'BREAKING CHANGES'. + */ + readonly notes: ConventionalCommitNote[]; + + /** + * References inside the commit body + * (for example, to issues or Pull Requests that this commit is linked to). + */ + readonly references: ConventionalCommitReference[]; +} + +/** + * Returns a list of all Conventional Commits in the Git repository since the tag `gitTag`. + * The commits will be sorted in chronologically descending order + * (that is, later/newer commits will be earlier in the array). + * + * @param gitTag the string representing the Git tag, + * will be used to limit the returned commits to only those added after that tag + */ +export async function getConventionalCommitsFromGitHistory(gitTag: string): Promise { + const ret = new Array(); + return new Promise((resolve, reject) => { + const conventionalCommitsStream = gitRawCommits({ + format: '%B%n-hash-%n%H', + // our tags have the 'v' prefix + from: gitTag, + // path: options.path, + }).pipe(conventionalCommitsParser()); + + conventionalCommitsStream.on('data', function (data: any) { + // filter out all commits that don't conform to the Conventional Commits standard + // (they will have an empty 'type' property) + if (data.type) { + ret.push(data); + } + }); + conventionalCommitsStream.on('end', function () { + resolve(ret); + }); + conventionalCommitsStream.on('error', function (err: any) { + reject(err); + }); + }); +} + +/** + * Filters commits based on the criteria in `args` + * (right now, the only criteria is whether to remove commits that relate to experimental packages). + * + * @param args configuration + * @param commits the array of Conventional Commits to filter + * @returns an array of ConventionalCommit objects which is a subset of `commits` + * (possibly exactly equal to `commits`) + */ +export function filterCommits(args: ReleaseOptions, commits: ConventionalCommit[]): ConventionalCommit[] { + if (!args.stripExperimentalChanges) { + return commits; + } + + // a get a list of packages from our monorepo + const project = new lerna_project.Project(); + const packages = project.getPackagesSync(); + const experimentalPackageNames: string[] = packages + .filter((pkg: any) => { + const pkgJson = fs.readJsonSync(pkg.manifestLocation); + return pkgJson.name.startsWith('@aws-cdk/') + && (pkgJson.maturity === 'experimental' || pkgJson.maturity === 'developer-preview'); + }) + .map((pkg: any) => pkg.name.substr('@aws-cdk/'.length)); + + const experimentalScopes = flatMap(experimentalPackageNames, (pkgName) => [ + pkgName, + ...(pkgName.startsWith('aws-') + ? [ + // if the package name starts with 'aws', like 'aws-s3', + // also include in the scopes variants without the prefix, + // and without the '-' in the prefix + // (so, 's3' and 'awss3') + pkgName.substr('aws-'.length), + pkgName.replace(/^aws-/, 'aws'), + ] + : [] + ), + ]); + + return commits.filter(commit => !commit.scope || !experimentalScopes.includes(commit.scope)); +} + +function flatMap(xs: T[], fn: (x: T) => U[]): U[] { + const ret = new Array(); + for (const x of xs) { + ret.push(...fn(x)); + } + return ret; +} diff --git a/tools/cdk-release/lib/defaults.ts b/tools/cdk-release/lib/defaults.ts new file mode 100644 index 0000000000000..22f53ebe806aa --- /dev/null +++ b/tools/cdk-release/lib/defaults.ts @@ -0,0 +1,32 @@ +import { ReleaseOptions } from './types'; + +const defaultPackageFiles = [ + 'package.json', + 'bower.json', + 'manifest.json', +]; + +export const defaultBumpFiles = defaultPackageFiles.concat([ + 'package-lock.json', + 'npm-shrinkwrap.json', +]); + +export const defaults: Partial = { + infile: 'CHANGELOG.md', + // firstRelease: false, + sign: false, + // noVerify: false, + // commitAll: false, + silent: false, + scripts: {}, + skip: { + tag: true, + }, + packageFiles: defaultPackageFiles, + bumpFiles: defaultBumpFiles, + dryRun: false, + // gitTagFallback: true, + releaseCommitMessageFormat: 'chore(release): {{currentTag}}', + changeLogHeader: '# Changelog\n\nAll notable changes to this project will be documented in this file. ' + + 'See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.\n', +}; diff --git a/tools/cdk-release/lib/index.ts b/tools/cdk-release/lib/index.ts new file mode 100644 index 0000000000000..a770db47868e6 --- /dev/null +++ b/tools/cdk-release/lib/index.ts @@ -0,0 +1,57 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { filterCommits, getConventionalCommitsFromGitHistory } from './conventional-commits'; +import { defaults } from './defaults'; +import { bump } from './lifecycles/bump'; +import { changelog } from './lifecycles/changelog'; +import { commit } from './lifecycles/commit'; +import { debug, debugObject } from './private/print'; +import { ReleaseOptions } from './types'; +import { resolveUpdaterObjectFromArgument } from './updaters'; + +module.exports = async function main(opts: ReleaseOptions): Promise { + // handle the default options + const args: ReleaseOptions = { + ...defaults, + ...opts, + }; + debugObject(args, 'options are (including defaults)', args); + + const packageInfo = determinePackageInfo(args); + debugObject(args, 'packageInfo is', packageInfo); + + const currentVersion = packageInfo.version; + debug(args, 'Current version is: ' + currentVersion); + + const commits = await getConventionalCommitsFromGitHistory(`v${currentVersion}`); + const filteredCommits = filterCommits(args, commits); + debugObject(args, 'Found and filtered commits', filteredCommits); + + const bumpResult = await bump(args, currentVersion); + const newVersion = bumpResult.newVersion; + debug(args, 'New version is: ' + newVersion); + + const changelogResult = await changelog(args, currentVersion, newVersion, filteredCommits); + + await commit(args, newVersion, [...bumpResult.changedFiles, ...changelogResult.changedFiles]); +}; + +interface PackageInfo { + version: string; + private: string | boolean | null | undefined; +} + +function determinePackageInfo(args: ReleaseOptions): PackageInfo { + for (const packageFile of args.packageFiles ?? []) { + const updater = resolveUpdaterObjectFromArgument(packageFile); + const pkgPath = path.resolve(process.cwd(), updater.filename); + const contents = fs.readFileSync(pkgPath, 'utf8'); + // we stop on the first (successful) option + return { + version: updater.updater.readVersion(contents), + private: typeof updater.updater.isPrivate === 'function' ? updater.updater.isPrivate(contents) : false, + }; + } + + throw new Error('Could not establish the version to bump!'); +} diff --git a/tools/cdk-release/lib/lifecycles/bump.ts b/tools/cdk-release/lib/lifecycles/bump.ts new file mode 100644 index 0000000000000..bb4465461f2d5 --- /dev/null +++ b/tools/cdk-release/lib/lifecycles/bump.ts @@ -0,0 +1,122 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as semver from 'semver'; +import { writeFile } from '../private/files'; +import { notify } from '../private/print'; +import { ReleaseOptions, ReleaseType } from '../types'; +import { resolveUpdaterObjectFromArgument } from '../updaters/index'; + +export interface BumpResult { + readonly newVersion: string; + readonly changedFiles: string[]; +} + +export async function bump(args: ReleaseOptions, currentVersion: string): Promise { + if (args.skip?.bump) { + return { + newVersion: currentVersion, + changedFiles: [], + }; + } + + const releaseType = getReleaseType(args.prerelease, args.releaseAs, currentVersion); + const newVersion = semver.inc(currentVersion, releaseType, args.prerelease); + if (!newVersion) { + throw new Error('Could not increment version: ' + currentVersion); + } + const changedFiles = updateBumpFiles(args, newVersion); + return { newVersion, changedFiles }; +} + +function getReleaseType(prerelease: string | undefined, expectedReleaseType: ReleaseType, currentVersion: string): semver.ReleaseType { + if (typeof prerelease === 'string') { + if (isInPrerelease(currentVersion)) { + if (shouldContinuePrerelease(currentVersion, expectedReleaseType) || + getTypePriority(getCurrentActiveType(currentVersion)) > getTypePriority(expectedReleaseType) + ) { + return 'prerelease'; + } + } + + return 'pre' + expectedReleaseType as semver.ReleaseType; + } else { + return expectedReleaseType; + } +} + +function isInPrerelease(version: string): boolean { + return Array.isArray(semver.prerelease(version)); +} + +/** + * if a version is currently in pre-release state, + * and if it current in-pre-release type is same as expect type, + * it should continue the pre-release with the same type + * + * @param version + * @param expectType + * @return {boolean} + */ +function shouldContinuePrerelease(version: string, expectType: ReleaseType): boolean { + return getCurrentActiveType(version) === expectType; +} + +const TypeList = ['major', 'minor', 'patch'].reverse(); +/** + * extract the in-pre-release type in target version + * + * @param version + * @return {string} + */ +function getCurrentActiveType(version: string): string { + for (const item of TypeList) { + if ((semver as any)[item](version)) { + return item; + } + } + throw new Error('unreachable'); +} + +/** + * calculate the priority of release type, + * major - 2, minor - 1, patch - 0 + * + * @param type + * @return {number} + */ +function getTypePriority(type: string): number { + return TypeList.indexOf(type); +} + +/** + * attempt to update the version number in provided `bumpFiles` + * @param args config object + * @param newVersion version number to update to. + * @return the collection of file paths that were actually changed + */ +function updateBumpFiles(args: ReleaseOptions, newVersion: string): string[] { + const ret = new Array(); + + for (const bumpFile of (args.bumpFiles ?? [])) { + const updater = resolveUpdaterObjectFromArgument(bumpFile); + if (!updater) { + continue; + } + const configPath = path.resolve(process.cwd(), updater.filename); + const stat = fs.lstatSync(configPath); + if (!stat.isFile()) { + continue; + } + const contents = fs.readFileSync(configPath, 'utf8'); + notify(args, + 'bumping version in ' + updater.filename + ' from %s to %s', + [updater.updater.readVersion(contents), newVersion], + ); + writeFile(args, configPath, + updater.updater.writeVersion(contents, newVersion), + ); + ret.push(updater.filename); + } + + return ret; +} diff --git a/tools/cdk-release/lib/lifecycles/changelog.ts b/tools/cdk-release/lib/lifecycles/changelog.ts new file mode 100644 index 0000000000000..f961fe53b175c --- /dev/null +++ b/tools/cdk-release/lib/lifecycles/changelog.ts @@ -0,0 +1,123 @@ +import * as stream from 'stream'; +import * as fs from 'fs-extra'; +import { ConventionalCommit } from '../conventional-commits'; +import { writeFile } from '../private/files'; +import { notify, debug, debugObject } from '../private/print'; +import { ReleaseOptions } from '../types'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const conventionalChangelogPresetLoader = require('conventional-changelog-preset-loader'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const conventionalChangelogWriter = require('conventional-changelog-writer'); + +const START_OF_LAST_RELEASE_PATTERN = /(^#+ \[?[0-9]+\.[0-9]+\.[0-9]+| { + if (args.skip?.changelog) { + return { + contents: '', + changedFiles: [], + }; + } + createChangelogIfMissing(args); + + // find the position of the last release and remove header + let oldContent = args.dryRun ? '' : fs.readFileSync(args.infile!, 'utf-8'); + const oldContentStart = oldContent.search(START_OF_LAST_RELEASE_PATTERN); + if (oldContentStart !== -1) { + oldContent = oldContent.substring(oldContentStart); + } + + // load the default configuration that we use for the Changelog generation + const presetConfig = await conventionalChangelogPresetLoader({ + name: 'conventional-changelog-conventionalcommits', + }); + debugObject(args, 'conventionalChangelogPresetLoader returned', presetConfig); + + return new Promise((resolve, reject) => { + // convert an array of commits into a Stream, + // which conventionalChangelogWriter expects + const commitsStream = new stream.Stream.Readable({ + objectMode: true, + }); + commits.forEach(commit => commitsStream.push(commit)); + // mark the end of the stream + commitsStream.push(null); + + const host = 'https://github.com', owner = 'aws', repository = 'aws-cdk'; + const context = { + issue: 'issues', + commit: 'commit', + version: newVersion, + host, + owner, + repository, + repoUrl: `${host}/${owner}/${repository}`, + linkCompare: true, + previousTag: `v${currentVersion}`, + currentTag: `v${newVersion}`, + // when isPatch is 'true', the default template used for the header renders an H3 instead of an H2 + // (see: https://github.com/conventional-changelog/conventional-changelog/blob/f1f50f56626099e92efe31d2f8c5477abd90f1b7/packages/conventional-changelog-conventionalcommits/templates/header.hbs#L1-L5) + isPatch: false, + }; + // invoke the conventionalChangelogWriter package that will perform the actual Changelog rendering + const changelogStream = commitsStream + .pipe(conventionalChangelogWriter(context, + { + // CDK uses the settings from 'conventional-changelog-conventionalcommits' + // (by way of 'standard-version'), + // which are different than the 'conventionalChangelogWriter' defaults + ...presetConfig.writerOpts, + finalizeContext: (ctx: { noteGroups?: { title: string }[], date?: string }) => { + // the heading of the "BREAKING CHANGES" section is governed by this Handlebars template: + // https://github.com/conventional-changelog/conventional-changelog/blob/f1f50f56626099e92efe31d2f8c5477abd90f1b7/packages/conventional-changelog-conventionalcommits/templates/template.hbs#L3-L12 + // to change the heading from 'BREAKING CHANGES' to 'BREAKING CHANGES TO EXPERIMENTAL FEATURES', + // we have to change the title of the 'BREAKING CHANGES' noteGroup + ctx.noteGroups?.forEach(noteGroup => { + if (noteGroup.title === 'BREAKING CHANGES') { + noteGroup.title = 'BREAKING CHANGES TO EXPERIMENTAL FEATURES'; + } + }); + // in unit tests, we don't want to have the date in the Changelog + if (args.includeDateInChangelog === false) { + ctx.date = undefined; + } + return ctx; + }, + })); + + changelogStream.on('error', function (err: any) { + reject(err); + }); + let content = ''; + changelogStream.on('data', function (buffer: any) { + content += buffer.toString(); + }); + changelogStream.on('end', function () { + notify(args, 'outputting changes to %s', [args.infile]); + if (args.dryRun) { + debug(args, `\n---\n${content.trim()}\n---\n`); + } else { + writeFile(args, args.infile!, args.changeLogHeader + '\n' + (content + oldContent).replace(/\n+$/, '\n')); + } + return resolve({ + contents: content, + changedFiles: [args.infile!], + }); + }); + }); +} + +function createChangelogIfMissing(args: ReleaseOptions) { + if (!fs.existsSync(args.infile!)) { + notify(args, 'created %s', [args.infile]); + // args.outputUnreleased = true + writeFile(args, args.infile!, '\n'); + } +} diff --git a/tools/cdk-release/lib/lifecycles/commit.ts b/tools/cdk-release/lib/lifecycles/commit.ts new file mode 100644 index 0000000000000..d908e0799954a --- /dev/null +++ b/tools/cdk-release/lib/lifecycles/commit.ts @@ -0,0 +1,45 @@ +import * as path from 'path'; +import { notify } from '../private/print'; +import { runExecFile } from '../private/run-exec-file'; +import { ReleaseOptions } from '../types'; + +export async function commit(args: ReleaseOptions, newVersion: string, modifiedFiles: string[]): Promise { + if (args.skip?.commit) { + return; + } + + let msg = 'committing %s'; + const paths = new Array(); + const toAdd = new Array(); + // commit any of the config files that we've updated + // the version # for. + for (const modifiedFile of modifiedFiles) { + paths.unshift(modifiedFile); + toAdd.push(path.relative(process.cwd(), modifiedFile)); + + // account for multiple files in the output message + if (paths.length > 1) { + msg += ' and %s'; + } + } + // nothing to do, exit without commit anything + if (toAdd.length === 0) { + return; + } + + notify(args, msg, paths); + + await runExecFile(args, 'git', ['add'].concat(toAdd)); + const sign = args.sign ? ['-S'] : []; + await runExecFile(args, 'git', ['commit'].concat( + sign, + [ + '-m', + `${formatCommitMessage(args.releaseCommitMessageFormat!, newVersion)}`, + ]), + ); +} + +function formatCommitMessage(rawMsg: string, newVersion: string): string { + return rawMsg.replace(/{{currentTag}}/g, newVersion); +} diff --git a/tools/cdk-release/lib/private/files.ts b/tools/cdk-release/lib/private/files.ts new file mode 100644 index 0000000000000..1850e8a79ad4a --- /dev/null +++ b/tools/cdk-release/lib/private/files.ts @@ -0,0 +1,9 @@ +import * as fs from 'fs'; +import { ReleaseOptions } from '../types'; + +export function writeFile(args: ReleaseOptions, filePath: string, content: string): void { + if (args.dryRun) { + return; + } + fs.writeFileSync(filePath, content, 'utf8'); +} diff --git a/tools/cdk-release/lib/private/print.ts b/tools/cdk-release/lib/private/print.ts new file mode 100644 index 0000000000000..84b7758a5c4b3 --- /dev/null +++ b/tools/cdk-release/lib/private/print.ts @@ -0,0 +1,23 @@ +import * as util from 'util'; +import { ReleaseOptions } from '../types'; + +export function debug(opts: ReleaseOptions, message: string): void { + if (opts.verbose) { + // eslint-disable-next-line no-console + console.log(`[cdk-release] ${message}`); + } +} + +export function debugObject(opts: ReleaseOptions, message: string, object: any): void { + if (opts.verbose) { + // eslint-disable-next-line no-console + console.log(`[cdk-release] ${message}:\n`, object); + } +} + +export function notify(opts: ReleaseOptions, msg: string, args: any[]) { + if (!opts.silent) { + // eslint-disable-next-line no-console + console.info('✔ ' + util.format(msg, ...args)); + } +} diff --git a/tools/cdk-release/lib/private/run-exec-file.ts b/tools/cdk-release/lib/private/run-exec-file.ts new file mode 100644 index 0000000000000..c19f312a1b92e --- /dev/null +++ b/tools/cdk-release/lib/private/run-exec-file.ts @@ -0,0 +1,19 @@ +import { execFile as childProcessExecFile } from 'child_process'; +import { promisify } from 'util'; +import { ReleaseOptions } from '../types'; +import { notify } from './print'; + +const execFile = promisify(childProcessExecFile); + +export async function runExecFile(args: ReleaseOptions, cmd: string, cmdArgs: string[]): Promise { + if (args.dryRun) { + notify(args, "would execute command: '%s %s'", [cmd, cmdArgs + // quote arguments with spaces, for a more realistic printing experience + .map(cmdArg => cmdArg.match(/\s/) ? `"${cmdArg}"` : cmdArg) + .join(' ')], + ); + return; + } + const streams = await execFile(cmd, cmdArgs); + return streams.stdout; +} diff --git a/tools/cdk-release/lib/types.ts b/tools/cdk-release/lib/types.ts new file mode 100644 index 0000000000000..f6314261cd5f1 --- /dev/null +++ b/tools/cdk-release/lib/types.ts @@ -0,0 +1,64 @@ +export interface Lifecycles { + bump?: string; + changelog?: string; + postchangelog?: string; + commit?: string; + + // we don't actually do tagging at all, but still support passing it as an option, + // for conformance with standard-version (CDK doesn't use its tagging capabilities anyway) + tag?: string; +} + +type LifecyclesSkip = { + [key in keyof Lifecycles]: boolean; +} + +/* ****** Updaters ******** */ + +export interface UpdaterModule { + isPrivate?: (contents: string) => string | boolean | null | undefined; + readVersion(contents: string): string; + writeVersion(contents: string, version: string): string; +} + +export interface ArgUpdater { + filename: string; + type?: string; + updater?: UpdaterModule | string; +} + +export type ArgFile = string | ArgUpdater; + +export interface Updater { + filename: string; + updater: UpdaterModule; +} + +export type ReleaseType = 'major' | 'minor' | 'patch'; + +export interface ConventionalCommitType { + type: string; + section?: string; + hidden?: boolean; +} + +/* ****** main options ******** */ + +export interface ReleaseOptions { + releaseAs: ReleaseType; + skip?: LifecyclesSkip; + packageFiles?: ArgFile[]; + bumpFiles?: ArgFile[]; + infile?: string; + prerelease?: string; + scripts?: Lifecycles; + dryRun?: boolean; + verbose?: boolean; + silent?: boolean; + sign?: boolean; + stripExperimentalChanges?: boolean; + + changeLogHeader?: string; + includeDateInChangelog?: boolean; + releaseCommitMessageFormat?: string; +} diff --git a/tools/cdk-release/lib/updaters/index.ts b/tools/cdk-release/lib/updaters/index.ts new file mode 100644 index 0000000000000..fd61828925316 --- /dev/null +++ b/tools/cdk-release/lib/updaters/index.ts @@ -0,0 +1,59 @@ +import * as path from 'path'; +import { defaultBumpFiles } from '../defaults'; +import { UpdaterModule, ArgFile, Updater } from '../types'; +import jsonUpdaterModule from './types/json'; +import plainTextUpdaterModule from './types/plain-text'; + +export function resolveUpdaterObjectFromArgument(arg: ArgFile): Updater { + arg = typeof arg === 'string' ? { filename: arg } : arg; + let updaterModule: UpdaterModule; + + if (arg.updater) { + updaterModule = getCustomUpdater(arg.updater); + } else if (arg.type) { + updaterModule = getUpdaterByType(arg.type); + } else { + updaterModule = getUpdaterByFilename(arg.filename); + } + + return { + updater: updaterModule, + filename: arg.filename, + }; +} + +function getCustomUpdater(updater: string | UpdaterModule): UpdaterModule { + if (typeof updater === 'string') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require(path.resolve(process.cwd(), updater)); + } + if ( + typeof updater.readVersion === 'function' && + typeof updater.writeVersion === 'function' + ) { + return updater; + } + throw new Error('Updater must be a string path or an object with readVersion and writeVersion methods'); +} + +const JSON_BUMP_FILES = defaultBumpFiles; +function getUpdaterByFilename(filename: any): UpdaterModule { + if (JSON_BUMP_FILES.includes(path.basename(filename))) { + return getUpdaterByType('json'); + } + throw Error( + `Unsupported file (${filename}) provided for bumping.\n Please specify the updater \`type\` or use a custom \`updater\`.`, + ); +} + +const updatersByType: { [key: string]: UpdaterModule } = { + 'json': jsonUpdaterModule, + 'plain-text': plainTextUpdaterModule, +}; +function getUpdaterByType(type: string): UpdaterModule { + const updater = updatersByType[type]; + if (!updater) { + throw Error(`Unable to locate updater for provided type (${type}).`); + } + return updater; +} diff --git a/tools/cdk-release/lib/updaters/types/json.ts b/tools/cdk-release/lib/updaters/types/json.ts new file mode 100644 index 0000000000000..23e8490f56f5c --- /dev/null +++ b/tools/cdk-release/lib/updaters/types/json.ts @@ -0,0 +1,28 @@ +import { UpdaterModule } from '../../types'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const detectIndent = require('detect-indent'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const detectNewline = require('detect-newline'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const stringifyPackage = require('stringify-package'); + +class JsonUpdaterModule implements UpdaterModule { + public readVersion(contents: string): string { + return JSON.parse(contents).version; + }; + + public writeVersion(contents: string, version: string): string { + const json = JSON.parse(contents); + const indent = detectIndent(contents).indent; + const newline = detectNewline(contents); + json.version = version; + return stringifyPackage(json, indent, newline); + }; + + public isPrivate(contents: string): string | boolean | null | undefined { + return JSON.parse(contents).private; + }; +} +const jsonUpdaterModule = new JsonUpdaterModule(); +export default jsonUpdaterModule; diff --git a/tools/cdk-release/lib/updaters/types/plain-text.ts b/tools/cdk-release/lib/updaters/types/plain-text.ts new file mode 100644 index 0000000000000..84ed4de20d563 --- /dev/null +++ b/tools/cdk-release/lib/updaters/types/plain-text.ts @@ -0,0 +1,12 @@ +import { UpdaterModule } from '../../types'; + +const plainTextUpdaterModule: UpdaterModule = { + readVersion(contents: string): string { + return contents; + }, + + writeVersion(_contents: string, version: string): string { + return version; + }, +}; +export default plainTextUpdaterModule; diff --git a/tools/cdk-release/package.json b/tools/cdk-release/package.json new file mode 100644 index 0000000000000..3608c4f212378 --- /dev/null +++ b/tools/cdk-release/package.json @@ -0,0 +1,70 @@ +{ + "name": "cdk-release", + "private": true, + "version": "0.0.0", + "description": "A tool for performing release-related tasks like version bumps, Changelog generation, etc.", + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "tools/cdk-release" + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "pkglint": "pkglint -f", + "build+test+package": "yarn build+test", + "build+test": "yarn build && yarn test", + "build+extract": "yarn build", + "build+test+extract": "yarn build+test" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/fs-extra": "^8.1.1", + "@types/jest": "^26.0.23", + "@types/yargs": "^15.0.13", + "cdk-build-tools": "0.0.0", + "jest": "^26.6.3", + "pkglint": "0.0.0" + }, + "dependencies": { + "@lerna/project": "^4.0.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.1", + "conventional-changelog-writer": "^4.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" + }, + "keywords": [ + "aws", + "cdk", + "changelog", + "bump", + "release", + "version" + ], + "homepage": "https://github.com/aws/aws-cdk", + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "cdk-build": { + "jest": true + }, + "ubergen": { + "exclude": true + } +} diff --git a/tools/cdk-release/test/changelog.test.ts b/tools/cdk-release/test/changelog.test.ts new file mode 100644 index 0000000000000..21fba9b6f2501 --- /dev/null +++ b/tools/cdk-release/test/changelog.test.ts @@ -0,0 +1,110 @@ +import { ConventionalCommit, filterCommits } from '../lib/conventional-commits'; +import { changelog } from '../lib/lifecycles/changelog'; +import { ReleaseOptions } from '../lib/types'; + +describe('Changelog generation', () => { + const args: ReleaseOptions = { + releaseAs: 'minor', + dryRun: true, + silent: true, + includeDateInChangelog: false, + }; + + test("correctly handles 'BREAKING CHANGES'", async () => { + const commits: ConventionalCommit[] = [ + buildCommit({ + type: 'feat', + subject: 'super important feature', + notes: [ + { + title: 'BREAKING CHANGE', + text: 'this is a breaking change', + }, + ], + }), + buildCommit({ + type: 'fix', + scope: 'scope', + subject: 'hairy bugfix', + }), + buildCommit({ + type: 'chore', + subject: 'this commit should not be rendered in the Changelog', + }), + ]; + + const changelogContents = await invokeChangelogFrom1_23_0to1_24_0(args, commits); + + expect(changelogContents).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* this is a breaking change + +### Features + +* super important feature + + +### Bug Fixes + +* **scope:** hairy bugfix + +`); + }); + + test("correctly skips experimental modules, even with 'BREAKING CHANGES'", async () => { + const commits: ConventionalCommit[] = [ + buildCommit({ + type: 'feat', + scope: 'scope', + subject: 'super important feature', + }), + buildCommit({ + type: 'fix', + scope: 'example-construct-library', // really hope we don't stabilize this one + subject: 'hairy bugfix', + notes: [ + { + title: 'BREAKING CHANGE', + text: 'this is a breaking change', + }, + ], + }), + ]; + + const changelogContents = await invokeChangelogFrom1_23_0to1_24_0({ + ...args, + stripExperimentalChanges: true, + }, commits); + + expect(changelogContents).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### Features + +* **scope:** super important feature + +`); + }); +}); + +interface PartialCommit extends Partial { + readonly type: string; + readonly subject: string; +} + +function buildCommit(commit: PartialCommit): ConventionalCommit { + return { + notes: [], + references: [], + header: `${commit.type}${commit.scope ? '(' + commit.scope + ')' : ''}: ${commit.subject}`, + ...commit, + }; +} + +async function invokeChangelogFrom1_23_0to1_24_0(args: ReleaseOptions, commits: ConventionalCommit[]): Promise { + const changelogResult = await changelog(args, '1.23.0', '1.24.0', filterCommits(args, commits)); + return changelogResult.contents; +} diff --git a/tools/cdk-release/tsconfig.json b/tools/cdk-release/tsconfig.json new file mode 100644 index 0000000000000..89d1f04da5020 --- /dev/null +++ b/tools/cdk-release/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": ["es2018"], + "strict": true, + "alwaysStrict": true, + "declaration": true, + "inlineSourceMap": true, + "inlineSources": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "composite": true, + "incremental": true + }, + "exclude": ["test/enrichments/**"], + "include": ["**/*.ts"] +} From 865b2660a01fa819afd75cc10aab344b8535896c Mon Sep 17 00:00:00 2001 From: Akash Askoolum Date: Tue, 6 Jul 2021 22:27:02 +0100 Subject: [PATCH 014/105] chore: Fix typo (#15436) Correct a small typo: `initTemplateLanuages` -> `initTemplateLanguages`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/bin/cdk.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index f38f5252c304d..f072a557c4783 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -38,7 +38,7 @@ async function parseCommandLineArguments() { // // ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. - const initTemplateLanuages = await availableInitLanguages(); + const initTemplateLanguages = await availableInitLanguages(); return yargs .env('CDK') .usage('Usage: cdk -a COMMAND') @@ -118,7 +118,7 @@ async function parseCommandLineArguments() { .option('fail', { type: 'boolean', desc: 'Fail with exit code 1 in case of diff', default: false }) .command('metadata [STACK]', 'Returns all metadata associated with this stack') .command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', yargs => yargs - .option('language', { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanuages }) + .option('language', { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanguages }) .option('list', { type: 'boolean', desc: 'List the available templates' }) .option('generate-only', { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }), ) From 393be523b5174359ca4ba9cde16d2e4f93bcd387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20M=C3=BCller?= <31097173+ManuelMueller1st@users.noreply.github.com> Date: Wed, 7 Jul 2021 00:05:16 +0200 Subject: [PATCH 015/105] docs: fixed example (#15434) Property 'addToPrincipalPolicy' does not exist on type 'SlackChannelConfiguration'. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-chatbot/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-chatbot/README.md b/packages/@aws-cdk/aws-chatbot/README.md index 857e2524ed784..d8242bf30bea2 100644 --- a/packages/@aws-cdk/aws-chatbot/README.md +++ b/packages/@aws-cdk/aws-chatbot/README.md @@ -24,7 +24,7 @@ const slackChannel = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel slackChannelId: 'YOUR_SLACK_CHANNEL_ID', }); -slackChannel.addToPrincipalPolicy(new iam.PolicyStatement({ +slackChannel.addToRolePolicy(new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 's3:GetObject', From c37c7f9fa927b922f0a6cfb4de9f5162ca7184d4 Mon Sep 17 00:00:00 2001 From: Ben Munro Date: Wed, 7 Jul 2021 00:43:14 +0200 Subject: [PATCH 016/105] docs(api-gateway): Fix IAM policy Effect enum case (#15417) The Effect enum values were changed to be upper case in #2918 (b735d1c) but this usage in the docs was missed ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-apigateway/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 4607442308138..9e33ef2125810 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -509,7 +509,7 @@ iamUser.attachInlinePolicy(new iam.Policy(this, 'AllowBooks', { statements: [ new iam.PolicyStatement({ actions: [ 'execute-api:Invoke' ], - effect: iam.Effect.Allow, + effect: iam.Effect.ALLOW, resources: [ getBooks.methodArn ] }) ] From ef1260976f1e231fd4c8f7fbac5b0a592e243432 Mon Sep 17 00:00:00 2001 From: antonioortegajr Date: Tue, 6 Jul 2021 17:22:07 -0700 Subject: [PATCH 017/105] docs(aws-ecr-assets): Update link for cdk-ecr-deployment repo (#15407) Link for cdk-ecr-deployment was https://github.com/wchaws/cdk-ecr-deployment, but is now https://github.com/cdklabs/cdk-ecr-deployment. Just updated this links. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecr-assets/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr-assets/README.md b/packages/@aws-cdk/aws-ecr-assets/README.md index a2d7ff50d8773..188d0a4ceb96e 100644 --- a/packages/@aws-cdk/aws-ecr-assets/README.md +++ b/packages/@aws-cdk/aws-ecr-assets/README.md @@ -77,10 +77,10 @@ The mechanics of where these images are published and how are intentionally kept does not support customizations such as specifying the ECR repository name or tags. If you are looking for a way to _publish_ image assets to an ECR repository in your control, you should consider using -[wchaws/cdk-ecr-deployment], which is able to replicate an image asset from the CDK-controlled ECR repository to a repository of +[cdklabs/cdk-ecr-deployment], which is able to replicate an image asset from the CDK-controlled ECR repository to a repository of your choice. -Here an example from the [wchaws/cdk-ecr-deployment] project: +Here an example from the [cdklabs/cdk-ecr-deployment] project: ```ts import * as ecrdeploy from 'cdk-ecr-deployment'; @@ -99,7 +99,7 @@ new ecrdeploy.ECRDeployment(this, 'DeployDockerImage', { You are welcome to +1 [this GitHub issue](https://github.com/aws/aws-cdk/issues/12597) if you would like to see native support for this use-case in the AWS CDK. -[wchaws/cdk-ecr-deployment]: https://github.com/wchaws/cdk-ecr-deployment +[cdklabs/cdk-ecr-deployment]: https://github.com/cdklabs/cdk-ecr-deployment ## Pull Permissions From 3a65b9c6117e9b3c038720262ac0eb6c8033af8f Mon Sep 17 00:00:00 2001 From: Thorsten Hoeger Date: Wed, 7 Jul 2021 11:33:34 +0200 Subject: [PATCH 018/105] fix(codebuild): merge spec correctly when using strings (#15429) The CodeBuild runtime allows fields that are defined as string[] to be strings and interprets them as singleton lists. When merging we need to normalize this to have the correct concat semantics ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-codebuild/lib/build-spec.ts | 55 +++++++++++++++++++ .../aws-codebuild/test/test.build-spec.ts | 8 +-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-codebuild/lib/build-spec.ts b/packages/@aws-cdk/aws-codebuild/lib/build-spec.ts index 9ba827a1e17eb..3d7c248c622ed 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/build-spec.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/build-spec.ts @@ -124,6 +124,9 @@ export function mergeBuildSpecs(lhs: BuildSpec, rhs: BuildSpec): BuildSpec { const lhsSpec = JSON.parse(JSON.stringify(lhs.spec)); const rhsSpec = JSON.parse(JSON.stringify(rhs.spec)); + normalizeSpec(lhsSpec); + normalizeSpec(rhsSpec); + const merged = mergeDeep(lhsSpec, rhsSpec); // In case of test reports we replace the whole object with the RHS (instead of recursively merging) @@ -134,6 +137,58 @@ export function mergeBuildSpecs(lhs: BuildSpec, rhs: BuildSpec): BuildSpec { return new ObjectBuildSpec(merged); } +/* + * Normalizes the build spec + * The CodeBuild runtime allows fields that are defined as string[] to be strings + * and interprets them as singleton lists. + * When merging we need to normalize this to have the correct concat semantics + */ +function normalizeSpec(spec: { [key: string]: any }): void { + if (spec.env && typeof spec.env['exported-variables'] === 'string') { + spec.env['exported-variables'] = [spec.env['exported-variables']]; + } + for (const key in spec.phases) { + if (Object.prototype.hasOwnProperty.call(spec.phases, key)) { + normalizeSpecPhase(spec.phases[key]); + } + } + if (spec.reports) { + for (const key in spec.reports) { + if (Object.prototype.hasOwnProperty.call(spec.reports, key)) { + const report = spec.reports[key]; + if (typeof report.files === 'string') { + report.files = [report.files]; + } + } + } + } + if (spec.artifacts) { + if (typeof spec.artifacts.files === 'string') { + spec.artifacts.files = [spec.artifacts.files]; + } + for (const key in spec.artifacts['secondary-artifacts']) { + if (Object.prototype.hasOwnProperty.call(spec.artifacts['secondary-artifacts'], key)) { + const secArtifact = spec.artifacts['secondary-artifacts'][key]; + if (typeof secArtifact.files === 'string') { + secArtifact.files = [secArtifact.files]; + } + } + } + } + if (spec.cache && typeof spec.cache.paths === 'string') { + spec.cache.paths = [spec.cache.paths]; + } +} + +function normalizeSpecPhase(phase: { [key: string]: any }): void { + if (phase.commands && typeof phase.commands === 'string') { + phase.commands = [phase.commands]; + } + if (phase.finally && typeof phase.finally === 'string') { + phase.finally = [phase.finally]; + } +} + function mergeDeep(lhs: any, rhs: any): any { if (Array.isArray(lhs) && Array.isArray(rhs)) { return [...lhs, ...rhs]; diff --git a/packages/@aws-cdk/aws-codebuild/test/test.build-spec.ts b/packages/@aws-cdk/aws-codebuild/test/test.build-spec.ts index acc9dd72b49cb..ad625d39c2087 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.build-spec.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.build-spec.ts @@ -20,9 +20,7 @@ export = { const rhs = codebuild.BuildSpec.fromObject({ phases: { build: { - commands: [ - 'build', - ], + commands: 'build', }, }, }); @@ -60,9 +58,7 @@ export = { const rhs = codebuild.BuildSpec.fromObject({ phases: { build: { - commands: [ - 'build2', - ], + commands: 'build2', }, }, }); From 71c0a4c413e77452f47c797d4e861aa542174ce9 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:22:55 +0300 Subject: [PATCH 019/105] feat(cfnspec): cloudformation spec v39.3.0 (#15446) Co-authored-by: AWS CDK Team Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- packages/@aws-cdk/cfnspec/CHANGELOG.md | 41 ++++ ...0_CloudFormationResourceSpecification.json | 199 +++++++++++------- .../rosetta/basic-constructs.ts-fixture | 22 ++ 3 files changed, 190 insertions(+), 72 deletions(-) create mode 100644 packages/monocdk/rosetta/basic-constructs.ts-fixture diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index d9a14cacbe851..ea32ea3126781 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,44 @@ +# CloudFormation Resource Specification v39.3.0 + +## New Resource Types + + +## Attribute Changes + +* AWS::ApiGateway::UsagePlanKey Id (__added__) +* AWS::Location::Map Arn (__added__) +* AWS::Location::PlaceIndex Arn (__added__) +* AWS::MediaPackage::Asset EgressEndpoints (__added__) + +## Property Changes + +* AWS::ApiGateway::RestApi Mode (__added__) +* AWS::AppSync::GraphQLApi LambdaAuthorizerConfig (__deleted__) +* AWS::AutoScaling::AutoScalingGroup LoadBalancerNames.DuplicatesAllowed (__changed__) + * Old: true + * New: false +* AWS::EC2::EC2Fleet Context (__added__) +* AWS::IoTAnalytics::Datastore DatastorePartitions (__added__) +* AWS::IoTSiteWise::Portal Alarms (__added__) +* AWS::IoTSiteWise::Portal NotificationSenderEmail (__added__) +* AWS::Lambda::EventSourceMapping StartingPositionTimestamp (__added__) +* AWS::MediaPackage::Asset EgressEndpoints (__deleted__) +* AWS::RDS::DBCluster CopyTagsToSnapshot (__added__) + +## Property Type Changes + +* AWS::AppSync::GraphQLApi.LambdaAuthorizerConfig (__removed__) +* AWS::FSx::FileSystem.AuditLogConfiguration (__removed__) +* AWS::IoTAnalytics::Datastore.DatastorePartition (__added__) +* AWS::IoTAnalytics::Datastore.DatastorePartitions (__added__) +* AWS::IoTAnalytics::Datastore.Partition (__added__) +* AWS::IoTAnalytics::Datastore.TimestampPartition (__added__) +* AWS::AppSync::GraphQLApi.AdditionalAuthenticationProvider LambdaAuthorizerConfig (__deleted__) +* AWS::ApplicationAutoScaling::ScalableTarget.ScheduledAction Timezone (__added__) +* AWS::EC2::SpotFleet.SpotFleetRequestConfigData Context (__added__) +* AWS::FSx::FileSystem.WindowsConfiguration AuditLogConfiguration (__deleted__) + + # Serverless Application Model (SAM) Resource Specification v2016-10-31 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 0f9c5d607d474..2ef6d5f76a30e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -7120,12 +7120,6 @@ "Required": true, "UpdateType": "Mutable" }, - "LambdaAuthorizerConfig": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-additionalauthenticationprovider.html#cfn-appsync-graphqlapi-additionalauthenticationprovider-lambdaauthorizerconfig", - "Required": false, - "Type": "LambdaAuthorizerConfig", - "UpdateType": "Mutable" - }, "OpenIDConnectConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-additionalauthenticationprovider.html#cfn-appsync-graphqlapi-additionalauthenticationprovider-openidconnectconfig", "Required": false, @@ -7170,29 +7164,6 @@ } } }, - "AWS::AppSync::GraphQLApi.LambdaAuthorizerConfig": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html", - "Properties": { - "AuthorizerResultTtlInSeconds": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html#cfn-appsync-graphqlapi-lambdaauthorizerconfig-authorizerresultttlinseconds", - "PrimitiveType": "Double", - "Required": false, - "UpdateType": "Mutable" - }, - "AuthorizerUri": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html#cfn-appsync-graphqlapi-lambdaauthorizerconfig-authorizeruri", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "IdentityValidationExpression": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html#cfn-appsync-graphqlapi-lambdaauthorizerconfig-identityvalidationexpression", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::AppSync::GraphQLApi.LogConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-logconfig.html", "Properties": { @@ -7394,6 +7365,12 @@ "PrimitiveType": "Timestamp", "Required": false, "UpdateType": "Mutable" + }, + "Timezone": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-applicationautoscaling-scalabletarget-scheduledaction.html#cfn-applicationautoscaling-scalabletarget-scheduledaction-timezone", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -21107,6 +21084,12 @@ "Required": false, "UpdateType": "Immutable" }, + "Context": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-spotfleet-spotfleetrequestconfigdata.html#cfn-ec2-spotfleet-spotfleetrequestconfigdata-context", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "ExcessCapacityTerminationPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-spotfleet-spotfleetrequestconfigdata.html#cfn-ec2-spotfleet-spotfleetrequestconfigdata-excesscapacityterminationpolicy", "PrimitiveType": "String", @@ -26954,29 +26937,6 @@ } } }, - "AWS::FSx::FileSystem.AuditLogConfiguration": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration-auditlogconfiguration.html", - "Properties": { - "AuditLogDestination": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration-auditlogconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-auditlogconfiguration-auditlogdestination", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "FileAccessAuditLogLevel": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration-auditlogconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-auditlogconfiguration-fileaccessauditloglevel", - "PrimitiveType": "String", - "Required": true, - "UpdateType": "Mutable" - }, - "FileShareAccessAuditLogLevel": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration-auditlogconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-auditlogconfiguration-fileshareaccessauditloglevel", - "PrimitiveType": "String", - "Required": true, - "UpdateType": "Mutable" - } - } - }, "AWS::FSx::FileSystem.LustreConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-lustreconfiguration.html", "Properties": { @@ -27112,12 +27072,6 @@ "Type": "List", "UpdateType": "Mutable" }, - "AuditLogConfiguration": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-auditlogconfiguration", - "Required": false, - "Type": "AuditLogConfiguration", - "UpdateType": "Mutable" - }, "AutomaticBackupRetentionDays": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-automaticbackupretentiondays", "PrimitiveType": "Integer", @@ -33920,6 +33874,35 @@ } } }, + "AWS::IoTAnalytics::Datastore.DatastorePartition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-datastorepartition.html", + "Properties": { + "Partition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-datastorepartition.html#cfn-iotanalytics-datastore-datastorepartition-partition", + "Required": false, + "Type": "Partition", + "UpdateType": "Mutable" + }, + "TimestampPartition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-datastorepartition.html#cfn-iotanalytics-datastore-datastorepartition-timestamppartition", + "Required": false, + "Type": "TimestampPartition", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoTAnalytics::Datastore.DatastorePartitions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-datastorepartitions.html", + "Properties": { + "Partitions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-datastorepartitions.html#cfn-iotanalytics-datastore-datastorepartitions-partitions", + "ItemType": "DatastorePartition", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::IoTAnalytics::Datastore.DatastoreStorage": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-datastorestorage.html", "Properties": { @@ -33969,6 +33952,17 @@ } } }, + "AWS::IoTAnalytics::Datastore.Partition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-partition.html", + "Properties": { + "AttributeName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-partition.html#cfn-iotanalytics-datastore-partition-attributename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::IoTAnalytics::Datastore.RetentionPeriod": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-retentionperiod.html", "Properties": { @@ -34002,6 +33996,23 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-servicemanageds3.html", "Properties": {} }, + "AWS::IoTAnalytics::Datastore.TimestampPartition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-timestamppartition.html", + "Properties": { + "AttributeName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-timestamppartition.html#cfn-iotanalytics-datastore-timestamppartition-attributename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "TimestampFormat": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-datastore-timestamppartition.html#cfn-iotanalytics-datastore-timestamppartition-timestampformat", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::IoTAnalytics::Pipeline.Activity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-activity.html", "Properties": { @@ -61964,6 +61975,12 @@ "Required": false, "UpdateType": "Mutable" }, + "Mode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-restapi.html#cfn-apigateway-restapi-mode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-restapi.html#cfn-apigateway-restapi-name", "PrimitiveType": "String", @@ -62135,6 +62152,11 @@ } }, "AWS::ApiGateway::UsagePlanKey": { + "Attributes": { + "Id": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-usageplankey.html", "Properties": { "KeyId": { @@ -64413,12 +64435,6 @@ "Required": true, "UpdateType": "Mutable" }, - "LambdaAuthorizerConfig": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-graphqlapi.html#cfn-appsync-graphqlapi-lambdaauthorizerconfig", - "Required": false, - "Type": "LambdaAuthorizerConfig", - "UpdateType": "Mutable" - }, "LogConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-graphqlapi.html#cfn-appsync-graphqlapi-logconfig", "Required": false, @@ -65051,7 +65067,7 @@ }, "LoadBalancerNames": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#cfn-as-group-loadbalancernames", - "DuplicatesAllowed": true, + "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -72171,6 +72187,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ec2fleet.html", "Properties": { + "Context": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ec2fleet.html#cfn-ec2-ec2fleet-context", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "ExcessCapacityTerminationPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-ec2fleet.html#cfn-ec2-ec2fleet-excesscapacityterminationpolicy", "PrimitiveType": "String", @@ -83096,6 +83118,12 @@ "Required": false, "UpdateType": "Immutable" }, + "DatastorePartitions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-datastore.html#cfn-iotanalytics-datastore-datastorepartitions", + "Required": false, + "Type": "DatastorePartitions", + "UpdateType": "Mutable" + }, "DatastoreStorage": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-datastore.html#cfn-iotanalytics-datastore-datastorestorage", "Required": false, @@ -83546,6 +83574,18 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-portal.html", "Properties": { + "Alarms": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-portal.html#cfn-iotsitewise-portal-alarms", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, + "NotificationSenderEmail": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-portal.html#cfn-iotsitewise-portal-notificationsenderemail", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "PortalAuthMode": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-portal.html#cfn-iotsitewise-portal-portalauthmode", "PrimitiveType": "String", @@ -84965,6 +85005,12 @@ "Required": false, "UpdateType": "Immutable" }, + "StartingPositionTimestamp": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-startingpositiontimestamp", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, "Topics": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html#cfn-lambda-eventsourcemapping-topics", "DuplicatesAllowed": false, @@ -85440,6 +85486,9 @@ }, "AWS::Location::Map": { "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, "CreateTime": { "PrimitiveType": "String" }, @@ -85483,6 +85532,9 @@ }, "AWS::Location::PlaceIndex": { "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, "CreateTime": { "PrimitiveType": "String" }, @@ -86966,17 +87018,14 @@ }, "CreatedAt": { "PrimitiveType": "String" + }, + "EgressEndpoints": { + "ItemType": "EgressEndpoint", + "Type": "List" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-mediapackage-asset.html", "Properties": { - "EgressEndpoints": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-mediapackage-asset.html#cfn-mediapackage-asset-egressendpoints", - "ItemType": "EgressEndpoint", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, "Id": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-mediapackage-asset.html#cfn-mediapackage-asset-id", "PrimitiveType": "String", @@ -90705,6 +90754,12 @@ "Required": false, "UpdateType": "Mutable" }, + "CopyTagsToSnapshot": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html#cfn-rds-dbcluster-copytagstosnapshot", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "DBClusterIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html#cfn-rds-dbcluster-dbclusteridentifier", "PrimitiveType": "String", diff --git a/packages/monocdk/rosetta/basic-constructs.ts-fixture b/packages/monocdk/rosetta/basic-constructs.ts-fixture new file mode 100644 index 0000000000000..19ffd84abf486 --- /dev/null +++ b/packages/monocdk/rosetta/basic-constructs.ts-fixture @@ -0,0 +1,22 @@ +// Fixture with packages imported, but nothing else +import * as cdk from '@aws-cdk/core'; +import * as appreg from '@aws-cdk/aws-servicecatalogappregistry'; + +class Fixture extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const application = new appreg.Application(stack, 'MyApplication', { + applicationName: 'MyApplication', + }); + + const attributeGroup = new appreg.AttributeGroup(stack, 'MyAttributeGroup', { + attributeGroupName: 'testAttributeGroup', + attributes: { + key: 'value', + }, + }); + + /// here + } +} From a80ad425a925c7f1b2a550d5605df109ea5a504f Mon Sep 17 00:00:00 2001 From: ABevier Date: Wed, 7 Jul 2021 19:36:11 -0400 Subject: [PATCH 020/105] feat(aws-elasticloadbalancingv2): Allow listing added listeners on application load balancers (#15259) This PR adds the ability to get access to the list of Listeners that have been added to an Application Load Balancer. This is necessary because when an L3 pattern creates a listener on a load balancer the caller may need to get access to the listener to customize it. I couldn't figure out a great way to make this work for Looked Up or Imported Load Balancers so I decided to throw an error. If there is a better way, let me know and I can change the PR. closes #11841 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-elasticloadbalancingv2/README.md | 5 ++ .../lib/alb/application-load-balancer.ts | 22 ++++++++- .../test/alb/load-balancer.test.ts | 49 ++++++++++++++++++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index 76a6b7e1ddd5c..200322e52999e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -113,6 +113,11 @@ Routing traffic from a Load Balancer to a Target involves the following steps: - Create a Target Group, register the Target into the Target Group - Add an Action to the Listener which forwards traffic to the Target Group. +A new listener can be added to the Load Balancer by calling `addListener()`. +Listeners that have been added to the load balancer can be listed using the +`listeners` property. Note that the `listeners` property will throw an Error +for imported or looked up Load Balancers. + Various methods on the `Listener` take care of this work for you to a greater or lesser extent: diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index d686396d5e9e0..c3cac3ae37eee 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -80,6 +80,7 @@ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplic public readonly connections: ec2.Connections; public readonly ipAddressType?: IpAddressType; + public readonly listeners: ApplicationListener[]; constructor(scope: Construct, id: string, props: ApplicationLoadBalancerProps) { super(scope, id, props, { @@ -95,6 +96,7 @@ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplic allowAllOutbound: false, })]; this.connections = new ec2.Connections({ securityGroups }); + this.listeners = []; if (props.http2Enabled === false) { this.setAttribute('routing.http2.enabled', 'false'); } if (props.idleTimeout !== undefined) { this.setAttribute('idle_timeout.timeout_seconds', props.idleTimeout.toSeconds().toString()); } @@ -104,10 +106,12 @@ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplic * Add a new listener to this load balancer */ public addListener(id: string, props: BaseApplicationListenerProps): ApplicationListener { - return new ApplicationListener(this, id, { + const listener = new ApplicationListener(this, id, { loadBalancer: this, ...props, }); + this.listeners.push(listener); + return listener; } /** @@ -487,6 +491,12 @@ export interface IApplicationLoadBalancer extends ILoadBalancerV2, ec2.IConnecta */ readonly ipAddressType?: IpAddressType; + /** + * A list of listeners that have been added to the load balancer. + * This list is only valid for owned constructs. + */ + readonly listeners: ApplicationListener[]; + /** * Add a new listener to this load balancer */ @@ -554,6 +564,10 @@ class ImportedApplicationLoadBalancer extends Resource implements IApplicationLo */ public readonly loadBalancerArn: string; + public get listeners(): ApplicationListener[] { + throw Error('.listeners can only be accessed if the class was constructed as an owned, not imported, load balancer'); + } + /** * VPC of the load balancer * @@ -603,6 +617,10 @@ class LookedUpApplicationLoadBalancer extends Resource implements IApplicationLo public readonly connections: ec2.Connections; public readonly vpc?: ec2.IVpc; + public get listeners(): ApplicationListener[] { + throw Error('.listeners can only be accessed if the class was constructed as an owned, not looked up, load balancer'); + } + constructor(scope: Construct, id: string, props: cxapi.LoadBalancerContextResponse) { super(scope, id, { environmentFromArn: props.loadBalancerArn, @@ -685,4 +703,4 @@ export interface ApplicationLoadBalancerRedirectConfig { */ readonly open?: boolean; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index 8aa186ffc5edf..7cf10a01d7595 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -126,6 +126,27 @@ describe('tests', () => { }); }); + test('Can add and list listeners for an owned ApplicationLoadBalancer', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + + // WHEN + const loadBalancer = new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true, + }); + + const listener = loadBalancer.addListener('listener', { + protocol: elbv2.ApplicationProtocol.HTTP, + defaultAction: elbv2.ListenerAction.fixedResponse(200), + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + expect(loadBalancer.listeners).toContain(listener); + }); + testFutureBehavior('Access logging', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); @@ -323,6 +344,30 @@ describe('tests', () => { expect(() => listener.addTargets('Targets', { port: 8080 })).not.toThrow(); }); + test('imported load balancer with vpc can add but not list listeners', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const albArn = 'arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188'; + const sg = new ec2.SecurityGroup(stack, 'sg', { + vpc, + securityGroupName: 'mySg', + }); + const alb = elbv2.ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(stack, 'ALB', { + loadBalancerArn: albArn, + securityGroupId: sg.securityGroupId, + vpc, + }); + + // WHEN + const listener = alb.addListener('Listener', { port: 80 }); + listener.addTargets('Targets', { port: 8080 }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + expect(() => alb.listeners).toThrow(); + }); + test('imported load balancer knows its region', () => { const stack = new cdk.Stack(); @@ -385,7 +430,7 @@ describe('tests', () => { expect(loadBalancer.env.region).toEqual('us-west-2'); }); - test('Can add listeners to a looked-up ApplicationLoadBalancer', () => { + test('Can add but not list listeners for a looked-up ApplicationLoadBalancer', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'stack', { @@ -409,6 +454,8 @@ describe('tests', () => { // THEN expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + expect(() => loadBalancer.listeners).toThrow(); }); + }); }); From 48dd7718d94026e29c0fe6b15c162616840fcabe Mon Sep 17 00:00:00 2001 From: Sami Turcotte Date: Thu, 8 Jul 2021 05:17:19 -0400 Subject: [PATCH 021/105] fix(pipelines): singlePublisherPerType overwrites assets buildspec file of other pipelines (#15356) This PR fixes https://github.com/aws/aws-cdk/issues/15355 It works by rendering the file name based on the CDK path (rather than on the generic name) of the assets action. --- .../lib/actions/publish-assets-action.ts | 2 +- ...ne-with-assets-single-upload.expected.json | 2 +- .../pipelines/test/pipeline-assets.test.ts | 31 +++++++++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts index 83e7629b91b3a..a9661da20a9c2 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts @@ -175,7 +175,7 @@ export class PublishAssetsAction extends CoreConstruct implements codepipeline.I } private getBuildSpecFileName(): string { - return `buildspec-assets-${this.props.actionName}.yaml`; + return `buildspec-assets-${this.node.path.replace(new RegExp('/', 'g'), '-')}.yaml`; } private _onSynth(session: ISynthesisSession): void { diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.expected.json index 2160f260dcd9e..8250f113b53e3 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.expected.json @@ -1503,7 +1503,7 @@ ] }, "Source": { - "BuildSpec": "buildspec-assets-FileAsset.yaml", + "BuildSpec": "buildspec-assets-PipelineStack-Pipeline-Assets-FileAsset.yaml", "Type": "CODEPIPELINE" }, "EncryptionKey": { diff --git a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts index 23f8815a3dc47..55e45d1808476 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts @@ -506,12 +506,19 @@ describe('pipeline with VPC', () => { }); describe('pipeline with single asset publisher', () => { + let otherPipelineStack: Stack; + let otherPipeline: cdkp.CdkPipeline; + beforeEach(() => { app = new TestApp(); pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { singlePublisherPerType: true, }); + otherPipelineStack = new Stack(app, 'OtherPipelineStack', { env: PIPELINE_ENV }); + otherPipeline = new TestGitHubNpmPipeline(otherPipelineStack, 'Cdk', { + singlePublisherPerType: true, + }); }); behavior('multiple assets are using the same job in singlePublisherMode', (suite) => { @@ -534,15 +541,35 @@ describe('pipeline with single asset publisher', () => { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: 'buildspec-assets-FileAsset.yaml', + BuildSpec: 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml', }, }); const assembly = SynthUtils.synthesize(pipelineStack, { skipValidation: true }).assembly; - const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, 'buildspec-assets-FileAsset.yaml')).toString()); + const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml')).toString()); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`); }); }); + + behavior('other pipeline writes to separate assets build spec file', (suite) => { + suite.legacy(() => { + // WHEN + pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + otherPipeline.addApplicationStage(new TwoFileAssetsApp(app, 'OtherFileAssetApp')); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Source: { + BuildSpec: 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml', + }, + }); + expect(otherPipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Source: { + BuildSpec: 'buildspec-assets-OtherPipelineStack-Cdk-Assets-FileAsset.yaml', + }, + }); + }); + }); }); describe('pipeline with Docker credentials', () => { From 1d19b3b66e7c91ae8d9b4c49432bd0249e18a366 Mon Sep 17 00:00:00 2001 From: Hugo Drumond Date: Thu, 8 Jul 2021 10:57:43 +0100 Subject: [PATCH 022/105] fix(lambda-nodejs): pnpm exec args separator order (#15410) Fixes the local bundling command when using the pnpm package manager. The args separator is now before the command as described in https://github.com/pnpm/pnpm/blob/76136751958ceac0ee77e9a0466b96d4a093a094/packages/plugin-commands-script-runners/src/exec.ts#L73. fixes #15164 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda-nodejs/lib/package-manager.ts | 2 +- .../@aws-cdk/aws-lambda-nodejs/test/package-manager.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/package-manager.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/package-manager.ts index d69ceb971f4c6..0f18c92cbde20 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/package-manager.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/package-manager.ts @@ -63,8 +63,8 @@ export class PackageManager { return [ os.platform() === 'win32' ? `${runCommand}.cmd` : runCommand, ...runArgs, - bin, ...(this.argsSeparator ? [this.argsSeparator] : []), + bin, ].join(' '); } } diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/package-manager.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/package-manager.test.ts index 7f64a18d2123f..19369bdd07d9a 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/package-manager.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/package-manager.test.ts @@ -19,7 +19,7 @@ test('from a pnpm-lock.yaml', () => { const packageManager = PackageManager.fromLockFile('/path/to/pnpm-lock.yaml'); expect(packageManager).toEqual(PackageManager.PNPM); - expect(packageManager.runBinCommand('my-bin')).toBe('pnpm exec my-bin --'); + expect(packageManager.runBinCommand('my-bin')).toBe('pnpm exec -- my-bin'); }); test('defaults to NPM', () => { From b95ee4414a632a5b8622c4d6a11bd2baaab536f2 Mon Sep 17 00:00:00 2001 From: TikiTDO Date: Thu, 8 Jul 2021 06:37:18 -0400 Subject: [PATCH 023/105] fix(iam): set principalAccount in AccountPrincipal and PrincipalWithConditions (#15430) As per to `IPrincipal` documentation `principalAccount` should be: ``` The AWS account ID of this principal. Can be undefined when the account is not known (for example, for service principals). Can be a Token - in that case, it's assumed to be AWS::AccountId. ``` This PR ensures that `AccountPrincipal` sets the principal account ID as expected. Also ensures that the same id is available after adding conditions to the account principal. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-iam/lib/principals.ts | 7 +++++++ packages/@aws-cdk/aws-iam/test/principals.test.ts | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/@aws-cdk/aws-iam/lib/principals.ts b/packages/@aws-cdk/aws-iam/lib/principals.ts index 02bde4cfb4cd2..80b4480209fb9 100644 --- a/packages/@aws-cdk/aws-iam/lib/principals.ts +++ b/packages/@aws-cdk/aws-iam/lib/principals.ts @@ -193,6 +193,10 @@ export class PrincipalWithConditions implements IPrincipal { return new PrincipalPolicyFragment(this.principal.policyFragment.principalJson, this.conditions); } + public get principalAccount(): string | undefined { + return this.principal.principalAccount; + } + public addToPolicy(statement: PolicyStatement): boolean { return this.addToPrincipalPolicy(statement).statementAdded; } @@ -294,12 +298,15 @@ export class ArnPrincipal extends PrincipalBase { * Specify AWS account ID as the principal entity in a policy to delegate authority to the account. */ export class AccountPrincipal extends ArnPrincipal { + public readonly principalAccount: string | undefined; + /** * * @param accountId AWS account ID (i.e. 123456789012) */ constructor(public readonly accountId: any) { super(new StackDependentToken(stack => `arn:${stack.partition}:iam::${accountId}:root`).toString()); + this.principalAccount = accountId; } public toString() { diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index ed23d7eb0ce09..c2495b2975bfd 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -166,3 +166,13 @@ test('SAML principal', () => { }, }); }); + +test('PrincipalWithConditions inherits principalAccount from AccountPrincipal ', () => { + // GIVEN + const accountPrincipal = new iam.AccountPrincipal('123456789012'); + const principalWithConditions = accountPrincipal.withConditions({ StringEquals: { hairColor: 'blond' } }); + + // THEN + expect(accountPrincipal.principalAccount).toStrictEqual('123456789012'); + expect(principalWithConditions.principalAccount).toStrictEqual('123456789012'); +}); \ No newline at end of file From afa5de108d393950034c8ff6c5e49d2ec5490c7f Mon Sep 17 00:00:00 2001 From: Alban Esc Date: Thu, 8 Jul 2021 04:16:02 -0700 Subject: [PATCH 024/105] fix(events): Archive event pattern fields are not translated correctly (#15376) The EventBridge Archive supports an `eventPattern` (same as EventBridge Rules). However, it does not translate `detailType` into `detail-type`. This change uses the same `renderEventPattern` utility for both rules and archives in order to fix this. Fixes #14905 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-events/lib/archive.ts | 3 +- packages/@aws-cdk/aws-events/lib/rule.ts | 20 +--------- packages/@aws-cdk/aws-events/lib/util.ts | 24 ++++++++++++ .../@aws-cdk/aws-events/test/test.archive.ts | 39 +++++++++++++++++++ 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-events/lib/archive.ts b/packages/@aws-cdk/aws-events/lib/archive.ts index 3da79df6682a2..b4710cc3c7fb8 100644 --- a/packages/@aws-cdk/aws-events/lib/archive.ts +++ b/packages/@aws-cdk/aws-events/lib/archive.ts @@ -3,6 +3,7 @@ import { Construct } from 'constructs'; import { IEventBus } from './event-bus'; import { EventPattern } from './event-pattern'; import { CfnArchive } from './events.generated'; +import { renderEventPattern } from './util'; /** * The event archive base properties @@ -66,7 +67,7 @@ export class Archive extends Resource { let archive = new CfnArchive(this, 'Archive', { sourceArn: props.sourceEventBus.eventBusArn, description: props.description, - eventPattern: props.eventPattern, + eventPattern: renderEventPattern(props.eventPattern), retentionDays: props.retention?.toDays({ integral: true }) || 0, archiveName: this.physicalName, }); diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 9962328f97619..ada50ddb994fa 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -6,7 +6,7 @@ import { CfnEventBusPolicy, CfnRule } from './events.generated'; import { IRule } from './rule-ref'; import { Schedule } from './schedule'; import { IRuleTarget } from './target'; -import { mergeEventPattern } from './util'; +import { mergeEventPattern, renderEventPattern } from './util'; /** * Properties for defining an EventBridge Rule @@ -354,23 +354,7 @@ export class Rule extends Resource implements IRule { * @internal */ public _renderEventPattern(): any { - const eventPattern = this.eventPattern; - - if (Object.keys(eventPattern).length === 0) { - return undefined; - } - - // rename 'detailType' to 'detail-type' - const out: any = {}; - for (let key of Object.keys(eventPattern)) { - const value = (eventPattern as any)[key]; - if (key === 'detailType') { - key = 'detail-type'; - } - out[key] = value; - } - - return out; + return renderEventPattern(this.eventPattern); } protected validate() { diff --git a/packages/@aws-cdk/aws-events/lib/util.ts b/packages/@aws-cdk/aws-events/lib/util.ts index 345bfda4ff89e..84ae6713ef119 100644 --- a/packages/@aws-cdk/aws-events/lib/util.ts +++ b/packages/@aws-cdk/aws-events/lib/util.ts @@ -1,3 +1,5 @@ +import { EventPattern } from './event-pattern'; + /** * Merge the `src` event pattern into the `dest` event pattern by adding all * values from `src` into the fields in `dest`. @@ -51,3 +53,25 @@ export function mergeEventPattern(dest: any, src: any) { } } } + +/** + * Transform an eventPattern object into a valid Event Rule Pattern + * by changing detailType into detail-type when present. + */ +export function renderEventPattern(eventPattern: EventPattern): any { + if (Object.keys(eventPattern).length === 0) { + return undefined; + } + + // rename 'detailType' to 'detail-type' + const out: any = {}; + for (let key of Object.keys(eventPattern)) { + const value = (eventPattern as any)[key]; + if (key === 'detailType') { + key = 'detail-type'; + } + out[key] = value; + } + + return out; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/test/test.archive.ts b/packages/@aws-cdk/aws-events/test/test.archive.ts index 5d04700bea20b..84c9d12222e42 100644 --- a/packages/@aws-cdk/aws-events/test/test.archive.ts +++ b/packages/@aws-cdk/aws-events/test/test.archive.ts @@ -40,6 +40,45 @@ export = { }, })); + test.done(); + }, + 'creates an archive for an EventBus with a pattern including a detailType property'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + let eventBus = new EventBus(stack, 'Bus'); + + new Archive(stack, 'Archive', { + sourceEventBus: eventBus, + eventPattern: { + account: [stack.account], + detailType: ['Custom Detail Type'], + }, + retention: Duration.days(10), + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::EventBus', { + Name: 'Bus', + })); + + expect(stack).to(haveResource('AWS::Events::Archive', { + EventPattern: { + 'account': [{ + Ref: 'AWS::AccountId', + }], + 'detail-type': ['Custom Detail Type'], + }, + RetentionDays: 10, + SourceArn: { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + })); + test.done(); }, } From bf6f7efd01ee3a4dc62124baa969eb5e22e58e52 Mon Sep 17 00:00:00 2001 From: Ruan Comelli Date: Thu, 8 Jul 2021 08:55:24 -0300 Subject: [PATCH 025/105] fix(autoscaling): scaling intervals are incorrect if the bottom one does not start at 0 (#15345) Before this fix, the first scaling step would be ignored whenever its lower bound was greater than zero (see linked issue). Now, instead of *replacing* the first interval with a "dummy" one, we *prepend* the list of intervals. This behavior complements what was already implemented in case the last interval's upper bound was not Infinity: a dummy one was appended. This fixes #10141 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/interval-utils.ts | 2 +- .../test/test.intervals.ts | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-autoscaling-common/lib/interval-utils.ts b/packages/@aws-cdk/aws-autoscaling-common/lib/interval-utils.ts index 246bfc018507b..6b5ae315b33de 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/lib/interval-utils.ts +++ b/packages/@aws-cdk/aws-autoscaling-common/lib/interval-utils.ts @@ -90,7 +90,7 @@ function makeGapsUndefined(intervals: CompleteScalingInterval[]) { // going to make scaling intervals extend all the way out to infinity on either side, // the result is the same for absolute adjustments anyway. if (intervals[0].lower !== 0) { - intervals.splice(0, 1, { + intervals.splice(0, 0, { lower: 0, upper: intervals[0].lower, change: undefined, diff --git a/packages/@aws-cdk/aws-autoscaling-common/test/test.intervals.ts b/packages/@aws-cdk/aws-autoscaling-common/test/test.intervals.ts index f90aafdb5e095..379ed29eafdf5 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/test/test.intervals.ts +++ b/packages/@aws-cdk/aws-autoscaling-common/test/test.intervals.ts @@ -19,6 +19,29 @@ export = { test.done(); }, + 'test interval completion'(test: Test) { + const lowerIncompleteIntervals = normalizeIntervals( + incompleteLowerRelativeIntervals(), + false); + const upperIncompleteIntervals = normalizeIntervals( + incompleteUpperRelativeIntervals(), + false); + + test.deepEqual(lowerIncompleteIntervals, [ + { lower: 0, upper: 65, change: undefined }, + { lower: 65, upper: 85, change: +2 }, + { lower: 85, upper: Infinity, change: +3 }, + ]); + + test.deepEqual(upperIncompleteIntervals, [ + { lower: 0, upper: 65, change: +2 }, + { lower: 65, upper: 85, change: +3 }, + { lower: 85, upper: Infinity, change: undefined }, + ]); + + test.done(); + }, + 'bounds propagation fails if middle boundary missing'(test: Test) { test.throws(() => { normalizeIntervals([ @@ -113,3 +136,21 @@ function realisticRelativeIntervals(): appscaling.ScalingInterval[] { { lower: 90, change: +2 }, ]; } + +function incompleteLowerRelativeIntervals(): appscaling.ScalingInterval[] { + // Function so we don't have to worry about cloning + // The first interval's lower is not zero nor undefined + return [ + { lower: 65, change: +2 }, + { lower: 85, change: +3 }, + ]; +} + +function incompleteUpperRelativeIntervals(): appscaling.ScalingInterval[] { + // Function so we don't have to worry about cloning + // The last interval's upper is not Infinity nor undefined + return [ + { upper: 65, change: +2 }, + { upper: 85, change: +3 }, + ]; +} From 5ecf2577350da2b9ff7115b2868192bcbd56a56e Mon Sep 17 00:00:00 2001 From: Alban Esc Date: Thu, 8 Jul 2021 05:37:03 -0700 Subject: [PATCH 026/105] feat(events): DLQ and retry policy support for BatchJob target (#15308) Add DLQ and Retry policy Configuration to BatchJob targets. Using a DLQ on a rule prevents the application to loose events after all retry attempts are exhausted. Update README.md and code examples to support Rosetta translation and compiling. Resolves #15238 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-events-targets/README.md | 103 +++++-- .../@aws-cdk/aws-events-targets/lib/batch.ts | 9 +- .../aws-events-targets/lib/kinesis-stream.ts | 8 +- .../@aws-cdk/aws-events-targets/lib/sns.ts | 8 +- .../@aws-cdk/aws-events-targets/lib/sqs.ts | 8 +- .../lib/withRepoAndKinesisStream.ts-fixture | 23 ++ .../rosetta/default.ts-fixture | 16 + .../withRepoAndKinesisStream.ts-fixture | 23 ++ .../rosetta/withRepoAndSqsQueue.ts-fixture | 23 ++ .../rosetta/withRepoAndTopic.ts-fixture | 23 ++ .../test/batch/batch.test.ts | 281 ++++++++++++++---- .../integ.job-definition-events.expected.json | 58 +++- .../test/batch/integ.job-definition-events.ts | 10 +- 13 files changed, 486 insertions(+), 107 deletions(-) create mode 100644 packages/@aws-cdk/aws-events-targets/lib/withRepoAndKinesisStream.ts-fixture create mode 100644 packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture create mode 100644 packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndKinesisStream.ts-fixture create mode 100644 packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndSqsQueue.ts-fixture create mode 100644 packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndTopic.ts-fixture diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index 6dcc3598a26e7..994c24ab8a0a5 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -23,7 +23,7 @@ Currently supported are: * Publish a message to an SNS topic * Send a message to an SQS queue * [Start a StepFunctions state machine](#start-a-stepfunctions-state-machine) -* Queue a Batch job +* [Queue a Batch job](#queue-a-batch-job) * Make an AWS API call * Put a record to a Kinesis stream * [Log an event into a LogGroup](#log-an-event-into-a-loggroup) @@ -47,16 +47,12 @@ triggered for every events from `aws.ec2` source. You can optionally attach a [dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). ```ts -import * as lambda from "@aws-cdk/aws-lambda"; -import * as events from "@aws-cdk/aws-events"; -import * as sqs from "@aws-cdk/aws-sqs"; -import * as targets from "@aws-cdk/aws-events-targets"; -import * as cdk from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; const fn = new lambda.Function(this, 'MyFunc', { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', - code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`), + code: lambda.Code.fromInline(`exports.handler = handler.toString()`), }); const rule = new events.Rule(this, 'rule', { @@ -82,9 +78,7 @@ For example, the following code snippet creates an event rule with a CloudWatch Every events sent from the `aws.ec2` source will be sent to the CloudWatch LogGroup. ```ts -import * as logs from "@aws-cdk/aws-logs"; -import * as events from "@aws-cdk/aws-events"; -import * as targets from "@aws-cdk/aws-events-targets"; +import * as logs from '@aws-cdk/aws-logs'; const logGroup = new logs.LogGroup(this, 'MyLogGroup', { logGroupName: 'MyLogGroup', @@ -108,10 +102,8 @@ on commit to the master branch. You can optionally attach a [dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). ```ts -import * as codebuild from '@aws-sdk/aws-codebuild'; -import * as codecommit from '@aws-sdk/aws-codecommit'; -import * as sqs from '@aws-sdk/aws-sqs'; -import * as targets from "@aws-cdk/aws-events-targets"; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codecommit from '@aws-cdk/aws-codecommit'; const repo = new codecommit.Repository(this, 'MyRepo', { repositoryName: 'aws-cdk-codebuild-events', @@ -139,12 +131,11 @@ Use the `CodePipeline` target to trigger a CodePipeline pipeline. The code snippet below creates a CodePipeline pipeline that is triggered every hour ```ts -import * as codepipeline from '@aws-sdk/aws-codepipeline'; -import * as sqs from '@aws-sdk/aws-sqs'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; const pipeline = new codepipeline.Pipeline(this, 'Pipeline'); -const rule = new events.Rule(stack, 'Rule', { +const rule = new events.Rule(this, 'Rule', { schedule: events.Schedule.expression('rate(1 hour)'), }); @@ -162,22 +153,20 @@ You can optionally attach a to the target. ```ts -import * as iam from '@aws-sdk/aws-iam'; -import * as sqs from '@aws-sdk/aws-sqs'; +import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as targets from "@aws-cdk/aws-events-targets"; -const rule = new events.Rule(stack, 'Rule', { +const rule = new events.Rule(this, 'Rule', { schedule: events.Schedule.rate(cdk.Duration.minutes(1)), }); -const dlq = new sqs.Queue(stack, 'DeadLetterQueue'); +const dlq = new sqs.Queue(this, 'DeadLetterQueue'); -const role = new iam.Role(stack, 'Role', { +const role = new iam.Role(this, 'Role', { assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), }); -const stateMachine = new sfn.StateMachine(stack, 'SM', { - definition: new sfn.Wait(stack, 'Hello', { time: sfn.WaitTime.duration(cdk.Duration.seconds(10)) }), +const stateMachine = new sfn.StateMachine(this, 'SM', { + definition: new sfn.Wait(this, 'Hello', { time: sfn.WaitTime.duration(cdk.Duration.seconds(10)) }), role, }); @@ -187,19 +176,67 @@ rule.addTarget(new targets.SfnStateMachine(stateMachine, { })); ``` +## Queue a Batch job + +Use the `BatchJob` target to queue a Batch job. + +The code snippet below creates a Simple JobQueue that is triggered every hour with a +dummy object as input. +You can optionally attach a +[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html) +to the target. + +```ts +import * as batch from '@aws-cdk/aws-batch'; +import { ContainerImage } from '@aws-cdk/aws-ecs'; + +const jobQueue = new batch.JobQueue(this, 'MyQueue', { + computeEnvironments: [ + { + computeEnvironment: new batch.ComputeEnvironment(this, 'ComputeEnvironment', { + managed: false, + }), + order: 1, + }, + ], +}); + +const jobDefinition = new batch.JobDefinition(this, 'MyJob', { + container: { + image: ContainerImage.fromRegistry('test-repo'), + }, +}); + +const queue = new sqs.Queue(this, 'Queue'); + +const rule = new events.Rule(this, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.hours(1)), +}); + +rule.addTarget(new targets.BatchJob( + jobQueue.jobQueueArn, + jobQueue, + jobDefinition.jobDefinitionArn, + jobDefinition, { + deadLetterQueue: queue, + event: events.RuleTargetInput.fromObject({ SomeParam: 'SomeValue' }), + retryAttempts: 2, + maxEventAge: cdk.Duration.hours(2), + }, +)); +``` + ## Invoke a API Gateway REST API Use the `ApiGateway` target to trigger a REST API. The code snippet below creates a Api Gateway REST API that is invoked every hour. -```typescript -import * as iam from '@aws-sdk/aws-iam'; -import * as sqs from '@aws-sdk/aws-sqs'; +```ts import * as api from '@aws-cdk/aws-apigateway'; -import * as targets from "@aws-cdk/aws-events-targets"; +import * as lambda from '@aws-cdk/aws-lambda'; -const rule = new events.Rule(stack, 'Rule', { +const rule = new events.Rule(this, 'Rule', { schedule: events.Schedule.rate(cdk.Duration.minutes(1)), }); @@ -211,12 +248,12 @@ const fn = new lambda.Function( this, 'MyFunc', { const restApi = new api.LambdaRestApi( this, 'MyRestAPI', { handler: fn } ); -const dlq = new sqs.Queue(stack, 'DeadLetterQueue'); +const dlq = new sqs.Queue(this, 'DeadLetterQueue'); rule.addTarget( new targets.ApiGateway( restApi, { path: '/*/test', - mehod: 'GET', + method: 'GET', stage: 'prod', pathParameterValues: ['path-value'], headerParameters: { @@ -225,7 +262,7 @@ rule.addTarget( queryStringParameters: { QueryParam1: 'query-param-1', }, - deadLetterQueue: queue + deadLetterQueue: dlq } ), ) ``` diff --git a/packages/@aws-cdk/aws-events-targets/lib/batch.ts b/packages/@aws-cdk/aws-events-targets/lib/batch.ts index 15a823549055c..f0186efe6089d 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/batch.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/batch.ts @@ -1,12 +1,12 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { Names, IConstruct } from '@aws-cdk/core'; -import { singletonEventRole } from './util'; +import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util'; /** * Customize the Batch Job Event Target */ -export interface BatchJobProps { +export interface BatchJobProps extends TargetBaseProps { /** * The event to send to the Lambda * @@ -83,7 +83,12 @@ export class BatchJob implements events.IRuleTarget { retryStrategy: this.props.attempts ? { attempts: this.props.attempts } : undefined, }; + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue); + } + return { + ...bindBaseTargetConfig(this.props), arn: this.jobQueueArn, // When scoping resource-level access for job submission, you must provide both job queue and job definition resource types. // https://docs.aws.amazon.com/batch/latest/userguide/ExamplePolicies_BATCH.html#iam-example-restrict-job-def diff --git a/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts b/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts index 60bb544223729..743b197e19d52 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts @@ -29,10 +29,10 @@ export interface KinesisStreamProps { * Use a Kinesis Stream as a target for AWS CloudWatch event rules. * * @example - * - * // put to a Kinesis stream every time code is committed - * // to a CodeCommit repository - * repository.onCommit(new targets.KinesisStream(stream)); + * /// fixture=withRepoAndKinesisStream + * // put to a Kinesis stream every time code is committed + * // to a CodeCommit repository + * repository.onCommit('onCommit', { target: new targets.KinesisStream(stream) }); * */ export class KinesisStream implements events.IRuleTarget { diff --git a/packages/@aws-cdk/aws-events-targets/lib/sns.ts b/packages/@aws-cdk/aws-events-targets/lib/sns.ts index 13f10f7d80552..81e5d4916718f 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sns.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sns.ts @@ -18,10 +18,10 @@ export interface SnsTopicProps { * Use an SNS topic as a target for Amazon EventBridge rules. * * @example - * - * // publish to an SNS topic every time code is committed - * // to a CodeCommit repository - * repository.onCommit(new targets.SnsTopic(topic)); + * /// fixture=withRepoAndTopic + * // publish to an SNS topic every time code is committed + * // to a CodeCommit repository + * repository.onCommit('onCommit', { target: new targets.SnsTopic(topic) }); * */ export class SnsTopic implements events.IRuleTarget { diff --git a/packages/@aws-cdk/aws-events-targets/lib/sqs.ts b/packages/@aws-cdk/aws-events-targets/lib/sqs.ts index 501414ecee348..43fb9b8ed15d0 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/sqs.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/sqs.ts @@ -31,10 +31,10 @@ export interface SqsQueueProps { * Use an SQS Queue as a target for Amazon EventBridge rules. * * @example - * - * // publish to an SQS queue every time code is committed - * // to a CodeCommit repository - * repository.onCommit(new targets.SqsQueue(queue)); + * /// fixture=withRepoAndSqsQueue + * // publish to an SQS queue every time code is committed + * // to a CodeCommit repository + * repository.onCommit('onCommit', { target: new targets.SqsQueue(queue) }); * */ export class SqsQueue implements events.IRuleTarget { diff --git a/packages/@aws-cdk/aws-events-targets/lib/withRepoAndKinesisStream.ts-fixture b/packages/@aws-cdk/aws-events-targets/lib/withRepoAndKinesisStream.ts-fixture new file mode 100644 index 0000000000000..431f8511e02c9 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/withRepoAndKinesisStream.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const stream = new kinesis.Stream(stack, 'MyStream'); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..f6bf67d19e31a --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture @@ -0,0 +1,16 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndKinesisStream.ts-fixture b/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndKinesisStream.ts-fixture new file mode 100644 index 0000000000000..115e1ece7e254 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndKinesisStream.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const stream = new kinesis.Stream(this, 'MyStream'); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndSqsQueue.ts-fixture b/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndSqsQueue.ts-fixture new file mode 100644 index 0000000000000..98d029d8d8283 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndSqsQueue.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const queue = new sqs.Queue(this, 'MyQueue'); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndTopic.ts-fixture b/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndTopic.ts-fixture new file mode 100644 index 0000000000000..30c1f29cc331b --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/rosetta/withRepoAndTopic.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as sns from '@aws-cdk/aws-sns'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const topic = new sns.Topic(this, 'MyTopic'); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-events-targets/test/batch/batch.test.ts b/packages/@aws-cdk/aws-events-targets/test/batch/batch.test.ts index acdf2477832f5..c379b72a523c6 100644 --- a/packages/@aws-cdk/aws-events-targets/test/batch/batch.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/batch/batch.test.ts @@ -2,74 +2,239 @@ import { expect, haveResource } from '@aws-cdk/assert-internal'; import * as batch from '@aws-cdk/aws-batch'; import { ContainerImage } from '@aws-cdk/aws-ecs'; import * as events from '@aws-cdk/aws-events'; -import { Stack } from '@aws-cdk/core'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { Duration, Stack } from '@aws-cdk/core'; import * as targets from '../../lib'; -test('use aws batch job as an eventrule target', () => { - // GIVEN - const stack = new Stack(); - const jobQueue = new batch.JobQueue(stack, 'MyQueue', { - computeEnvironments: [ - { - computeEnvironment: new batch.ComputeEnvironment(stack, 'ComputeEnvironment', { - managed: false, - }), - order: 1, +describe('Batch job event target', () => { + let stack: Stack; + let jobQueue: batch.JobQueue; + let jobDefinition: batch.JobDefinition; + + + beforeEach(() => { + stack = new Stack(); + jobQueue = new batch.JobQueue(stack, 'MyQueue', { + computeEnvironments: [ + { + computeEnvironment: new batch.ComputeEnvironment(stack, 'ComputeEnvironment', { + managed: false, + }), + order: 1, + }, + ], + }); + jobDefinition = new batch.JobDefinition(stack, 'MyJob', { + container: { + image: ContainerImage.fromRegistry('test-repo'), }, - ], - }); - const jobDefinition = new batch.JobDefinition(stack, 'MyJob', { - container: { - image: ContainerImage.fromRegistry('test-repo'), - }, - }); - const rule = new events.Rule(stack, 'Rule', { - schedule: events.Schedule.expression('rate(1 min)'), + }); }); - // WHEN - rule.addTarget(new targets.BatchJob(jobQueue.jobQueueArn, jobQueue, jobDefinition.jobDefinitionArn, jobDefinition)); - - // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { - ScheduleExpression: 'rate(1 min)', - State: 'ENABLED', - Targets: [ - { - Arn: { - Ref: 'MyQueueE6CA6235', + test('use aws batch job as an event rule target', () => { + // GIVEN + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // WHEN + rule.addTarget(new targets.BatchJob(jobQueue.jobQueueArn, jobQueue, jobDefinition.jobDefinitionArn, jobDefinition)); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 min)', + State: 'ENABLED', + Targets: [ + { + Arn: { + Ref: 'MyQueueE6CA6235', + }, + Id: 'Target0', + RoleArn: { + 'Fn::GetAtt': [ + 'MyJobEventsRoleCF43C336', + 'Arn', + ], + }, + BatchParameters: { + JobDefinition: { Ref: 'MyJob8719E923' }, + JobName: 'Rule', + }, }, - Id: 'Target0', - RoleArn: { - 'Fn::GetAtt': [ - 'MyJobEventsRoleCF43C336', - 'Arn', - ], + ], + })); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'batch:SubmitJob', + Effect: 'Allow', + Resource: [ + { Ref: 'MyJob8719E923' }, + { Ref: 'MyQueueE6CA6235' }, + ], + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { Ref: 'MyJobEventsRoleCF43C336' }, + ], + })); + }); + + test('use a Dead Letter Queue for the rule target', () => { + // GIVEN + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 hour)'), + }); + + const queue = new sqs.Queue(stack, 'Queue'); + + // WHEN + const eventInput = { + buildspecOverride: 'buildspecs/hourly.yml', + }; + + rule.addTarget(new targets.BatchJob( + jobQueue.jobQueueArn, + jobQueue, + jobDefinition.jobDefinitionArn, + jobDefinition, { + deadLetterQueue: queue, + event: events.RuleTargetInput.fromObject(eventInput), + }, + )); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: { + Ref: 'MyQueueE6CA6235', + }, + Id: 'Target0', + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + }, + Input: JSON.stringify(eventInput), + RoleArn: { + 'Fn::GetAtt': ['MyJobEventsRoleCF43C336', 'Arn'], + }, + BatchParameters: { + JobDefinition: { Ref: 'MyJob8719E923' }, + JobName: 'Rule', + }, }, - BatchParameters: { - JobDefinition: { Ref: 'MyJob8719E923' }, - JobName: 'Rule', + ], + })); + + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + 'Fn::GetAtt': [ + 'Rule4C995B7F', + 'Arn', + ], + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + Sid: 'AllowEventRuleRule', + }, + ], + Version: '2012-10-17', + }, + Queues: [ + { + Ref: 'Queue4A7E3555', }, + ], + })); + }); + + test('specifying retry policy', () => { + // GIVEN + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 hour)'), + }); + + const queue = new sqs.Queue(stack, 'Queue'); + + // WHEN + const eventInput = { + buildspecOverride: 'buildspecs/hourly.yml', + }; + + rule.addTarget(new targets.BatchJob( + jobQueue.jobQueueArn, + jobQueue, + jobDefinition.jobDefinitionArn, + jobDefinition, { + deadLetterQueue: queue, + event: events.RuleTargetInput.fromObject(eventInput), + retryAttempts: 2, + maxEventAge: Duration.hours(2), }, - ], - })); + )); - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 hour)', + State: 'ENABLED', + Targets: [ { - Action: 'batch:SubmitJob', - Effect: 'Allow', - Resource: [ - { Ref: 'MyJob8719E923' }, - { Ref: 'MyQueueE6CA6235' }, - ], + Arn: { + Ref: 'MyQueueE6CA6235', + }, + BatchParameters: { + JobDefinition: { + Ref: 'MyJob8719E923', + }, + JobName: 'Rule', + }, + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + }, + Id: 'Target0', + Input: JSON.stringify(eventInput), + RetryPolicy: { + MaximumEventAgeInSeconds: 7200, + MaximumRetryAttempts: 2, + }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyJobEventsRoleCF43C336', + 'Arn', + ], + }, }, ], - Version: '2012-10-17', - }, - Roles: [ - { Ref: 'MyJobEventsRoleCF43C336' }, - ], - })); -}); + })); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/batch/integ.job-definition-events.expected.json b/packages/@aws-cdk/aws-events-targets/test/batch/integ.job-definition-events.expected.json index 9319f72814c38..77a8854041e1f 100644 --- a/packages/@aws-cdk/aws-events-targets/test/batch/integ.job-definition-events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/batch/integ.job-definition-events.expected.json @@ -34,13 +34,13 @@ "ComputeEnvironmentC570994D": { "Type": "AWS::Batch::ComputeEnvironment", "Properties": { + "Type": "UNMANAGED", "ServiceRole": { "Fn::GetAtt": [ "ComputeEnvironmentResourceServiceInstanceRoleDC6D4445", "Arn" ] }, - "Type": "UNMANAGED", "State": "ENABLED" } }, @@ -164,7 +164,19 @@ }, "JobName": "batcheventsTimer232549135" }, + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + } + }, "Id": "Target0", + "RetryPolicy": { + "MaximumEventAgeInSeconds": 7200, + "MaximumRetryAttempts": 2 + }, "RoleArn": { "Fn::GetAtt": [ "MyJobEventsRoleCF43C336", @@ -174,6 +186,50 @@ } ] } + }, + "Queue4A7E3555": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "QueuePolicy25439813": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "Timer2B6F162E9", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + }, + "Sid": "AllowEventRulebatcheventsTimer232549135" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "Queue4A7E3555" + } + ] + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/batch/integ.job-definition-events.ts b/packages/@aws-cdk/aws-events-targets/test/batch/integ.job-definition-events.ts index 01d1058eb4064..a3c2fa2f01e2e 100644 --- a/packages/@aws-cdk/aws-events-targets/test/batch/integ.job-definition-events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/batch/integ.job-definition-events.ts @@ -1,6 +1,7 @@ import * as batch from '@aws-cdk/aws-batch'; import { ContainerImage } from '@aws-cdk/aws-ecs'; import * as events from '@aws-cdk/aws-events'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -32,6 +33,13 @@ timer.addTarget(new targets.BatchJob(queue.jobQueueArn, queue, job.jobDefinition const timer2 = new events.Rule(stack, 'Timer2', { schedule: events.Schedule.rate(cdk.Duration.minutes(2)), }); -timer2.addTarget(new targets.BatchJob(queue.jobQueueArn, queue, job.jobDefinitionArn, job)); + +const dlq = new sqs.Queue(stack, 'Queue'); + +timer2.addTarget(new targets.BatchJob(queue.jobQueueArn, queue, job.jobDefinitionArn, job, { + deadLetterQueue: dlq, + retryAttempts: 2, + maxEventAge: cdk.Duration.hours(2), +})); app.synth(); From 850cba0ad206d4e4e47ca48ca50a073880221dcc Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Thu, 8 Jul 2021 14:14:58 +0100 Subject: [PATCH 027/105] fix(iam): remove incorrect normalization of principal (#15248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the incorrect normalization of principal like below: `"Principal": {"AWS": "*"}` → `"Principal": "*"` I thought this normalization was made in accordance with this doc. https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#principal-anonymous As discussed in #14274, this normalization sometimes cause error like `Error occurred while parsing policy: no statement block with AWS provider found.` I also fixed all of the related tests. Closes #14274 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../integ.restapi.vpc-endpoint.expected.json | 8 ++++--- .../@aws-cdk/aws-backup/test/vault.test.ts | 2 +- .../integ.cloudfront-custom-s3.expected.json | 4 +++- .../test/integ.vpc-endpoint.lit.expected.json | 4 +++- .../aws-ec2/test/vpc-endpoint.test.ts | 2 +- .../@aws-cdk/aws-ecr/test/repository.test.ts | 4 ++-- .../test/elasticsearch-access-policy.test.ts | 4 ++-- ...elasticsearch.custom-kms-key.expected.json | 4 ++-- .../test/integ.elasticsearch.expected.json | 8 +++---- ...sticsearch.unsignedbasicauth.expected.json | 4 ++-- .../@aws-cdk/aws-iam/lib/policy-statement.ts | 3 --- .../aws-iam/test/policy-document.test.ts | 6 ++--- packages/@aws-cdk/aws-kms/test/key.test.ts | 2 +- .../test/via-service-principal.test.ts | 4 ++-- .../@aws-cdk/aws-msk/test/cluster.test.ts | 2 +- .../aws-s3/test/bucket-policy.test.ts | 6 ++--- packages/@aws-cdk/aws-s3/test/bucket.test.ts | 24 +++++++++---------- 17 files changed, 47 insertions(+), 44 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json index 872513b9b89a2..9051ff580c010 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json @@ -624,7 +624,9 @@ { "Action": "execute-api:Invoke", "Effect": "Allow", - "Principal": "*" + "Principal": { + "AWS": "*" + } } ], "Version": "2012-10-17" @@ -676,7 +678,7 @@ "MyApi49610EDF" ] }, - "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc": { + "MyApiDeploymentECB0D05E58dcfc85d01f2b81270e177f5347476d": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -695,7 +697,7 @@ "Ref": "MyApi49610EDF" }, "DeploymentId": { - "Ref": "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc" + "Ref": "MyApiDeploymentECB0D05E58dcfc85d01f2b81270e177f5347476d" }, "StageName": "prod" } diff --git a/packages/@aws-cdk/aws-backup/test/vault.test.ts b/packages/@aws-cdk/aws-backup/test/vault.test.ts index 6f3b4f9446720..e1a67e0e0ed8b 100644 --- a/packages/@aws-cdk/aws-backup/test/vault.test.ts +++ b/packages/@aws-cdk/aws-backup/test/vault.test.ts @@ -52,7 +52,7 @@ test('with access policy', () => { Statement: [ { Effect: 'Deny', - Principal: '*', + Principal: { AWS: '*' }, Action: 'backup:DeleteRecoveryPoint', Resource: '*', Condition: { diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-custom-s3.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-custom-s3.expected.json index 7699faf6df792..c5a63febc279a 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-custom-s3.expected.json +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-custom-s3.expected.json @@ -22,7 +22,9 @@ { "Action": "s3:GetObject", "Effect": "Allow", - "Principal": "*", + "Principal": { + "AWS": "*" + }, "Resource": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json index 4adbc5e48fc91..bf26c497e9064 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json @@ -581,7 +581,9 @@ "dynamodb:ListTables" ], "Effect": "Allow", - "Principal": "*", + "Principal": { + "AWS": "*" + }, "Resource": "*" } ], diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts index 036e173910454..a59dfbbca07eb 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts @@ -133,7 +133,7 @@ nodeunitShim({ 's3:ListBucket', ], Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: '*', }, ], diff --git a/packages/@aws-cdk/aws-ecr/test/repository.test.ts b/packages/@aws-cdk/aws-ecr/test/repository.test.ts index 9b8c1f79796c5..37cc58a8485eb 100644 --- a/packages/@aws-cdk/aws-ecr/test/repository.test.ts +++ b/packages/@aws-cdk/aws-ecr/test/repository.test.ts @@ -290,7 +290,7 @@ describe('repository', () => { { Action: '*', Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, }, ], Version: '2012-10-17', @@ -512,7 +512,7 @@ describe('repository', () => { 'ecr:BatchGetImage', ], 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, }, ], 'Version': '2012-10-17', diff --git a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts index 6d2d484cabd9f..ebb83e123ed84 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts @@ -38,7 +38,7 @@ test('minimal example renders correctly', () => { service: 'ES', parameters: { DomainName: 'TestDomain', - AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":"*","Resource":"test:arn"}],"Version":"2012-10-17"}', + AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":{"AWS":"*"},"Resource":"test:arn"}],"Version":"2012-10-17"}', }, outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', physicalResourceId: { id: 'TestDomainAccessPolicy' }, @@ -48,7 +48,7 @@ test('minimal example renders correctly', () => { service: 'ES', parameters: { DomainName: 'TestDomain', - AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":"*","Resource":"test:arn"}],"Version":"2012-10-17"}', + AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":{"AWS":"*"},"Resource":"test:arn"}],"Version":"2012-10-17"}', }, outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', physicalResourceId: { id: 'TestDomainAccessPolicy' }, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json index 714b07a76c75b..181a2ca93d6fe 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json @@ -244,7 +244,7 @@ { "Ref": "Domain66AC69E0" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain66AC69E0" }, @@ -260,7 +260,7 @@ { "Ref": "Domain66AC69E0" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain66AC69E0" }, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 4644a8905a74b..2f43d85055ad1 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -206,7 +206,7 @@ { "Ref": "Domain19FCBCB91" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain19FCBCB91" }, @@ -222,7 +222,7 @@ { "Ref": "Domain19FCBCB91" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain19FCBCB91" }, @@ -531,7 +531,7 @@ { "Ref": "Domain2644FE48C" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain2644FE48C" }, @@ -547,7 +547,7 @@ { "Ref": "Domain2644FE48C" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain2644FE48C" }, diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json index 35a39a6eea608..7ff999080fed6 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json @@ -108,7 +108,7 @@ { "Ref": "Domain66AC69E0" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"", { "Fn::GetAtt": [ "Domain66AC69E0", @@ -131,7 +131,7 @@ { "Ref": "Domain66AC69E0" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"", { "Fn::GetAtt": [ "Domain66AC69E0", diff --git a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts index c0e7d80a806f0..14ca172de5506 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts @@ -377,9 +377,6 @@ export class PolicyStatement { result[key] = normVal; } } - if (Object.keys(result).length === 1 && result.AWS === '*') { - return '*'; - } return result; } } diff --git a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts index 7c0272a882bbe..c548b1aeb63fd 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts @@ -283,7 +283,7 @@ describe('IAM policy document', () => { expect(stack.resolve(p)).toEqual({ Statement: [ - { Effect: 'Allow', Principal: '*' }, + { Effect: 'Allow', Principal: { AWS: '*' } }, ], Version: '2012-10-17', }); @@ -297,7 +297,7 @@ describe('IAM policy document', () => { expect(stack.resolve(p)).toEqual({ Statement: [ - { Effect: 'Allow', Principal: '*' }, + { Effect: 'Allow', Principal: { AWS: '*' } }, ], Version: '2012-10-17', }); @@ -313,7 +313,7 @@ describe('IAM policy document', () => { expect(stack.resolve(p)).toEqual({ Statement: [ - { Effect: 'Allow', Principal: '*' }, + { Effect: 'Allow', Principal: { AWS: '*' } }, ], Version: '2012-10-17', }); diff --git a/packages/@aws-cdk/aws-kms/test/key.test.ts b/packages/@aws-cdk/aws-kms/test/key.test.ts index f80dce397b4b1..a7144cbb950d7 100644 --- a/packages/@aws-cdk/aws-kms/test/key.test.ts +++ b/packages/@aws-cdk/aws-kms/test/key.test.ts @@ -691,7 +691,7 @@ describe('fromCfnKey()', () => { { Action: 'kms:action', Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: '*', }, ], diff --git a/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts b/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts index 0afb8a731376f..1e5eeb95f28ff 100644 --- a/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts +++ b/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts @@ -15,7 +15,7 @@ test('Via service, any principal', () => { Action: 'abc:call', Condition: { StringEquals: { 'kms:ViaService': 'bla.amazonaws.com' } }, Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: '*', }); }); @@ -38,7 +38,7 @@ test('Via service, principal with conditions', () => { }, }, Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: '*', }); }); diff --git a/packages/@aws-cdk/aws-msk/test/cluster.test.ts b/packages/@aws-cdk/aws-msk/test/cluster.test.ts index 699af1cc29b98..86b15c099f2a5 100644 --- a/packages/@aws-cdk/aws-msk/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-msk/test/cluster.test.ts @@ -279,7 +279,7 @@ describe('MSK Cluster', () => { }, }, Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: '*', }, ], diff --git a/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts b/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts index 3599c1c0e316a..b64ab3d99df43 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts @@ -31,7 +31,7 @@ nodeunitShim({ { 'Action': 's3:GetObject*', 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, }, ], @@ -73,7 +73,7 @@ nodeunitShim({ { 'Action': 's3:GetObject*', 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, }, ], @@ -118,7 +118,7 @@ nodeunitShim({ { 'Action': 's3:GetObject*', 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, }, ], diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 1c90c48957416..d9be67427ab9c 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -306,7 +306,7 @@ describe('bucket', () => { }, }, 'Effect': 'Deny', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': [ { 'Fn::GetAtt': [ @@ -525,7 +525,7 @@ describe('bucket', () => { { 'Action': 'bar:baz', 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': 'foo', }, ], @@ -553,7 +553,7 @@ describe('bucket', () => { expect(stack.resolve(x.toStatementJson())).toEqual({ Action: 's3:ListBucket', Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, }); @@ -574,7 +574,7 @@ describe('bucket', () => { expect(stack.resolve(p.toStatementJson())).toEqual({ Action: 's3:GetObject', Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: { 'Fn::Join': [ '', @@ -605,7 +605,7 @@ describe('bucket', () => { expect(stack.resolve(p.toStatementJson())).toEqual({ Action: 's3:GetObject', Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: { 'Fn::Join': [ '', @@ -670,7 +670,7 @@ describe('bucket', () => { expect(p.toStatementJson()).toEqual({ Action: 's3:ListBucket', Effect: 'Allow', - Principal: '*', + Principal: { AWS: '*' }, Resource: 'arn:aws:s3:::my-bucket', }); @@ -912,7 +912,7 @@ describe('bucket', () => { 'Action': ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], 'Condition': { 'StringEquals': { 'aws:PrincipalOrgID': 'o-1234' } }, 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': [ { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, '/*']] }, @@ -929,7 +929,7 @@ describe('bucket', () => { 'Action': ['kms:Decrypt', 'kms:DescribeKey'], 'Effect': 'Allow', 'Resource': '*', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Condition': { 'StringEquals': { 'aws:PrincipalOrgID': 'o-1234' } }, }, ), @@ -1783,7 +1783,7 @@ describe('bucket', () => { { 'Action': 's3:GetObject', 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['bC3BBCC65', 'Arn'] }, '/*']] }, }, ], @@ -1808,7 +1808,7 @@ describe('bucket', () => { { 'Action': 's3:GetObject', 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['bC3BBCC65', 'Arn'] }, '/only/access/these/*']] }, }, ], @@ -1833,7 +1833,7 @@ describe('bucket', () => { { 'Action': ['s3:GetObject', 's3:PutObject'], 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['bC3BBCC65', 'Arn'] }, '/*']] }, }, ], @@ -1859,7 +1859,7 @@ describe('bucket', () => { { 'Action': 's3:GetObject', 'Effect': 'Allow', - 'Principal': '*', + 'Principal': { AWS: '*' }, 'Resource': { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['bC3BBCC65', 'Arn'] }, '/*']] }, 'Condition': { 'IpAddress': { 'aws:SourceIp': '54.240.143.0/24' }, From af7ad2cd279dce2d355e3678433a84eb5c8334e8 Mon Sep 17 00:00:00 2001 From: Arun Donti Date: Thu, 8 Jul 2021 09:54:04 -0400 Subject: [PATCH 028/105] feat(autoscaling): ScalingEvents.TERMINATION_EVENTS (#15302) Add the option to easily add notifications to only TERMINATION_EVENTS ( ScalingEvent.INSTANCE_TERMINATE, ScalingEvent.INSTANCE_TERMINATE_ERROR ) for ASGs ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 3fa71ecb9a0e9..a57f226208c9c 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -1460,6 +1460,12 @@ export class ScalingEvents { */ public static readonly LAUNCH_EVENTS = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH, ScalingEvent.INSTANCE_LAUNCH_ERROR); + /** + * Fleet termination launch events + */ + public static readonly TERMINATION_EVENTS = new ScalingEvents(ScalingEvent.INSTANCE_TERMINATE, ScalingEvent.INSTANCE_TERMINATE_ERROR); + + /** * @internal */ From c39d0eb5b75a59123c79af05a8043f3bb26e6a1e Mon Sep 17 00:00:00 2001 From: Berend de Boer Date: Fri, 9 Jul 2021 05:06:11 +1200 Subject: [PATCH 029/105] docs(codepipeline-actions): clarify how to use an encrypted existing Bucket with the S3SourceAction (#15310) If you import an S3 bucket encrypted with a custom key, it's impossible to give the source action role permission to use that key for decryption as far as I could see. I.e. I was not able to access this source action role. By adding `encryptionKey` you can specify it at in the S3 source props, and permissions can then be granted inside this private heavy object. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-codepipeline-actions/lib/s3/source-action.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts index 87e046d33cf5a..d165373f2fce5 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts @@ -72,7 +72,10 @@ export interface S3SourceActionProps extends codepipeline.CommonAwsActionProps { readonly trigger?: S3Trigger; /** - * The Amazon S3 bucket that stores the source code + * The Amazon S3 bucket that stores the source code. + * + * If you import an encrypted bucket in your stack, please specify + * the encryption key at import time by using `Bucket.fromBucketAttributes()` method. */ readonly bucket: s3.IBucket; } From adee46c0d3dd128a58b852d9a4c505682423e6b0 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 8 Jul 2021 19:44:52 +0200 Subject: [PATCH 030/105] revert(migration): add constructs migration to rewrite script (#15461) Reverts aws/aws-cdk#14916 This is causing issues in the v2 forward merge. ``` import * as cdk from '@aws-cdk/core'; ``` Is being rewritten as `import * as constructs from 'constructs'`, regardless of what from `cdk` we use. In the case of https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/assert-internal/lib/synth-utils.ts for example, we only use `cdk.Stack`, but the imports turn into: ``` import * as fs from 'fs'; import * as path from 'path'; import * as core from 'aws-cdk-lib'; import * as constructs from 'constructs'; import * as cxapi from 'aws-cdk-lib/cx-api'; ``` And then TSC complains: ``` @aws-cdk/assert: lib/synth-utils.ts(4,1): error TS6133: 'constructs' is declared but its value is never read. @aws-cdk/assert: lib/expect.ts(2,1): error TS6133: 'constructs' is declared but its value is never read. @aws-cdk/assert: jest.ts(2,1): error TS6133: 'constructs' is declared but its value is never read. @aws-cdk/assert: test/assertions.test.ts(2,13): error TS2300: Duplicate identifier 'constructs'. @aws-cdk/assert: test/assertions.test.ts(4,13): error TS2300: Duplicate identifier 'constructs'. ``` (Also note `Duplicate identifier` errors) --- packages/aws-cdk-migration/README.md | 2 +- packages/aws-cdk-migration/lib/rewrite.ts | 288 ++++-------------- .../aws-cdk-migration/test/rewrite.test.ts | 102 +------ 3 files changed, 69 insertions(+), 323 deletions(-) diff --git a/packages/aws-cdk-migration/README.md b/packages/aws-cdk-migration/README.md index 97bbd948a16a0..ba37d609c28db 100644 --- a/packages/aws-cdk-migration/README.md +++ b/packages/aws-cdk-migration/README.md @@ -1,6 +1,6 @@ # aws-cdk-migration -Migrate TypeScript `import` statements from modular CDK (i.e. `@aws-cdk/aws-s3`) to aws-cdk-lib (i.e. `aws-cdk-lib`), as well as imports of `Construct` from `@aws-cdk/core` to `constructs`. +Migrate TypeScript `import` statements from modular CDK (i.e. `@aws-cdk/aws-s3`) to aws-cdk-lib (i.e. `aws-cdk-lib`); Usage: diff --git a/packages/aws-cdk-migration/lib/rewrite.ts b/packages/aws-cdk-migration/lib/rewrite.ts index d678f78253480..f25ae7b6735f2 100644 --- a/packages/aws-cdk-migration/lib/rewrite.ts +++ b/packages/aws-cdk-migration/lib/rewrite.ts @@ -1,19 +1,9 @@ import * as ts from 'typescript'; -interface Import { - location: ts.StringLiteral; - value?: ts.Identifier | ts.NodeArray; -} - -interface Replacement { - original: ts.Node; - updated: string; -} - /** * Re-writes "hyper-modular" CDK imports (most packages in `@aws-cdk/*`) to the * relevant "mono" CDK import path. The re-writing will only modify the imported - * library path, preserving the existing quote style, etc... + * library path, presrving the existing quote style, etc... * * Syntax errors in the source file being processed may cause some import * statements to not be re-written. @@ -23,6 +13,7 @@ interface Replacement { * - `import { Type } from '@aws-cdk/lib';` * - `import '@aws-cdk/lib';` * - `import lib = require('@aws-cdk/lib');` + * - `import { Type } = require('@aws-cdk/lib'); * - `require('@aws-cdk/lib'); * * @param sourceText the source code where imports should be re-written. @@ -33,248 +24,91 @@ interface Replacement { export function rewriteImports(sourceText: string, fileName: string = 'index.ts'): string { const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.ES2018); - const replacements = new Array(); - - let lookForConstruct: { - searchName: string, - replacementName: string, - } | undefined; + const replacements = new Array<{ original: ts.Node, updatedLocation: string }>(); const visitor = (node: T): ts.VisitResult => { - const { location: moduleSpecifier, value: importedValue } = getModuleSpecifier(node) ?? {}; - if (moduleSpecifier) { - lookForConstruct = extractConstructImport(moduleSpecifier, importedValue, node, sourceFile, replacements); - replaceModuleLocation(moduleSpecifier, sourceFile, replacements); - } + const moduleSpecifier = getModuleSpecifier(node); + const newTarget = moduleSpecifier && updatedLocationOf(moduleSpecifier.text); - if (lookForConstruct) { - replaceConstruct(lookForConstruct, node, sourceFile, replacements); + if (moduleSpecifier != null && newTarget != null) { + replacements.push({ original: moduleSpecifier, updatedLocation: newTarget }); } - node.forEachChild(visitor); - - return undefined; + return node; }; - sourceFile.forEachChild(visitor); + sourceFile.statements.forEach(node => ts.visitNode(node, visitor)); - return executeReplacements(sourceFile, replacements); -} + let updatedSourceText = sourceText; + // Applying replacements in reverse order, so node positions remain valid. + for (const replacement of replacements.sort(({ original: l }, { original: r }) => r.getStart(sourceFile) - l.getStart(sourceFile))) { + const prefix = updatedSourceText.substring(0, replacement.original.getStart(sourceFile) + 1); + const suffix = updatedSourceText.substring(replacement.original.getEnd() - 1); -function getModuleSpecifier(node: ts.Node): Import | undefined { - if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { - // import { Type } from 'location'; - // import * as name from 'location'; - const location = node.moduleSpecifier; - if (node.importClause && node.importClause.namedBindings) { - if (ts.isNamespaceImport(node.importClause.namedBindings)) { - return { - location: location, - value: node.importClause.namedBindings.name, - }; - } else if (ts.isNamedImports(node.importClause.namedBindings)) { - return { - location: location, - value: node.importClause.namedBindings.elements, - }; - } - } else { - return { - location: location, - }; - } - } else if ( - ts.isImportEqualsDeclaration(node) - && ts.isExternalModuleReference(node.moduleReference) - && ts.isStringLiteral(node.moduleReference.expression) - ) { - // import name = require('location'); - return { - location: node.moduleReference.expression, - value: node.name, - }; - } else if ( - (ts.isCallExpression(node)) - && ts.isIdentifier(node.expression) - && node.expression.escapedText === 'require' - && node.arguments.length === 1 - ) { - // require('location'); - const argument = node.arguments[0]; - if (ts.isStringLiteral(argument)) { - return { - location: argument, - }; - } + updatedSourceText = prefix + replacement.updatedLocation + suffix; } - return undefined; -} -function extractConstructImport( - moduleSpecifier: ts.StringLiteral, - importedValue: ts.Identifier | ts.NodeArray | undefined, - node: ts.Node, - sourceFile: ts.SourceFile, - replacements: Replacement[], -): { searchName: string, replacementName: string } | undefined { - if (moduleSpecifier.text === '@aws-cdk/core' && importedValue) { - let constructImport: { searchName: string, replacementName: string, newImport: string } | undefined; - if (Array.isArray(importedValue)) { - // import { ..., Construct, ... } from '@aws-cdk/core'; - constructImport = extractBarrelConstructImport(importedValue, sourceFile, replacements); - } else if (ts.isIdentifier(importedValue as ts.Node)) { - // import * as cdk from '@aws-cdk/core'; - constructImport = extractNamespaceConstructImport(importedValue as ts.Identifier, node); - } - if (constructImport) { - addNewConstructImport(constructImport.newImport, node, sourceFile, replacements); - return { - searchName: constructImport.searchName, - replacementName: constructImport.replacementName, - }; - } - } - return undefined; -} + return updatedSourceText; -function extractBarrelConstructImport( - importedNames: ts.NodeArray, - sourceFile: ts.SourceFile, - replacements: Replacement[], -): { searchName: string, replacementName: string, newImport: string } | undefined { - // if the imported name is an alias (`{ Construct as CoreConstruct }`), then `name` holds the alias and `propertyName` holds the original name - // if the imported name is not an alias (`{ Construct }`), then `name` holds the original name and `propertyName` is `undefined` - const constructName = importedNames.find((name) => (name.propertyName ?? name.name).text === 'Construct'); - if (constructName) { - // remove the old import to avoid a name conflict - const constructIndex = importedNames.indexOf(constructName); - let importSpecifierStart = constructName.getStart(sourceFile); - let importSpecifierEnd = constructName.getEnd(); - // remove a leading or trailing comma, if they exist - if (constructIndex > 0) { - importSpecifierStart = importedNames[constructIndex - 1].getEnd(); - } else if (constructIndex < importedNames.length - 1) { - importSpecifierEnd = importedNames[constructIndex + 1].getStart(sourceFile); + function getModuleSpecifier(node: ts.Node): ts.StringLiteral | undefined { + if (ts.isImportDeclaration(node)) { + // import style + const moduleSpecifier = node.moduleSpecifier; + if (ts.isStringLiteral(moduleSpecifier)) { + // import from 'location'; + // import * as name from 'location'; + return moduleSpecifier; + } else if (ts.isBinaryExpression(moduleSpecifier) && ts.isCallExpression(moduleSpecifier.right)) { + // import { Type } = require('location'); + return getModuleSpecifier(moduleSpecifier.right); + } + } else if ( + ts.isImportEqualsDeclaration(node) + && ts.isExternalModuleReference(node.moduleReference) + && ts.isStringLiteral(node.moduleReference.expression) + ) { + // import name = require('location'); + return node.moduleReference.expression; + } else if ( + (ts.isCallExpression(node)) + && ts.isIdentifier(node.expression) + && node.expression.escapedText === 'require' + && node.arguments.length === 1 + ) { + // require('location'); + const argument = node.arguments[0]; + if (ts.isStringLiteral(argument)) { + return argument; + } + } else if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression)) { + // require('location'); // This is an alternate AST version of it + return getModuleSpecifier(node.expression); } - replacements.push({ - original: { - getStart() { - return importSpecifierStart; - }, - getEnd() { - return importSpecifierEnd; - }, - } as ts.Node, - updated: '', - }); - - const aliasStatement = constructName.propertyName ? ` as ${constructName.name.text}` : ''; - return { - searchName: constructName.name.text, - replacementName: constructName.name.text, - newImport: `import { Construct${aliasStatement} } from 'constructs';`, - }; - } - return undefined; -} - -function extractNamespaceConstructImport( - constructNamespace: ts.Identifier, - node: ts.Node, -): { searchName: string, replacementName: string, newImport: string } | undefined { - const searchName = `${(constructNamespace as ts.Identifier).text}.Construct`; - const replacementName = 'constructs.Construct'; - if (ts.isImportDeclaration(node)) { - return { - searchName, - replacementName, - newImport: 'import * as constructs from \'constructs\';', - }; - } else if (ts.isImportEqualsDeclaration(node)) { - return { - searchName, - replacementName, - newImport: 'import constructs = require(\'constructs\');', - }; - } else { return undefined; } } -function addNewConstructImport(newImport: string, node: ts.Node, sourceFile: ts.SourceFile, replacements: Replacement[]) { - // insert a new line and indent - const beginningLinePos = Array.from(sourceFile.getLineStarts()) - .reverse() - .find((start) => start <= node.getStart(sourceFile)) - ?? node.getStart(sourceFile); - const leadingSpaces = node.getStart(sourceFile) - beginningLinePos; - const newImportPrefix = `\n${' '.repeat(leadingSpaces)}`; - - replacements.push({ - original: { - getStart() { - return node.getEnd(); - }, - getEnd() { - return node.getEnd(); - }, - } as ts.Node, - updated: `${newImportPrefix}${newImport}`, - }); -} - -function replaceModuleLocation(moduleSpecifier: ts.StringLiteral, sourceFile: ts.SourceFile, replacements: Replacement[]) { - const newModuleLocation = updatedLocationOf(moduleSpecifier.text); - if (newModuleLocation) { - replacements.push({ - // keep the original quotation marks - original: { - getStart() { - return moduleSpecifier.getStart(sourceFile) + 1; - }, - getEnd() { - return moduleSpecifier.getEnd() - 1; - }, - } as ts.Node, - updated: newModuleLocation, - }); - } -} - -const MODULE_EXEMPTIONS = new Set([ +const EXEMPTIONS = new Set([ '@aws-cdk/cloudformation-diff', - '@aws-cdk/assert', - '@aws-cdk/assert/jest', ]); function updatedLocationOf(modulePath: string): string | undefined { - if (!modulePath.startsWith('@aws-cdk/') || MODULE_EXEMPTIONS.has(modulePath)) { + if (!modulePath.startsWith('@aws-cdk/') || EXEMPTIONS.has(modulePath)) { return undefined; - } else if (modulePath === '@aws-cdk/core') { + } + + if (modulePath === '@aws-cdk/core') { return 'aws-cdk-lib'; - } else { - return `aws-cdk-lib/${modulePath.substring(9)}`; } -} -function replaceConstruct( - { searchName, replacementName }: { searchName: string, replacementName: string }, - node: ts.Node, - sourceFile: ts.SourceFile, - replacements: Replacement[], -) { - if ((ts.isTypeReferenceNode(node) || ts.isPropertyAccessExpression(node)) && node.getText(sourceFile) === searchName) { - replacements.push({ original: node, updated: replacementName }); + // These 2 are unchanged + if (modulePath === '@aws-cdk/assert') { + return '@aws-cdk/assert'; } -} -function executeReplacements(sourceFile: ts.SourceFile, replacements: Replacement[]): string { - let updatedSourceText = sourceFile.getFullText(); - // Applying replacements in reverse order, so node positions remain valid. - for (const replacement of replacements.sort(({ original: l }, { original: r }) => r.getStart(sourceFile) - l.getStart(sourceFile))) { - const prefix = updatedSourceText.substring(0, replacement.original.getStart(sourceFile)); - const suffix = updatedSourceText.substring(replacement.original.getEnd()); - updatedSourceText = prefix + replacement.updated + suffix; + if (modulePath === '@aws-cdk/assert/jest') { + return '@aws-cdk/assert/jest'; } - return updatedSourceText; + + return `aws-cdk-lib/${modulePath.substring(9)}`; } diff --git a/packages/aws-cdk-migration/test/rewrite.test.ts b/packages/aws-cdk-migration/test/rewrite.test.ts index 7f87945081414..f9ff8b247d6ce 100644 --- a/packages/aws-cdk-migration/test/rewrite.test.ts +++ b/packages/aws-cdk-migration/test/rewrite.test.ts @@ -7,7 +7,7 @@ describe(rewriteImports, () => { import '@aws-cdk/aws-s3/hello'; // something after - console.log('Look! I did something!');`, 'subject.ts'); + console.log('Look! I did something!');`, 'subhect.ts'); expect(output).toBe(` // something before @@ -23,7 +23,7 @@ describe(rewriteImports, () => { require('@aws-cdk/aws-s3/hello'); // something after - console.log('Look! I did something!');`, 'subject.ts'); + console.log('Look! I did something!');`, 'subhect.ts'); expect(output).toBe(` // something before @@ -38,7 +38,7 @@ describe(rewriteImports, () => { // something before import * as s3 from '@aws-cdk/aws-s3'; import * as cfndiff from '@aws-cdk/cloudformation-diff'; - import { Stack } from "@aws-cdk/core"; + import { Construct } from "@aws-cdk/core"; // something after console.log('Look! I did something!');`, 'subject.ts'); @@ -47,7 +47,7 @@ describe(rewriteImports, () => { // something before import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cfndiff from '@aws-cdk/cloudformation-diff'; - import { Stack } from "aws-cdk-lib"; + import { Construct } from "aws-cdk-lib"; // something after console.log('Look! I did something!');`); @@ -58,6 +58,7 @@ describe(rewriteImports, () => { // something before import s3 = require('@aws-cdk/aws-s3'); import cfndiff = require('@aws-cdk/cloudformation-diff'); + import { Construct } = require("@aws-cdk/core"); // something after console.log('Look! I did something!');`, 'subject.ts'); @@ -66,6 +67,7 @@ describe(rewriteImports, () => { // something before import s3 = require('aws-cdk-lib/aws-s3'); import cfndiff = require('@aws-cdk/cloudformation-diff'); + import { Construct } = require("aws-cdk-lib"); // something after console.log('Look! I did something!');`); @@ -77,7 +79,7 @@ describe(rewriteImports, () => { import '@aws-cdk/assert/jest'; // something after - console.log('Look! I did something!');`, 'subject.ts'); + console.log('Look! I did something!');`, 'subhect.ts'); expect(output).toBe(` // something before @@ -86,94 +88,4 @@ describe(rewriteImports, () => { console.log('Look! I did something!');`); }); - - test('correctly rewrites import namespaced Construct by moving to constructs', () => { - const output = rewriteImports(` - // something before - import * as cdk from '@aws-cdk/core'; - // something after - - ${constructFileBody('cdk.Construct')} - console.log('Look! I did something!');`, 'subject.ts'); - - expect(output).toBe(` - // something before - import * as cdk from 'aws-cdk-lib'; - import * as constructs from 'constructs'; - // something after - - ${constructFileBody('constructs.Construct')} - console.log('Look! I did something!');`); - }); - - test('correctly rewrites require namespaced Construct by moving to constructs', () => { - const output = rewriteImports(` - // something before - import cdk = require('@aws-cdk/core'); - // something after - - ${constructFileBody('cdk.Construct')} - console.log('Look! I did something!');`, 'subject.ts'); - - expect(output).toBe(` - // something before - import cdk = require('aws-cdk-lib'); - import constructs = require('constructs'); - // something after - - ${constructFileBody('constructs.Construct')} - console.log('Look! I did something!');`); - }); - - test('correctly rewrites barrel Construct by moving to constructs', () => { - const output = rewriteImports(` - // something before - import { Construct, Stack } from '@aws-cdk/core'; - // something after - - ${constructFileBody('Construct')} - console.log('Look! I did something!');`, 'subject.ts'); - - expect(output).toBe(` - // something before - import { Stack } from 'aws-cdk-lib'; - import { Construct } from 'constructs'; - // something after - - ${constructFileBody('Construct')} - console.log('Look! I did something!');`); - }); - - test('correctly rewrites aliased barrel Construct by moving to constructs', () => { - const output = rewriteImports(` - // something before - import { App, Construct as CoreConstruct } from '@aws-cdk/core'; - // something after - - ${constructFileBody('CoreConstruct')} - console.log('Look! I did something!');`, 'subject.ts'); - - expect(output).toBe(` - // something before - import { App } from 'aws-cdk-lib'; - import { Construct as CoreConstruct } from 'constructs'; - // something after - - ${constructFileBody('CoreConstruct')} - console.log('Look! I did something!');`); - }); }); - -function constructFileBody(identifier: string) { - return ` -interface I { - c: ${identifier}; -} -class C { - constructor(c: ${identifier}) {} -} -function f(c: ${identifier}) { - new ${identifier}(c as ${identifier}, 'id') -} -`; -} From bd94be875a8e5942a1a28894658b5ab2c89ce19f Mon Sep 17 00:00:00 2001 From: Niko Virtala Date: Thu, 8 Jul 2021 21:24:35 +0300 Subject: [PATCH 031/105] chore(rds): add missing RDS database engine versions. (#15458) This pull request will add currently missing but supported RDS database engine versions. - PostgreSQL versions: `9.5.24`, `9.5.25`, `9.6.22`, `10.17`, `11.12`, `12.7` and `13.3` - MySQL version: `8.0.25` - MariaDB versions: `10.3.28`, `10.4.18` and `10.5.9` - Oracle versions: Reserve Update Releases (RUR) for `12.1`, `12.2` and `19` - SQL Server versions: `14.00.3356.20.v1` and `15.00.4043.23.v1` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-rds/lib/instance-engine.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index b1d67f65ce2b8..2921d828f37ba 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -245,6 +245,8 @@ export class MariaDbEngineVersion { public static readonly VER_10_2_21 = MariaDbEngineVersion.of('10.2.21', '10.2'); /** Version "10.2.32". */ public static readonly VER_10_2_32 = MariaDbEngineVersion.of('10.2.32', '10.2'); + /** Version "10.2.37". */ + public static readonly VER_10_2_37 = MariaDbEngineVersion.of('10.2.37', '10.2'); /** Version "10.3" (only a major version, without a specific minor version). */ public static readonly VER_10_3 = MariaDbEngineVersion.of('10.3', '10.3'); @@ -256,6 +258,8 @@ export class MariaDbEngineVersion { public static readonly VER_10_3_20 = MariaDbEngineVersion.of('10.3.20', '10.3'); /** Version "10.3.23". */ public static readonly VER_10_3_23 = MariaDbEngineVersion.of('10.3.23', '10.3'); + /** Version "10.3.28". */ + public static readonly VER_10_3_28 = MariaDbEngineVersion.of('10.3.28', '10.3'); /** Version "10.4" (only a major version, without a specific minor version). */ public static readonly VER_10_4 = MariaDbEngineVersion.of('10.4', '10.4'); @@ -263,11 +267,15 @@ export class MariaDbEngineVersion { public static readonly VER_10_4_8 = MariaDbEngineVersion.of('10.4.8', '10.4'); /** Version "10.4.13". */ public static readonly VER_10_4_13 = MariaDbEngineVersion.of('10.4.13', '10.4'); + /** Version "10.4.18". */ + public static readonly VER_10_4_18 = MariaDbEngineVersion.of('10.4.18', '10.4'); /** Version "10.5" (only a major version, without a specific minor version). */ public static readonly VER_10_5 = MariaDbEngineVersion.of('10.5', '10.5'); /** Version "10.5.8". */ public static readonly VER_10_5_8 = MariaDbEngineVersion.of('10.5.8', '10.5'); + /** Version "10.5.9". */ + public static readonly VER_10_5_9 = MariaDbEngineVersion.of('10.5.9', '10.5'); /** * Create a new MariaDbEngineVersion with an arbitrary version. @@ -475,6 +483,8 @@ export class MysqlEngineVersion { public static readonly VER_8_0_21 = MysqlEngineVersion.of('8.0.21', '8.0'); /** Version "8.0.23". */ public static readonly VER_8_0_23 = MysqlEngineVersion.of('8.0.23', '8.0'); + /** Version "8.0.25". */ + public static readonly VER_8_0_25 = MysqlEngineVersion.of('8.0.25', '8.0'); /** * Create a new MysqlEngineVersion with an arbitrary version. @@ -632,6 +642,16 @@ export class PostgresEngineVersion { * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 */ public static readonly VER_9_5_23 = PostgresEngineVersion.of('9.5.23', '9.5'); + /** + * Version "9.5.24". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ + public static readonly VER_9_5_24 = PostgresEngineVersion.of('9.5.24', '9.5'); + /** + * Version "9.5.25". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ + public static readonly VER_9_5_25 = PostgresEngineVersion.of('9.5.25', '9.5'); /** * Version "9.6" (only a major version, without a specific minor version). @@ -728,6 +748,11 @@ export class PostgresEngineVersion { * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 */ public static readonly VER_9_6_21 = PostgresEngineVersion.of('9.6.21', '9.6'); + /** + * Version "9.6.22". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2022 + */ + public static readonly VER_9_6_22 = PostgresEngineVersion.of('9.6.22', '9.6'); /** Version "10" (only a major version, without a specific minor version). */ public static readonly VER_10 = PostgresEngineVersion.of('10', '10'); @@ -759,6 +784,8 @@ export class PostgresEngineVersion { public static readonly VER_10_15 = PostgresEngineVersion.of('10.15', '10', { s3Import: true }); /** Version "10.16". */ public static readonly VER_10_16 = PostgresEngineVersion.of('10.16', '10', { s3Import: true }); + /** Version "10.17". */ + public static readonly VER_10_17 = PostgresEngineVersion.of('10.17', '10', { s3Import: true }); /** Version "11" (only a major version, without a specific minor version). */ public static readonly VER_11 = PostgresEngineVersion.of('11', '11', { s3Import: true }); @@ -782,6 +809,8 @@ export class PostgresEngineVersion { public static readonly VER_11_10 = PostgresEngineVersion.of('11.10', '11', { s3Import: true }); /** Version "11.11". */ public static readonly VER_11_11 = PostgresEngineVersion.of('11.11', '11', { s3Import: true }); + /** Version "11.12". */ + public static readonly VER_11_12 = PostgresEngineVersion.of('11.12', '11', { s3Import: true }); /** Version "12" (only a major version, without a specific minor version). */ public static readonly VER_12 = PostgresEngineVersion.of('12', '12', { s3Import: true }); @@ -795,6 +824,8 @@ export class PostgresEngineVersion { public static readonly VER_12_5 = PostgresEngineVersion.of('12.5', '12', { s3Import: true }); /** Version "12.6". */ public static readonly VER_12_6 = PostgresEngineVersion.of('12.6', '12', { s3Import: true }); + /** Version "12.7". */ + public static readonly VER_12_7 = PostgresEngineVersion.of('12.7', '12', { s3Import: true }); /** Version "13" (only a major version, without a specific minor version). */ public static readonly VER_13 = PostgresEngineVersion.of('13', '13', { s3Import: true }); @@ -802,6 +833,8 @@ export class PostgresEngineVersion { public static readonly VER_13_1 = PostgresEngineVersion.of('13.1', '13', { s3Import: true }); /** Version "13.2". */ public static readonly VER_13_2 = PostgresEngineVersion.of('13.2', '13', { s3Import: true }); + /** Version "13.3". */ + public static readonly VER_13_3 = PostgresEngineVersion.of('13.3', '13', { s3Import: true }); /** * Create a new PostgresEngineVersion with an arbitrary version. @@ -994,6 +1027,12 @@ export class OracleEngineVersion { public static readonly VER_12_1_0_2_V20 = OracleEngineVersion.of('12.1.0.2.v20', '12.1'); /** Version "12.1.0.2.v21". */ public static readonly VER_12_1_0_2_V21 = OracleEngineVersion.of('12.1.0.2.v21', '12.1'); + /** Version "12.1.0.2.v22". */ + public static readonly VER_12_1_0_2_V22 = OracleEngineVersion.of('12.1.0.2.v22', '12.1'); + /** Version "12.1.0.2.v23". */ + public static readonly VER_12_1_0_2_V23 = OracleEngineVersion.of('12.1.0.2.v23', '12.1'); + /** Version "12.1.0.2.v24". */ + public static readonly VER_12_1_0_2_V24 = OracleEngineVersion.of('12.1.0.2.v24', '12.1'); /** Version "12.2" (only a major version, without a specific minor version). */ public static readonly VER_12_2 = OracleEngineVersion.of('12.2', '12.2'); @@ -1013,6 +1052,13 @@ export class OracleEngineVersion { public static readonly VER_12_2_0_1_2020_04_R1 = OracleEngineVersion.of('12.2.0.1.ru-2020-04.rur-2020-04.r1', '12.2'); /** Version "12.2.0.1.ru-2020-07.rur-2020-07.r1". */ public static readonly VER_12_2_0_1_2020_07_R1 = OracleEngineVersion.of('12.2.0.1.ru-2020-07.rur-2020-07.r1', '12.2'); + /** Version "12.2.0.1.ru-2021-10.rur-2020-10.r1". */ + public static readonly VER_12_2_0_1_2020_10_R1 = OracleEngineVersion.of('12.2.0.1.ru-2020-10.rur-2020-10.r1', '12.2'); + /** Version "12.2.0.1.ru-2021-01.rur-2021-01.r1". */ + public static readonly VER_12_2_0_1_2021_01_R1 = OracleEngineVersion.of('12.2.0.1.ru-2021-01.rur-2021-01.r1', '12.2'); + /** Version "12.2.0.1.ru-2021-04.rur-2021-04.r1". */ + public static readonly VER_12_2_0_1_2021_04_R1 = OracleEngineVersion.of('12.2.0.1.ru-2021-04.rur-2021-04.r1', '12.2'); + /** Version "18" (only a major version, without a specific minor version). */ public static readonly VER_18 = OracleEngineVersion.of('18', '18'); @@ -1039,6 +1085,15 @@ export class OracleEngineVersion { public static readonly VER_19_0_0_0_2020_04_R1 = OracleEngineVersion.of('19.0.0.0.ru-2020-04.rur-2020-04.r1', '19'); /** Version "19.0.0.0.ru-2020-07.rur-2020-07.r1". */ public static readonly VER_19_0_0_0_2020_07_R1 = OracleEngineVersion.of('19.0.0.0.ru-2020-07.rur-2020-07.r1', '19'); + /** Version "19.0.0.0.ru-2020-07.rur-2020-10.r1". */ + public static readonly VER_19_0_0_0_2020_10_R1 = OracleEngineVersion.of('19.0.0.0.ru-2020-10.rur-2020-10.r1', '19'); + /** Version "19.0.0.0.ru-2021-01.rur-2021-01.r1". */ + public static readonly VER_19_0_0_0_2021_01_R1 = OracleEngineVersion.of('19.0.0.0.ru-2021-01.rur-2021-01.r1', '19'); + /** Version "19.0.0.0.ru-2021-01.rur-2021-01.r2". */ + public static readonly VER_19_0_0_0_2021_01_R2 = OracleEngineVersion.of('19.0.0.0.ru-2021-01.rur-2021-01.r2', '19'); + /** Version "19.0.0.0.ru-2021-01.rur-2021-04.r1". */ + public static readonly VER_19_0_0_0_2021_04_R1 = OracleEngineVersion.of('19.0.0.0.ru-2021-04.rur-2021-04.r1', '19'); + /** * Creates a new OracleEngineVersion with an arbitrary version. @@ -1286,11 +1341,15 @@ export class SqlServerEngineVersion { public static readonly VER_14_00_3281_6_V1 = SqlServerEngineVersion.of('14.00.3281.6.v1', '14.00'); /** Version "14.00.3294.2.v1". */ public static readonly VER_14_00_3294_2_V1 = SqlServerEngineVersion.of('14.00.3294.2.v1', '14.00'); + /** Version "14.00.3356.20.v1". */ + public static readonly VER_14_00_3356_20_V1 = SqlServerEngineVersion.of('14.00.3356.20.v1', '14.00'); /** Version "15.00" (only a major version, without a specific minor version). */ public static readonly VER_15 = SqlServerEngineVersion.of('15.00', '15.00'); /** Version "15.00.4043.16.v1". */ public static readonly VER_15_00_4043_16_V1 = SqlServerEngineVersion.of('15.00.4043.16.v1', '15.00'); + /** Version "15.00.4043.23.v1". */ + public static readonly VER_15_00_4043_23_V1 = SqlServerEngineVersion.of('15.00.4043.23.v1', '15.00'); /** * Create a new SqlServerEngineVersion with an arbitrary version. From fc01d22d042e34cef391458ffd5a0dd2d290b655 Mon Sep 17 00:00:00 2001 From: Seiya6329 Date: Thu, 8 Jul 2021 15:41:13 -0700 Subject: [PATCH 032/105] feat(appmesh): rename the class HttpRouteMatchMethod to HttpRouteMethod (#15466) Renaming `HttpRouteMatchMethod` to `HttpRouteMethod`. Also move it into its own file since this class is going to be used by multiple classes. BREAKING CHANGE: the class `HttpRouteMatchMethod` has been renamed to `HttpRouteMethod` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 2 +- .../aws-appmesh/lib/http-route-method.ts | 49 +++++++++++++++++ packages/@aws-cdk/aws-appmesh/lib/index.ts | 1 + .../@aws-cdk/aws-appmesh/lib/route-spec.ts | 55 +------------------ .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 2 +- .../@aws-cdk/aws-appmesh/test/test.route.ts | 3 +- 6 files changed, 56 insertions(+), 56 deletions(-) create mode 100644 packages/@aws-cdk/aws-appmesh/lib/http-route-method.ts diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index e2436bed89295..f3b0bdd5aac76 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -433,7 +433,7 @@ router.addRoute('route-http2', { ], match: { prefixPath: '/', - method: appmesh.HttpRouteMatchMethod.POST, + method: appmesh.HttpRouteMethod.POST, protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ // All specified headers must match for the route to match. diff --git a/packages/@aws-cdk/aws-appmesh/lib/http-route-method.ts b/packages/@aws-cdk/aws-appmesh/lib/http-route-method.ts new file mode 100644 index 0000000000000..2dab846e6fda4 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/http-route-method.ts @@ -0,0 +1,49 @@ +/** + * Supported values for matching routes based on the HTTP request method + */ +export enum HttpRouteMethod { + /** + * GET request + */ + GET = 'GET', + + /** + * HEAD request + */ + HEAD = 'HEAD', + + /** + * POST request + */ + POST = 'POST', + + /** + * PUT request + */ + PUT = 'PUT', + + /** + * DELETE request + */ + DELETE = 'DELETE', + + /** + * CONNECT request + */ + CONNECT = 'CONNECT', + + /** + * OPTIONS request + */ + OPTIONS = 'OPTIONS', + + /** + * TRACE request + */ + TRACE = 'TRACE', + + /** + * PATCH request + */ + PATCH = 'PATCH', +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index 1e8e7b2566a3e..424e054f300b8 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -19,3 +19,4 @@ export * from './health-checks'; export * from './listener-tls-options'; export * from './tls-validation'; export * from './tls-client-policy'; +export * from './http-route-method'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index be1da3c67af19..9054e8bd92911 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -1,6 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { CfnRoute } from './appmesh.generated'; -import { HttpTimeout, GrpcTimeout, Protocol, TcpTimeout } from './shared-interfaces'; +import { HttpRouteMethod } from './http-route-method'; +import { GrpcTimeout, HttpTimeout, Protocol, TcpTimeout } from './shared-interfaces'; import { IVirtualNode } from './virtual-node'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -49,7 +50,7 @@ export interface HttpRouteMatch { * * @default - do not match on request method */ - readonly method?: HttpRouteMatchMethod; + readonly method?: HttpRouteMethod; /** * The client request protocol to match on. Applicable only for HTTP2 routes. @@ -59,56 +60,6 @@ export interface HttpRouteMatch { readonly protocol?: HttpRouteProtocol; } -/** - * Supported values for matching routes based on the HTTP request method - */ -export enum HttpRouteMatchMethod { - /** - * GET request - */ - GET = 'GET', - - /** - * HEAD request - */ - HEAD = 'HEAD', - - /** - * POST request - */ - POST = 'POST', - - /** - * PUT request - */ - PUT = 'PUT', - - /** - * DELETE request - */ - DELETE = 'DELETE', - - /** - * CONNECT request - */ - CONNECT = 'CONNECT', - - /** - * OPTIONS request - */ - OPTIONS = 'OPTIONS', - - /** - * TRACE request - */ - TRACE = 'TRACE', - - /** - * PATCH request - */ - PATCH = 'PATCH', -} - /** * Supported :scheme options for HTTP2 */ diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 53a539204ef36..6b0e94fb761ad 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -203,7 +203,7 @@ router.addRoute('route-matching', { weightedTargets: [{ virtualNode: node3 }], match: { prefixPath: '/', - method: appmesh.HttpRouteMatchMethod.POST, + method: appmesh.HttpRouteMethod.POST, protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ appmesh.HttpHeaderMatch.valueIs('Content-Type', 'application/json'), diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index 588c784d7ba71..cab807a4bf0ea 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -1,7 +1,6 @@ import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; - import * as appmesh from '../lib'; export = { @@ -751,7 +750,7 @@ export = { weightedTargets: [{ virtualNode }], match: { prefixPath: '/', - method: appmesh.HttpRouteMatchMethod.GET, + method: appmesh.HttpRouteMethod.GET, }, }), }); From bf656e4bc9fe77caa3e96660d8ba1b7c26c26f87 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 8 Jul 2021 16:33:12 -0700 Subject: [PATCH 033/105] chore(cdk-release): do not get commits if Changelog generation is skipped (#15467) Our build in the Pipeline bump the version to a prerelease one before running the integ tests. We recently switched to using the `cdk-release` tool, and it always fetches the Git history when doing the bump. But that's a problem, as our Pipeline build does not run in a Git repository, and querying the Git history there crashes. Make it so that querying the Git history is skipped when Changelog generation is skipped (as that's when those commits are actually used). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- tools/cdk-release/lib/conventional-commits.ts | 10 +++++++++- tools/cdk-release/lib/index.ts | 2 +- tools/cdk-release/test/changelog.test.ts | 13 ++++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tools/cdk-release/lib/conventional-commits.ts b/tools/cdk-release/lib/conventional-commits.ts index ddf8b82f22050..4ad2b88e33005 100644 --- a/tools/cdk-release/lib/conventional-commits.ts +++ b/tools/cdk-release/lib/conventional-commits.ts @@ -60,7 +60,15 @@ export interface ConventionalCommit { * @param gitTag the string representing the Git tag, * will be used to limit the returned commits to only those added after that tag */ -export async function getConventionalCommitsFromGitHistory(gitTag: string): Promise { +export async function getConventionalCommitsFromGitHistory(args: ReleaseOptions, gitTag: string): Promise { + // Since the commits are needed mainly for the Changelog generation, + // skip getting them if skipChangelog is `true`. + // This is needed to make our build succeed in environments without a Git repository, + // like CodeBuild in CodePipeline + if (args.skip?.changelog) { + return []; + } + const ret = new Array(); return new Promise((resolve, reject) => { const conventionalCommitsStream = gitRawCommits({ diff --git a/tools/cdk-release/lib/index.ts b/tools/cdk-release/lib/index.ts index a770db47868e6..0f72cdda7fe2e 100644 --- a/tools/cdk-release/lib/index.ts +++ b/tools/cdk-release/lib/index.ts @@ -23,7 +23,7 @@ module.exports = async function main(opts: ReleaseOptions): Promise { const currentVersion = packageInfo.version; debug(args, 'Current version is: ' + currentVersion); - const commits = await getConventionalCommitsFromGitHistory(`v${currentVersion}`); + const commits = await getConventionalCommitsFromGitHistory(args, `v${currentVersion}`); const filteredCommits = filterCommits(args, commits); debugObject(args, 'Found and filtered commits', filteredCommits); diff --git a/tools/cdk-release/test/changelog.test.ts b/tools/cdk-release/test/changelog.test.ts index 21fba9b6f2501..7079aee1d2ac8 100644 --- a/tools/cdk-release/test/changelog.test.ts +++ b/tools/cdk-release/test/changelog.test.ts @@ -1,4 +1,4 @@ -import { ConventionalCommit, filterCommits } from '../lib/conventional-commits'; +import { ConventionalCommit, filterCommits, getConventionalCommitsFromGitHistory } from '../lib/conventional-commits'; import { changelog } from '../lib/lifecycles/changelog'; import { ReleaseOptions } from '../lib/types'; @@ -88,6 +88,17 @@ describe('Changelog generation', () => { `); }); + + test('makes it so that no Git commits are queried if Changelog generation is skipped', async () => { + const commits = await getConventionalCommitsFromGitHistory({ + ...args, + skip: { + changelog: true, + }, + }, '3.9.2'); + + expect(commits).toHaveLength(0); + }); }); interface PartialCommit extends Partial { From d88b45eb21bcd051146477e3c97de7dd7b8634d3 Mon Sep 17 00:00:00 2001 From: Seiya6329 Date: Thu, 8 Jul 2021 18:28:30 -0700 Subject: [PATCH 034/105] feat(appmesh): rename the class HttpHeaderMatch to HeaderMatch (#15468) - Renaming the `HttpHeaderMatch` class to `HeaderMatch`. - Adding `validateMatchArrayLength` function to check the number of headers BREAKING CHANGE: the class `HttpHeaderMatch` has been renamed to `HeaderMatch` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 4 +- .../@aws-cdk/aws-appmesh/lib/header-match.ts | 167 +++++++++++++++++ packages/@aws-cdk/aws-appmesh/lib/index.ts | 1 + .../@aws-cdk/aws-appmesh/lib/private/utils.ts | 13 ++ .../@aws-cdk/aws-appmesh/lib/route-spec.ts | 171 +----------------- .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 20 +- .../@aws-cdk/aws-appmesh/test/test.route.ts | 78 +++++++- 7 files changed, 268 insertions(+), 186 deletions(-) create mode 100644 packages/@aws-cdk/aws-appmesh/lib/header-match.ts diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index f3b0bdd5aac76..9b228e77bbb86 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -437,8 +437,8 @@ router.addRoute('route-http2', { protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ // All specified headers must match for the route to match. - appmesh.HttpHeaderMatch.valueIs('Content-Type', 'application/json'), - appmesh.HttpHeaderMatch.valueIsNot('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'application/json'), ] }, }), diff --git a/packages/@aws-cdk/aws-appmesh/lib/header-match.ts b/packages/@aws-cdk/aws-appmesh/lib/header-match.ts new file mode 100644 index 0000000000000..52fe1d59f6391 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/header-match.ts @@ -0,0 +1,167 @@ +import { CfnRoute } from './index'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Configuration for `HeaderMatch` + */ +export interface HeaderMatchConfig { + /** + * Route CFN configuration for the route header match. + */ + readonly headerMatch: CfnRoute.HttpRouteHeaderProperty; +} + +/** + * Used to generate header matching methods. + */ +export abstract class HeaderMatch { + /** + * The value of the header with the given name in the request must match the + * specified value exactly. + * + * @param headerName the name of the header to match against + * @param headerValue The exact value to test against + */ + public static valueIs(headerName: string, headerValue: string): HeaderMatch { + return new HeaderMatchImpl(headerName, false, { exact: headerValue }); + } + + /** + * The value of the header with the given name in the request must not match + * the specified value exactly. + * + * @param headerName the name of the header to match against + * @param headerValue The exact value to test against + */ + public static valueIsNot(headerName: string, headerValue: string): HeaderMatch { + return new HeaderMatchImpl(headerName, true, { exact: headerValue }); + } + + /** + * The value of the header with the given name in the request must start with + * the specified characters. + * + * @param headerName the name of the header to match against + * @param prefix The prefix to test against + */ + public static valueStartsWith(headerName: string, prefix: string): HeaderMatch { + return new HeaderMatchImpl(headerName, false, { prefix }); + } + + /** + * The value of the header with the given name in the request must not start + * with the specified characters. + * + * @param headerName the name of the header to match against + * @param prefix The prefix to test against + */ + public static valueDoesNotStartWith(headerName: string, prefix: string): HeaderMatch { + return new HeaderMatchImpl(headerName, true, { prefix }); + } + + /** + * The value of the header with the given name in the request must end with + * the specified characters. + * + * @param headerName the name of the header to match against + * @param suffix The suffix to test against + */ + public static valueEndsWith(headerName: string, suffix: string): HeaderMatch { + return new HeaderMatchImpl(headerName, false, { suffix }); + } + + /** + * The value of the header with the given name in the request must not end + * with the specified characters. + * + * @param headerName the name of the header to match against + * @param suffix The suffix to test against + */ + public static valueDoesNotEndWith(headerName: string, suffix: string): HeaderMatch { + return new HeaderMatchImpl(headerName, true, { suffix }); + } + + /** + * The value of the header with the given name in the request must include + * the specified characters. + * + * @param headerName the name of the header to match against + * @param regex The regex to test against + */ + public static valueMatchesRegex(headerName: string, regex: string): HeaderMatch { + return new HeaderMatchImpl(headerName, false, { regex }); + } + + /** + * The value of the header with the given name in the request must not + * include the specified characters. + * + * @param headerName the name of the header to match against + * @param regex The regex to test against + */ + public static valueDoesNotMatchRegex(headerName: string, regex: string): HeaderMatch { + return new HeaderMatchImpl(headerName, true, { regex }); + } + + /** + * The value of the header with the given name in the request must be in a + * range of values. + * + * @param headerName the name of the header to match against + * @param start Match on values starting at and including this value + * @param end Match on values up to but not including this value + */ + public static valuesIsInRange(headerName: string, start: number, end: number): HeaderMatch { + return new HeaderMatchImpl(headerName, false, { + range: { + start, + end, + }, + }); + } + + /** + * The value of the header with the given name in the request must not be in + * a range of values. + * + * @param headerName the name of the header to match against + * @param start Match on values starting at and including this value + * @param end Match on values up to but not including this value + */ + public static valuesIsNotInRange(headerName: string, start: number, end: number): HeaderMatch { + return new HeaderMatchImpl(headerName, true, { + range: { + start, + end, + }, + }); + } + + /** + * Returns the header match configuration. + */ + public abstract bind(scope: Construct): HeaderMatchConfig; +} + +class HeaderMatchImpl extends HeaderMatch { + constructor( + private readonly headerName: string, + private readonly invert: boolean, + private readonly matchProperty: CfnRoute.HeaderMatchMethodProperty, + ) { + super(); + } + + bind(_scope: Construct): HeaderMatchConfig { + return { + headerMatch: { + name: this.headerName, + invert: this.invert, + match: this.matchProperty, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index 424e054f300b8..dbdd9d79bb610 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -20,3 +20,4 @@ export * from './listener-tls-options'; export * from './tls-validation'; export * from './tls-client-policy'; export * from './http-route-method'; +export * from './header-match'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts index daa95f18bc410..cb442ac34044f 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -1,5 +1,6 @@ import { Token, TokenComparison } from '@aws-cdk/core'; import { CfnVirtualNode } from '../appmesh.generated'; +import { HeaderMatch } from '../header-match'; import { ListenerTlsOptions } from '../listener-tls-options'; import { TlsClientPolicy } from '../tls-client-policy'; @@ -92,3 +93,15 @@ export function renderMeshOwner(resourceAccount: string, meshAccount: string) : ? meshAccount : undefined; } + +/** + * This is the helper method to validate the length of match array when it is specified. + */ +export function validateMatchArrayLength(headers?: HeaderMatch[]) { + const MIN_LENGTH = 1; + const MAX_LENGTH = 10; + + if (headers && (headers.length < MIN_LENGTH || headers.length > MAX_LENGTH)) { + throw new Error(`Number of headers provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${headers.length}`); + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index 9054e8bd92911..bdaa8a5fc486b 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -1,6 +1,8 @@ import * as cdk from '@aws-cdk/core'; import { CfnRoute } from './appmesh.generated'; +import { HeaderMatch } from './header-match'; import { HttpRouteMethod } from './http-route-method'; +import { validateMatchArrayLength } from './private/utils'; import { GrpcTimeout, HttpTimeout, Protocol, TcpTimeout } from './shared-interfaces'; import { IVirtualNode } from './virtual-node'; @@ -43,7 +45,7 @@ export interface HttpRouteMatch { * * @default - do not match on headers */ - readonly headers?: HttpHeaderMatch[]; + readonly headers?: HeaderMatch[]; /** * The HTTP client request method to match on. @@ -75,168 +77,6 @@ export enum HttpRouteProtocol { HTTPS = 'https', } -/** - * Configuration for `HeaderMatch` - */ -export interface HttpHeaderMatchConfig { - /** - * The HTTP route header. - */ - readonly httpRouteHeader: CfnRoute.HttpRouteHeaderProperty; -} - -/** - * Used to generate header matching methods. - */ -export abstract class HttpHeaderMatch { - /** - * The value of the header with the given name in the request must match the - * specified value exactly. - * - * @param headerName the name of the HTTP header to match against - * @param headerValue The exact value to test against - */ - static valueIs(headerName: string, headerValue: string): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, false, { exact: headerValue }); - } - - /** - * The value of the header with the given name in the request must not match - * the specified value exactly. - * - * @param headerName the name of the HTTP header to match against - * @param headerValue The exact value to test against - */ - static valueIsNot(headerName: string, headerValue: string): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, true, { exact: headerValue }); - } - - /** - * The value of the header with the given name in the request must start with - * the specified characters. - * - * @param headerName the name of the HTTP header to match against - * @param prefix The prefix to test against - */ - static valueStartsWith(headerName: string, prefix: string): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, false, { prefix }); - } - - /** - * The value of the header with the given name in the request must not start - * with the specified characters. - * - * @param headerName the name of the HTTP header to match against - * @param prefix The prefix to test against - */ - static valueDoesNotStartWith(headerName: string, prefix: string): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, true, { prefix }); - } - - /** - * The value of the header with the given name in the request must end with - * the specified characters. - * - * @param headerName the name of the HTTP header to match against - * @param suffix The suffix to test against - */ - static valueEndsWith(headerName: string, suffix: string): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, false, { suffix }); - } - - /** - * The value of the header with the given name in the request must not end - * with the specified characters. - * - * @param headerName the name of the HTTP header to match against - * @param suffix The suffix to test against - */ - static valueDoesNotEndWith(headerName: string, suffix: string): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, true, { suffix }); - } - - /** - * The value of the header with the given name in the request must include - * the specified characters. - * - * @param headerName the name of the HTTP header to match against - * @param regex The regex to test against - */ - static valueMatchesRegex(headerName: string, regex: string): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, false, { regex }); - } - - /** - * The value of the header with the given name in the request must not - * include the specified characters. - * - * @param headerName the name of the HTTP header to match against - * @param regex The regex to test against - */ - static valueDoesNotMatchRegex(headerName: string, regex: string): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, true, { regex }); - } - - /** - * The value of the header with the given name in the request must be in a - * range of values. - * - * @param headerName the name of the HTTP header to match against - * @param start Match on values starting at and including this value - * @param end Match on values up to but not including this value - */ - static valuesIsInRange(headerName: string, start: number, end: number): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, false, { - range: { - start, - end, - }, - }); - } - - /** - * The value of the header with the given name in the request must not be in - * a range of values. - * - * @param headerName the name of the HTTP header to match against - * @param start Match on values starting at and including this value - * @param end Match on values up to but not including this value - */ - static valuesIsNotInRange(headerName: string, start: number, end: number): HttpHeaderMatch { - return new HeaderMatchImpl(headerName, true, { - range: { - start, - end, - }, - }); - } - - /** - * Returns the header match configuration. - */ - abstract bind(scope: Construct): HttpHeaderMatchConfig; -} - -class HeaderMatchImpl extends HttpHeaderMatch { - constructor( - private readonly headerName: string, - private readonly invert: boolean, - private readonly matchProperty: CfnRoute.HeaderMatchMethodProperty, - ) { - super(); - } - - bind(_scope: Construct): HttpHeaderMatchConfig { - return { - httpRouteHeader: { - name: this.headerName, - invert: this.invert, - match: this.matchProperty, - }, - }; - } -} - /** * The criterion for determining a request match for this GatewayRoute */ @@ -580,13 +420,16 @@ class HttpRouteSpec extends RouteSpec { throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); } + const headers = this.match?.headers; + validateMatchArrayLength(headers); + const httpConfig: CfnRoute.HttpRouteProperty = { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), }, match: { prefix: prefixPath, - headers: this.match?.headers?.map(header => header.bind(scope).httpRouteHeader), + headers: headers?.map(header => header.bind(scope).headerMatch), method: this.match?.method, scheme: this.match?.protocol, }, diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 6b0e94fb761ad..45c1a2c4917fe 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -206,16 +206,16 @@ router.addRoute('route-matching', { method: appmesh.HttpRouteMethod.POST, protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ - appmesh.HttpHeaderMatch.valueIs('Content-Type', 'application/json'), - appmesh.HttpHeaderMatch.valueStartsWith('Content-Type', 'application/json'), - appmesh.HttpHeaderMatch.valueEndsWith('Content-Type', 'application/json'), - appmesh.HttpHeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), - appmesh.HttpHeaderMatch.valuesIsInRange('Content-Type', 1, 5), - appmesh.HttpHeaderMatch.valueIsNot('Content-Type', 'application/json'), - appmesh.HttpHeaderMatch.valueDoesNotStartWith('Content-Type', 'application/json'), - appmesh.HttpHeaderMatch.valueDoesNotEndWith('Content-Type', 'application/json'), - appmesh.HttpHeaderMatch.valueDoesNotMatchRegex('Content-Type', 'application/.*'), - appmesh.HttpHeaderMatch.valuesIsNotInRange('Content-Type', 1, 5), + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valuesIsInRange('Content-Type', 1, 5), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valuesIsNotInRange('Content-Type', 1, 5), ], }, }), diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index cab807a4bf0ea..3e268fdbc8bd2 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -637,16 +637,16 @@ export = { match: { prefixPath: '/', headers: [ - appmesh.HttpHeaderMatch.valueIs('Content-Type', 'application/json'), - appmesh.HttpHeaderMatch.valueIsNot('Content-Type', 'text/html'), - appmesh.HttpHeaderMatch.valueStartsWith('Content-Type', 'application/'), - appmesh.HttpHeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), - appmesh.HttpHeaderMatch.valueEndsWith('Content-Type', '/json'), - appmesh.HttpHeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), - appmesh.HttpHeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), - appmesh.HttpHeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), - appmesh.HttpHeaderMatch.valuesIsInRange('Max-Forward', 1, 5), - appmesh.HttpHeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), ], }, }), @@ -812,6 +812,64 @@ export = { test.done(); }, + 'should throw an error with invalid number of headers'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + prefixPath: '/', + // Empty header + headers: [], + }, + }), + }); + }, /Number of headers provided for matching must be between 1 and 10, got: 0/); + + test.throws(() => { + router.addRoute('route2', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + prefixPath: '/', + // 11 headers + headers: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + }); + }, /Number of headers provided for matching must be between 1 and 10, got: 11/); + + test.done(); + }, + 'should allow route priority'(test: Test) { // GIVEN const stack = new cdk.Stack(); From ab72bf611a0ce5ef425f5add667c21cdf68a8aff Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Fri, 9 Jul 2021 16:29:59 +0000 Subject: [PATCH 035/105] chore(release): 1.112.0 --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 389371628e376..0ab22b2ac5d69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,51 @@ 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.112.0](https://github.com/aws/aws-cdk/compare/v1.111.0...v1.112.0) (2021-07-09) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **appmesh:** the class `HttpHeaderMatch` has been renamed to `HeaderMatch` +* **appmesh:** the class `HttpRouteMatchMethod` has been renamed to `HttpRouteMethod` +* **appmesh:** `ServiceDiscovery.cloudMap()` method has been changed to accept positional arguments + +### Features + +* **acm:** DaysToExpiry metric ([#15424](https://github.com/aws/aws-cdk/issues/15424)) ([ff044ed](https://github.com/aws/aws-cdk/commit/ff044eddccfe1e4812e686343ca1a614b73a1a1f)) +* **appmesh:** add support for shared Meshes ([#15353](https://github.com/aws/aws-cdk/issues/15353)) ([6a68873](https://github.com/aws/aws-cdk/commit/6a68873f51545e394efa30cd855aae1fc4484d62)) +* **appmesh:** allow setting the DnsResponseType in DNS ServiceDiscovery ([#15388](https://github.com/aws/aws-cdk/issues/15388)) ([647acfa](https://github.com/aws/aws-cdk/commit/647acfa3fdca6013614dfb9ebf0a2d55ea74e828)) +* **appmesh:** rename the class HttpHeaderMatch to HeaderMatch ([#15468](https://github.com/aws/aws-cdk/issues/15468)) ([d88b45e](https://github.com/aws/aws-cdk/commit/d88b45eb21bcd051146477e3c97de7dd7b8634d3)) +* **appmesh:** rename the class HttpRouteMatchMethod to HttpRouteMethod ([#15466](https://github.com/aws/aws-cdk/issues/15466)) ([fc01d22](https://github.com/aws/aws-cdk/commit/fc01d22d042e34cef391458ffd5a0dd2d290b655)) +* **autoscaling:** ScalingEvents.TERMINATION_EVENTS ([#15302](https://github.com/aws/aws-cdk/issues/15302)) ([af7ad2c](https://github.com/aws/aws-cdk/commit/af7ad2cd279dce2d355e3678433a84eb5c8334e8)) +* **aws-elasticloadbalancingv2:** Allow listing added listeners on application load balancers ([#15259](https://github.com/aws/aws-cdk/issues/15259)) ([a80ad42](https://github.com/aws/aws-cdk/commit/a80ad425a925c7f1b2a550d5605df109ea5a504f)), closes [#11841](https://github.com/aws/aws-cdk/issues/11841) +* **cfnspec:** cloudformation spec v39.3.0 ([#15446](https://github.com/aws/aws-cdk/issues/15446)) ([71c0a4c](https://github.com/aws/aws-cdk/commit/71c0a4c413e77452f47c797d4e861aa542174ce9)) +* **cloudwatch:** allow arbitrary statistics in Metric and Alarm ([#15387](https://github.com/aws/aws-cdk/issues/15387)) ([86a44f9](https://github.com/aws/aws-cdk/commit/86a44f93bf718e20e32f9968ec75bee28dbc2198)) +* **core:** add docker security option to asset bundling ([#15204](https://github.com/aws/aws-cdk/issues/15204)) ([cbee18a](https://github.com/aws/aws-cdk/commit/cbee18acf750319488238dd926ae7b86392c8356)), closes [#14681](https://github.com/aws/aws-cdk/issues/14681) +* **ec2:** add rds-data vpc endpoint ([#15240](https://github.com/aws/aws-cdk/issues/15240)) ([e61a5b8](https://github.com/aws/aws-cdk/commit/e61a5b80fb19270a0ed21938b777390ce5d835cc)) +* **events:** DLQ and retry policy support for BatchJob target ([#15308](https://github.com/aws/aws-cdk/issues/15308)) ([5ecf257](https://github.com/aws/aws-cdk/commit/5ecf2577350da2b9ff7115b2868192bcbd56a56e)), closes [#15238](https://github.com/aws/aws-cdk/issues/15238) +* **pipelines:** Docker registry credentials ([#15364](https://github.com/aws/aws-cdk/issues/15364)) ([e289822](https://github.com/aws/aws-cdk/commit/e289822ebbbdafe3dff7e8822ba2c5cd75ff2d63)), closes [#10999](https://github.com/aws/aws-cdk/issues/10999) [#11774](https://github.com/aws/aws-cdk/issues/11774) + + +### Bug Fixes + +* **autoscaling:** scaling intervals are incorrect if the bottom one does not start at 0 ([#15345](https://github.com/aws/aws-cdk/issues/15345)) ([bf6f7ef](https://github.com/aws/aws-cdk/commit/bf6f7efd01ee3a4dc62124baa969eb5e22e58e52)), closes [#10141](https://github.com/aws/aws-cdk/issues/10141) +* **build:** explicit non-private package not respected in packaging ([#15435](https://github.com/aws/aws-cdk/issues/15435)) ([31e6b1a](https://github.com/aws/aws-cdk/commit/31e6b1a88aaafb28e3916fbba918894435495906)), closes [#15203](https://github.com/aws/aws-cdk/issues/15203) +* **cfnspec:** .npmignore generated by cfnspec does not pass pkglint ([#15409](https://github.com/aws/aws-cdk/issues/15409)) ([c432d48](https://github.com/aws/aws-cdk/commit/c432d481aa9e26bae9092a3084eca55fdc7038ce)), closes [#15064](https://github.com/aws/aws-cdk/issues/15064) +* **cli:** prevent 'Failed resources:' message when no failures and report all progress steps ([#15207](https://github.com/aws/aws-cdk/issues/15207)) ([f3c1b6d](https://github.com/aws/aws-cdk/commit/f3c1b6d29416bdb19828cff9a4facd690c416d5f)) +* **codebuild:** merge spec correctly when using strings ([#15429](https://github.com/aws/aws-cdk/issues/15429)) ([3a65b9c](https://github.com/aws/aws-cdk/commit/3a65b9c6117e9b3c038720262ac0eb6c8033af8f)) +* **events:** Archive event pattern fields are not translated correctly ([#15376](https://github.com/aws/aws-cdk/issues/15376)) ([afa5de1](https://github.com/aws/aws-cdk/commit/afa5de108d393950034c8ff6c5e49d2ec5490c7f)), closes [#14905](https://github.com/aws/aws-cdk/issues/14905) +* **iam:** remove incorrect normalization of principal ([#15248](https://github.com/aws/aws-cdk/issues/15248)) ([850cba0](https://github.com/aws/aws-cdk/commit/850cba0ad206d4e4e47ca48ca50a073880221dcc)), closes [#14274](https://github.com/aws/aws-cdk/issues/14274) [#14274](https://github.com/aws/aws-cdk/issues/14274) +* **iam:** set principalAccount in AccountPrincipal and PrincipalWithConditions ([#15430](https://github.com/aws/aws-cdk/issues/15430)) ([b95ee44](https://github.com/aws/aws-cdk/commit/b95ee4414a632a5b8622c4d6a11bd2baaab536f2)) +* **lambda-nodejs:** pnpm exec args separator order ([#15410](https://github.com/aws/aws-cdk/issues/15410)) ([1d19b3b](https://github.com/aws/aws-cdk/commit/1d19b3b66e7c91ae8d9b4c49432bd0249e18a366)), closes [/github.com/pnpm/pnpm/blob/76136751958ceac0ee77e9a0466b96d4a093a094/packages/plugin-commands-script-runners/src/exec.ts#L73](https://github.com/aws//github.com/pnpm/pnpm/blob/76136751958ceac0ee77e9a0466b96d4a093a094/packages/plugin-commands-script-runners/src/exec.ts/issues/L73) [#15164](https://github.com/aws/aws-cdk/issues/15164) +* **pipelines:** singlePublisherPerType overwrites assets buildspec file of other pipelines ([#15356](https://github.com/aws/aws-cdk/issues/15356)) ([48dd771](https://github.com/aws/aws-cdk/commit/48dd7718d94026e29c0fe6b15c162616840fcabe)) +* **pipelines:** unable to add assets stage to existing VPC pipeline ([#15401](https://github.com/aws/aws-cdk/issues/15401)) ([b010239](https://github.com/aws/aws-cdk/commit/b010239ee0a027c25ec90c5ed8784a36315536d2)), closes [#14343](https://github.com/aws/aws-cdk/issues/14343) + + +### Reverts + +* **migration:** add constructs migration to rewrite script ([#15461](https://github.com/aws/aws-cdk/issues/15461)) ([adee46c](https://github.com/aws/aws-cdk/commit/adee46c0d3dd128a58b852d9a4c505682423e6b0)), closes [aws/aws-cdk#14916](https://github.com/aws/aws-cdk/issues/14916) + ## [1.111.0](https://github.com/aws/aws-cdk/compare/v1.110.1...v1.111.0) (2021-07-01) diff --git a/version.v1.json b/version.v1.json index 31ce6042226ec..7a6e0be682aad 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.111.0" + "version": "1.112.0" } From 45b97154dfeac02d72d01404fbc1446f1fab0f5c Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 9 Jul 2021 09:35:26 -0700 Subject: [PATCH 036/105] Fix bugfix entry for lambda-nodejs --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab22b2ac5d69..a30e9b2c649c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ All notable changes to this project will be documented in this file. See [standa * **events:** Archive event pattern fields are not translated correctly ([#15376](https://github.com/aws/aws-cdk/issues/15376)) ([afa5de1](https://github.com/aws/aws-cdk/commit/afa5de108d393950034c8ff6c5e49d2ec5490c7f)), closes [#14905](https://github.com/aws/aws-cdk/issues/14905) * **iam:** remove incorrect normalization of principal ([#15248](https://github.com/aws/aws-cdk/issues/15248)) ([850cba0](https://github.com/aws/aws-cdk/commit/850cba0ad206d4e4e47ca48ca50a073880221dcc)), closes [#14274](https://github.com/aws/aws-cdk/issues/14274) [#14274](https://github.com/aws/aws-cdk/issues/14274) * **iam:** set principalAccount in AccountPrincipal and PrincipalWithConditions ([#15430](https://github.com/aws/aws-cdk/issues/15430)) ([b95ee44](https://github.com/aws/aws-cdk/commit/b95ee4414a632a5b8622c4d6a11bd2baaab536f2)) -* **lambda-nodejs:** pnpm exec args separator order ([#15410](https://github.com/aws/aws-cdk/issues/15410)) ([1d19b3b](https://github.com/aws/aws-cdk/commit/1d19b3b66e7c91ae8d9b4c49432bd0249e18a366)), closes [/github.com/pnpm/pnpm/blob/76136751958ceac0ee77e9a0466b96d4a093a094/packages/plugin-commands-script-runners/src/exec.ts#L73](https://github.com/aws//github.com/pnpm/pnpm/blob/76136751958ceac0ee77e9a0466b96d4a093a094/packages/plugin-commands-script-runners/src/exec.ts/issues/L73) [#15164](https://github.com/aws/aws-cdk/issues/15164) +* **lambda-nodejs:** pnpm exec args separator order ([#15410](https://github.com/aws/aws-cdk/issues/15410)) ([1d19b3b](https://github.com/aws/aws-cdk/commit/1d19b3b66e7c91ae8d9b4c49432bd0249e18a366)), closes [#15164](https://github.com/aws/aws-cdk/issues/15164) * **pipelines:** singlePublisherPerType overwrites assets buildspec file of other pipelines ([#15356](https://github.com/aws/aws-cdk/issues/15356)) ([48dd771](https://github.com/aws/aws-cdk/commit/48dd7718d94026e29c0fe6b15c162616840fcabe)) * **pipelines:** unable to add assets stage to existing VPC pipeline ([#15401](https://github.com/aws/aws-cdk/issues/15401)) ([b010239](https://github.com/aws/aws-cdk/commit/b010239ee0a027c25ec90c5ed8784a36315536d2)), closes [#14343](https://github.com/aws/aws-cdk/issues/14343) From 23abe22a173281361855efc8671529e4bae30ae5 Mon Sep 17 00:00:00 2001 From: Unnati Parekh <80710604+upparekh@users.noreply.github.com> Date: Fri, 9 Jul 2021 10:09:07 -0700 Subject: [PATCH 037/105] fix(aws-ecs): token is added to Options instead of SecretOptions in SplunkLogDriver (#15408) ---- This PR closes [#7264](https://github.com/aws/aws-cdk/issues/7264). The `token` field of the Splunk log driver populates the `Options` property of the Log Configuration which leads to the secret being resolved to its value on deploying, and then the token is viewable in plain text in the console and may be stored in plain text elsewhere. Thus, we are deprecating the `token` field of the Splunk log driver and are introducing a new `secretToken` field. `secretToken` can be used to provide the Splunk token as a Secrets Manager Secret or a Systems Manager Parameter and will be populated in the `SecretOptions` property of the Log Configuration. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs/README.md | 2 +- .../lib/log-drivers/splunk-log-driver.ts | 60 +++++++--- .../aws-ecs/test/splunk-log-driver.test.ts | 110 ++++++++++++++++++ 3 files changed, 153 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index bb7cc9546ceff..449d67f986ac4 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -611,7 +611,7 @@ taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromRegistry('example-image'), memoryLimitMiB: 256, logging: ecs.LogDrivers.splunk({ - token: cdk.SecretValue.secretsManager('my-splunk-token'), + secretToken: cdk.SecretValue.secretsManager('my-splunk-token'), url: 'my-splunk-url' }) }); diff --git a/packages/@aws-cdk/aws-ecs/lib/log-drivers/splunk-log-driver.ts b/packages/@aws-cdk/aws-ecs/lib/log-drivers/splunk-log-driver.ts index ecb4d9f0dd9c1..c1d3ceac6908f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/log-drivers/splunk-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/lib/log-drivers/splunk-log-driver.ts @@ -1,8 +1,8 @@ import { SecretValue } from '@aws-cdk/core'; -import { ContainerDefinition } from '../container-definition'; +import { ContainerDefinition, Secret } from '../container-definition'; import { BaseLogDriverProps } from './base-log-driver'; import { LogDriver, LogDriverConfig } from './log-driver'; -import { ensureInRange, renderCommonLogDriverOptions, stringifyOptions } from './utils'; +import { ensureInRange, renderCommonLogDriverOptions, renderLogDriverSecretOptions, stringifyOptions } from './utils'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line @@ -25,8 +25,26 @@ export enum SplunkLogFormat { export interface SplunkLogDriverProps extends BaseLogDriverProps { /** * Splunk HTTP Event Collector token. + * + * The splunk-token is added to the Options property of the Log Driver Configuration. So the secret value will be resolved and + * viewable in plain text in the console. + * + * Please provide at least one of `token` or `secretToken`. + * @deprecated Use {@link SplunkLogDriverProps.secretToken} instead. + * @default - token not provided. */ - readonly token: SecretValue; + readonly token?: SecretValue; + + /** + * Splunk HTTP Event Collector token (Secret). + * + * The splunk-token is added to the SecretOptions property of the Log Driver Configuration. So the secret value will not be + * resolved or viewable as plain text. + * + * Please provide at least one of `token` or `secretToken`. + * @default - If secret token is not provided, then the value provided in `token` will be used. + */ + readonly secretToken?: Secret; /** * Path to your Splunk Enterprise, self-service Splunk Cloud instance, or Splunk @@ -121,6 +139,9 @@ export class SplunkLogDriver extends LogDriver { constructor(private readonly props: SplunkLogDriverProps) { super(); + if (!props.token && !props.secretToken) { + throw new Error('Please provide either token or secretToken.'); + } if (props.gzipLevel) { ensureInRange(props.gzipLevel, -1, 9); } @@ -130,23 +151,26 @@ export class SplunkLogDriver extends LogDriver { * Called when the log driver is configured on a container */ public bind(_scope: CoreConstruct, _containerDefinition: ContainerDefinition): LogDriverConfig { + const options = stringifyOptions({ + 'splunk-token': this.props.token, + 'splunk-url': this.props.url, + 'splunk-source': this.props.source, + 'splunk-sourcetype': this.props.sourceType, + 'splunk-index': this.props.index, + 'splunk-capath': this.props.caPath, + 'splunk-caname': this.props.caName, + 'splunk-insecureskipverify': this.props.insecureSkipVerify, + 'splunk-format': this.props.format, + 'splunk-verify-connection': this.props.verifyConnection, + 'splunk-gzip': this.props.gzip, + 'splunk-gzip-level': this.props.gzipLevel, + ...renderCommonLogDriverOptions(this.props), + }); + return { logDriver: 'splunk', - options: stringifyOptions({ - 'splunk-token': this.props.token, - 'splunk-url': this.props.url, - 'splunk-source': this.props.source, - 'splunk-sourcetype': this.props.sourceType, - 'splunk-index': this.props.index, - 'splunk-capath': this.props.caPath, - 'splunk-caname': this.props.caName, - 'splunk-insecureskipverify': this.props.insecureSkipVerify, - 'splunk-format': this.props.format, - 'splunk-verify-connection': this.props.verifyConnection, - 'splunk-gzip': this.props.gzip, - 'splunk-gzip-level': this.props.gzipLevel, - ...renderCommonLogDriverOptions(this.props), - }), + options, + secretOptions: this.props.secretToken && renderLogDriverSecretOptions({ 'splunk-token': this.props.secretToken }, _containerDefinition.taskDefinition), }; } } diff --git a/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts index 2ee8e9e433ce8..ee7d3f4b68ee8 100644 --- a/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts @@ -1,5 +1,7 @@ import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as ssm from '@aws-cdk/aws-ssm'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; @@ -103,4 +105,112 @@ nodeunitShim({ test.done(); }, + + 'create a splunk log driver using secret splunk token from secrets manager'(test: Test) { + const secret = new secretsmanager.Secret(stack, 'Secret'); + // WHEN + td.addContainer('Container', { + image, + logging: ecs.LogDrivers.splunk({ + secretToken: ecs.Secret.fromSecretsManager(secret), + url: 'my-splunk-url', + }), + memoryLimitMiB: 128, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + LogConfiguration: { + LogDriver: 'splunk', + Options: { + 'splunk-url': 'my-splunk-url', + }, + SecretOptions: [ + { + Name: 'splunk-token', + ValueFrom: { + Ref: 'SecretA720EF05', + }, + }, + ], + }, + }, + ], + })); + + test.done(); + }, + + 'create a splunk log driver using secret splunk token from systems manager parameter store'(test: Test) { + const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/token', + version: 1, + }); + // WHEN + td.addContainer('Container', { + image, + logging: ecs.LogDrivers.splunk({ + secretToken: ecs.Secret.fromSsmParameter(parameter), + url: 'my-splunk-url', + }), + memoryLimitMiB: 128, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + LogConfiguration: { + LogDriver: 'splunk', + Options: { + 'splunk-url': 'my-splunk-url', + }, + SecretOptions: [ + { + Name: 'splunk-token', + ValueFrom: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter/token', + ], + ], + }, + }, + ], + }, + }, + ], + })); + + test.done(); + }, + + 'throws when neither token nor secret token are provided'(test: Test) { + test.throws(() => { + td.addContainer('Container', { + image, + logging: ecs.LogDrivers.splunk({ + url: 'my-splunk-url', + }), + memoryLimitMiB: 128, + }); + }, 'Please provide either token or secretToken.'); + + test.done(); + }, }); From 480e371ac1321f78530c3853a046e66840f0b0f2 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 9 Jul 2021 10:54:20 -0700 Subject: [PATCH 038/105] Change 'migration' entry to say 'reverts' instead of 'closes' Co-authored-by: Ben Chaimberg --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a30e9b2c649c1..0b80ca0bd411d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ All notable changes to this project will be documented in this file. See [standa ### Reverts -* **migration:** add constructs migration to rewrite script ([#15461](https://github.com/aws/aws-cdk/issues/15461)) ([adee46c](https://github.com/aws/aws-cdk/commit/adee46c0d3dd128a58b852d9a4c505682423e6b0)), closes [aws/aws-cdk#14916](https://github.com/aws/aws-cdk/issues/14916) +* **migration:** add constructs migration to rewrite script ([#15461](https://github.com/aws/aws-cdk/issues/15461)) ([adee46c](https://github.com/aws/aws-cdk/commit/adee46c0d3dd128a58b852d9a4c505682423e6b0)), reverts [#14916](https://github.com/aws/aws-cdk/issues/14916) ## [1.111.0](https://github.com/aws/aws-cdk/compare/v1.110.1...v1.111.0) (2021-07-01) From 0952f1f2045f2dbdffeb840c4737522d12ea7df6 Mon Sep 17 00:00:00 2001 From: Martin Micunda Date: Fri, 9 Jul 2021 23:03:33 +0100 Subject: [PATCH 039/105] feat(codepipeline-actions): support combining batch build artifacts in CodeBuildAction (#15457) See this [doc](https://docs.aws.amazon.com/codebuild/latest/userguide/sample-pipeline-batch.html) which says > To enable batch builds in CodePipeline, set the BatchEnabled parameter of the configuration object to true. To combine the build artifacts into the same location, set the CombineArtifacts parameter of the configuration object to true. The configuration object it is referring to is [this](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html#cfn-codepipeline-pipeline-stages-actions-configuration). Closes #15455 --- .../aws-codepipeline-actions/README.md | 3 +- .../lib/codebuild/build-action.ts | 14 +++++ .../test/codebuild/codebuild-action.test.ts | 57 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index a462f5082c1ca..e57381c517c1f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -333,7 +333,8 @@ const buildAction = new codepipeline_actions.CodeBuildAction({ project, input: sourceOutput, outputs: [new codepipeline.Artifact()], // optional - executeBatchBuild: true // optional, defaults to false + executeBatchBuild: true, // optional, defaults to false + combineBatchBuildArtifacts: true, // optional, defaults to false }); new codepipeline.Pipeline(this, 'MyPipeline', { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index c825682d16c76..f53351dc533f7 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -102,6 +102,16 @@ export interface CodeBuildActionProps extends codepipeline.CommonAwsActionProps * @default false */ readonly executeBatchBuild?: boolean; + + /** + * Combine the build artifacts for a batch builds. + * + * Enabling this will combine the build artifacts into the same location for batch builds. + * If `executeBatchBuild` is not set to `true`, this property is ignored. + * + * @default false + */ + readonly combineBatchBuildArtifacts?: boolean; } /** @@ -216,6 +226,10 @@ export class CodeBuildAction extends Action { if (this.props.executeBatchBuild) { configuration.BatchEnabled = 'true'; this.props.project.enableBatchBuilds(); + + if (this.props.combineBatchBuildArtifacts) { + configuration.CombineArtifacts = 'true'; + } } return { configuration, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts index b4fec6fc9eb83..09b583c011513 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts @@ -258,6 +258,63 @@ nodeunitShim({ test.done(); }, + 'sets the CombineArtifacts configuration'(test: Test) { + const stack = new Stack(); + + const codeBuildProject = new codebuild.PipelineProject(stack, 'CodeBuild'); + + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.S3SourceAction({ + actionName: 'S3_Source', + bucket: new s3.Bucket(stack, 'Bucket'), + bucketKey: 'key', + output: sourceOutput, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'CodeBuild', + input: sourceOutput, + project: codeBuildProject, + executeBatchBuild: true, + combineBatchBuildArtifacts: true, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { + 'Name': 'Source', + }, + { + 'Name': 'Build', + 'Actions': [ + { + 'Name': 'CodeBuild', + 'Configuration': { + 'BatchEnabled': 'true', + 'CombineArtifacts': 'true', + }, + }, + ], + }, + ], + })); + + test.done(); + }, + 'environment variables': { 'should fail by default when added to a Pipeline while using a secret value in a plaintext variable'(test: Test) { const stack = new Stack(); From 76f06fc2ae4404242e5854d2eeaf4f11b98f98f0 Mon Sep 17 00:00:00 2001 From: Philipp Garbe Date: Sun, 11 Jul 2021 08:48:25 +0200 Subject: [PATCH 040/105] feat(assets): docker images from tar file (#15438) Allows to use an existing tarball for an container image. It loads the image from the tarball instead of building the image from a Dockerfile. Fixes #15419 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecr-assets/README.md | 19 +++ packages/@aws-cdk/aws-ecr-assets/lib/index.ts | 1 + .../aws-ecr-assets/lib/tarball-asset.ts | 85 ++++++++++ .../test/demo-tarball/empty.tar | 0 .../aws-ecr-assets/test/tarball-asset.test.ts | 150 ++++++++++++++++++ packages/@aws-cdk/aws-ecs/README.md | 5 +- .../@aws-cdk/aws-ecs/lib/container-image.ts | 24 ++- 7 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts create mode 100644 packages/@aws-cdk/aws-ecr-assets/test/demo-tarball/empty.tar create mode 100644 packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts diff --git a/packages/@aws-cdk/aws-ecr-assets/README.md b/packages/@aws-cdk/aws-ecr-assets/README.md index 188d0a4ceb96e..2414dc57c084e 100644 --- a/packages/@aws-cdk/aws-ecr-assets/README.md +++ b/packages/@aws-cdk/aws-ecr-assets/README.md @@ -11,6 +11,8 @@ This module allows bundling Docker images as assets. +## Images from Dockerfile + Images are built from a local Docker context directory (with a `Dockerfile`), uploaded to ECR by the CDK toolkit and/or your app's CI-CD pipeline, and can be naturally referenced in your CDK app. @@ -69,6 +71,23 @@ const asset = new DockerImageAsset(this, 'MyBuildImage', { }) ``` +## Images from Tarball + +Images are loaded from a local tarball, uploaded to ECR by the CDK toolkit and/or your app's CI-CD pipeline, and can be +naturally referenced in your CDK app. + +```ts +import { TarballImageAsset } from '@aws-cdk/aws-ecr-assets'; + +const asset = new TarballImageAsset(this, 'MyBuildImage', { + tarballFile: 'local-image.tar' +}); +``` + +This will instruct the toolkit to add the tarball as a file asset. During deployment it will load the container image +from `local-image.tar`, push it to an AWS ECR repository and wire the name of the repository as CloudFormation parameters +to your stack. + ## Publishing images to ECR repositories `DockerImageAsset` is designed for seamless build & consumption of image assets by CDK code deployed to multiple environments diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/index.ts b/packages/@aws-cdk/aws-ecr-assets/lib/index.ts index 579fee533587d..e770bbd197383 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/index.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/index.ts @@ -1 +1,2 @@ export * from './image-asset'; +export * from './tarball-asset'; diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts new file mode 100644 index 0000000000000..48af505e1148e --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts @@ -0,0 +1,85 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as ecr from '@aws-cdk/aws-ecr'; +import { AssetStaging, Stack, Stage } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line +import { IAsset } from '@aws-cdk/assets'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Options for TarballImageAsset + */ +export interface TarballImageAssetProps { + /** + * Path to the tarball. + */ + readonly tarballFile: string; +} + +/** + * An asset that represents a Docker image. + * + * The image will loaded from an existing tarball and uploaded to an ECR repository. + */ +export class TarballImageAsset extends CoreConstruct implements IAsset { + /** + * The full URI of the image (including a tag). Use this reference to pull + * the asset. + */ + public imageUri: string; + + /** + * Repository where the image is stored + */ + public repository: ecr.IRepository; + + /** + * A hash of the source of this asset, which is available at construction time. As this is a plain + * string, it can be used in construct IDs in order to enforce creation of a new resource when + * the content hash has changed. + * @deprecated use assetHash + */ + public readonly sourceHash: string; + + /** + * A hash of this asset, which is available at construction time. As this is a plain string, it + * can be used in construct IDs in order to enforce creation of a new resource when the content + * hash has changed. + */ + public readonly assetHash: string; + + constructor(scope: Construct, id: string, props: TarballImageAssetProps) { + super(scope, id); + + if (!fs.existsSync(props.tarballFile)) { + throw new Error(`Cannot find file at ${props.tarballFile}`); + } + + const stagedTarball = new AssetStaging(scope, 'Staging', { sourcePath: props.tarballFile }); + + this.sourceHash = stagedTarball.assetHash; + this.assetHash = stagedTarball.assetHash; + + const stage = Stage.of(this); + const relativePathInOutDir = stage ? path.relative(stage.assetOutdir, stagedTarball.absoluteStagedPath) : stagedTarball.absoluteStagedPath; + + const stack = Stack.of(this); + const location = stack.synthesizer.addDockerImageAsset({ + sourceHash: stagedTarball.assetHash, + executable: [ + 'sh', + '-c', + `docker load -i ${relativePathInOutDir} | sed "s/Loaded image: //g"`, + ], + }); + + this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName); + this.imageUri = location.imageUri; + } +} + diff --git a/packages/@aws-cdk/aws-ecr-assets/test/demo-tarball/empty.tar b/packages/@aws-cdk/aws-ecr-assets/test/demo-tarball/empty.tar new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts new file mode 100644 index 0000000000000..c4654fed87044 --- /dev/null +++ b/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts @@ -0,0 +1,150 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { expect as ourExpect, haveResource } from '@aws-cdk/assert-internal'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { App, Stack } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; +import { TarballImageAsset } from '../lib'; + +/* eslint-disable quote-props */ + +const flags = { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: true }; + +describe('image asset', () => { + testFutureBehavior('test instantiating Asset Image', flags, App, (app) => { + // GIVEN + const stack = new Stack(app); + const assset = new TarballImageAsset(stack, 'Image', { + tarballFile: __dirname + '/demo-tarball/empty.tar', + }); + + // WHEN + const asm = app.synth(); + + // THEN + const manifestArtifact = getAssetManifest(asm); + const manifest = readAssetManifest(manifestArtifact); + + expect(Object.keys(manifest.files ?? {}).length).toBe(1); + expect(Object.keys(manifest.dockerImages ?? {}).length).toBe(1); + + expect(manifest.dockerImages?.[assset.assetHash]?.destinations?.['current_account-current_region']).toStrictEqual( + { + assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}', + imageTag: assset.assetHash, + repositoryName: 'cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}', + }, + ); + + expect(manifest.dockerImages?.[assset.assetHash]?.source).toStrictEqual( + { + executable: [ + 'sh', + '-c', + `docker load -i asset.${assset.assetHash}.tar | sed "s/Loaded image: //g"`, + ], + }, + ); + }); + + testFutureBehavior('asset.repository.grantPull can be used to grant a principal permissions to use the image', flags, App, (app) => { + // GIVEN + const stack = new Stack(app); + const user = new iam.User(stack, 'MyUser'); + const asset = new TarballImageAsset(stack, 'Image', { + tarballFile: 'test/demo-tarball/empty.tar', + }); + + // WHEN + asset.repository.grantPull(user); + + // THEN + ourExpect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + 'Statement': [ + { + 'Action': [ + 'ecr:BatchCheckLayerAvailability', + 'ecr:GetDownloadUrlForLayer', + 'ecr:BatchGetImage', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':ecr:', + { + 'Ref': 'AWS::Region', + }, + ':', + { + 'Ref': 'AWS::AccountId', + }, + ':repository/', + { + 'Fn::Sub': 'cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}', + }, + ], + ], + }, + }, + { + 'Action': 'ecr:GetAuthorizationToken', + 'Effect': 'Allow', + 'Resource': '*', + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'MyUserDefaultPolicy7B897426', + 'Users': [ + { + 'Ref': 'MyUserDC45028B', + }, + ], + })); + }); + + testFutureBehavior('docker directory is staged if asset staging is enabled', flags, App, (app) => { + const stack = new Stack(app); + const image = new TarballImageAsset(stack, 'MyAsset', { + tarballFile: 'test/demo-tarball/empty.tar', + }); + + const session = app.synth(); + + expect(fs.existsSync(path.join(session.directory, `asset.${image.assetHash}.tar`))).toBeDefined(); + }); + + test('fails if the file does not exist', () => { + const stack = new Stack(); + // THEN + expect(() => { + new TarballImageAsset(stack, 'MyAsset', { + tarballFile: `/does/not/exist/${Math.floor(Math.random() * 9999)}`, + }); + }).toThrow(/Cannot find file at/); + + }); +}); + +function isAssetManifest(x: cxapi.CloudArtifact): x is cxapi.AssetManifestArtifact { + return x instanceof cxapi.AssetManifestArtifact; +} + +function getAssetManifest(asm: cxapi.CloudAssembly): cxapi.AssetManifestArtifact { + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + if (!manifestArtifact) { throw new Error('no asset manifest in assembly'); } + return manifestArtifact; +} + +function readAssetManifest(manifestArtifact: cxapi.AssetManifestArtifact): cxschema.AssetManifest { + return JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); +} diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 449d67f986ac4..7edb8e0584370 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -302,7 +302,7 @@ const taskDefinition = new ecs.TaskDefinition(this, 'TaskDef', { ### Images Images supply the software that runs inside the container. Images can be -obtained from either DockerHub or from ECR repositories, or built directly from a local Dockerfile. +obtained from either DockerHub or from ECR repositories, built directly from a local Dockerfile, or use an existing tarball. - `ecs.ContainerImage.fromRegistry(imageName)`: use a public image. - `ecs.ContainerImage.fromRegistry(imageName, { credentials: mySecret })`: use a private image that requires credentials. @@ -312,7 +312,8 @@ obtained from either DockerHub or from ECR repositories, or built directly from image directly from a `Dockerfile` in your source directory. - `ecs.ContainerImage.fromDockerImageAsset(asset)`: uses an existing `@aws-cdk/aws-ecr-assets.DockerImageAsset` as a container image. -- `new ecs.TagParameterContainerImage(repository)`: use the given ECR repository as the image +- `ecs.ContainerImage.fromTarball(file)`: use an existing tarball. +- `new ecs.TagParameterContainerImage(repository)`: use the given ECR repository as the image but a CloudFormation parameter as the tag. ### Environment variables diff --git a/packages/@aws-cdk/aws-ecs/lib/container-image.ts b/packages/@aws-cdk/aws-ecs/lib/container-image.ts index 52ca23dcb5aed..05b098fdafedd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-image.ts @@ -1,4 +1,5 @@ import * as ecr from '@aws-cdk/aws-ecr'; +import { DockerImageAsset, TarballImageAsset } from '@aws-cdk/aws-ecr-assets'; import { ContainerDefinition } from './container-definition'; import { CfnTaskDefinition } from './ecs.generated'; @@ -52,6 +53,28 @@ export abstract class ContainerImage { }; } + /** + * Use an existing tarball for this container image. + * + * Use this method if the container image has already been created by another process (e.g. jib) + * and you want to add it as a container image asset. + * + * @param tarballFile Path to the tarball (relative to the directory). + */ + public static fromTarball(tarballFile: string): ContainerImage { + return { + bind(scope: CoreConstruct, containerDefinition: ContainerDefinition): ContainerImageConfig { + + const asset = new TarballImageAsset(scope, 'Tarball', { tarballFile }); + asset.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole()); + + return { + imageName: asset.imageUri, + }; + }, + }; + } + /** * Called when the image is used by a ContainerDefinition */ @@ -73,7 +96,6 @@ export interface ContainerImageConfig { readonly repositoryCredentials?: CfnTaskDefinition.RepositoryCredentialsProperty; } -import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets'; import { AssetImage, AssetImageProps } from './images/asset-image'; import { EcrImage } from './images/ecr'; import { RepositoryImage, RepositoryImageProps } from './images/repository'; From c62afe905c0414d2feb38271b0b3df4907ac3caf Mon Sep 17 00:00:00 2001 From: Stephen Date: Mon, 12 Jul 2021 13:12:29 +0100 Subject: [PATCH 041/105] feat(events): cross-region event rules (#14731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request aims to extend the current support for cross-account event targets to also support limited cross-region event targets. Currently, the initial list of supported destination regions is: US East (N. Virginia – us-east-1), US West (Oregon – us-west-2), and Europe (Ireland – eu-west-1). The event can originate in any AWS region. The original feature request is described here: https://github.com/aws/aws-cdk/issues/14635 and the blog post describing this feature launch is here: https://aws.amazon.com/blogs/compute/introducing-cross-region-event-routing-with-amazon-eventbridge/ ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-events-targets/lib/util.ts | 3 +- packages/@aws-cdk/aws-events/README.md | 9 +- packages/@aws-cdk/aws-events/lib/rule.ts | 169 ++++++++---- packages/@aws-cdk/aws-events/lib/util.ts | 12 + .../@aws-cdk/aws-events/test/test.rule.ts | 245 ++++++++++++++++-- 5 files changed, 361 insertions(+), 77 deletions(-) diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts index 6805b7245000f..086c63b4c2224 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/util.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -2,7 +2,7 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sqs from '@aws-cdk/aws-sqs'; -import { Annotations, ConstructNode, IConstruct, Names, Token, TokenComparison, Duration } from '@aws-cdk/core'; +import { Annotations, ConstructNode, IConstruct, Names, Token, TokenComparison, Duration, PhysicalName } from '@aws-cdk/core'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -77,6 +77,7 @@ export function singletonEventRole(scope: IConstruct, policyStatements: iam.Poli if (existing) { return existing; } const role = new iam.Role(scope as Construct, id, { + roleName: PhysicalName.GENERATE_IF_NEEDED, assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), }); diff --git a/packages/@aws-cdk/aws-events/README.md b/packages/@aws-cdk/aws-events/README.md index 584d14df46a2c..0288407ee094f 100644 --- a/packages/@aws-cdk/aws-events/README.md +++ b/packages/@aws-cdk/aws-events/README.md @@ -136,9 +136,9 @@ The following targets are supported: * `targets.BatchJob`: Queue an AWS Batch Job * `targets.AwsApi`: Make an AWS API call -### Cross-account targets +### Cross-account and cross-region targets -It's possible to have the source of the event and a target in separate AWS accounts: +It's possible to have the source of the event and a target in separate AWS accounts and regions: ```ts import { App, Stack } from '@aws-cdk/core'; @@ -148,7 +148,7 @@ import * as targets from '@aws-cdk/aws-events-targets'; const app = new App(); -const stack1 = new Stack(app, 'Stack1', { env: { account: account1, region: 'us-east-1' } }); +const stack1 = new Stack(app, 'Stack1', { env: { account: account1, region: 'us-west-1' } }); const repo = new codecommit.Repository(stack1, 'Repository', { // ... }); @@ -171,9 +171,6 @@ In this situation, the CDK will wire the 2 accounts together: to the event bus of the target account in the given region, and make sure its deployed before the source stack -**Note**: while events can span multiple accounts, they _cannot_ span different regions -(that is an EventBridge, not CDK, limitation). - For more information, see the [AWS documentation on cross-account events](https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-cross-account-event-delivery.html). diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index ada50ddb994fa..08ecb18bbc1d7 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,12 +1,17 @@ -import { App, Lazy, Names, Resource, Stack, Token } from '@aws-cdk/core'; -import { Construct, Node } from 'constructs'; +import { IRole, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { App, IConstruct, IResource, Lazy, Names, Resource, Stack, Token, PhysicalName } from '@aws-cdk/core'; +import { Node, Construct } from 'constructs'; import { IEventBus } from './event-bus'; import { EventPattern } from './event-pattern'; import { CfnEventBusPolicy, CfnRule } from './events.generated'; import { IRule } from './rule-ref'; import { Schedule } from './schedule'; import { IRuleTarget } from './target'; -import { mergeEventPattern, renderEventPattern } from './util'; +import { mergeEventPattern, renderEventPattern, sameEnvDimension } from './util'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Properties for defining an EventBridge Rule @@ -113,7 +118,7 @@ export class Rule extends Resource implements IRule { private readonly eventPattern: EventPattern = { }; private readonly scheduleExpression?: string; private readonly description?: string; - private readonly accountEventBusTargets: { [account: string]: boolean } = {}; + private readonly targetAccounts: {[key: string]: Set} = {}; constructor(scope: Construct, id: string, props: RuleProps = { }) { super(scope, id, { @@ -171,52 +176,76 @@ export class Rule extends Resource implements IRule { if (targetProps.targetResource) { const targetStack = Stack.of(targetProps.targetResource); - const targetAccount = targetStack.account; - const targetRegion = targetStack.region; + + const targetAccount = (targetProps.targetResource as IResource).env?.account || targetStack.account; + const targetRegion = (targetProps.targetResource as IResource).env?.region || targetStack.region; const sourceStack = Stack.of(this); const sourceAccount = sourceStack.account; const sourceRegion = sourceStack.region; - if (targetRegion !== sourceRegion) { - throw new Error('Rule and target must be in the same region'); - } - - if (targetAccount !== sourceAccount) { - // cross-account event - strap in, this works differently than regular events! + // if the target is in a different account or region and is defined in this CDK App + // we can generate all the needed components: + // - forwarding rule in the source stack (target: default event bus of the receiver region) + // - eventbus permissions policy (creating an extra stack) + // - receiver rule in the target stack (target: the actual target) + if (!sameEnvDimension(sourceAccount, targetAccount) || !sameEnvDimension(sourceRegion, targetRegion)) { + // cross-account and/or cross-region event - strap in, this works differently than regular events! // based on: - // https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-cross-account-event-delivery.html + // https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-cross-account.html - // for cross-account events, we require concrete accounts - if (Token.isUnresolved(targetAccount)) { - throw new Error('You need to provide a concrete account for the target stack when using cross-account events'); + // for cross-account or cross-region events, we cannot create new components for an imported resource + // because we don't have the target stack + const isImportedResource = !sameEnvDimension(targetStack.account, targetAccount) || !sameEnvDimension(targetStack.region, targetRegion); //(targetAccount !== targetStack.account) || (targetRegion !== targetStack.region); + if (isImportedResource) { + throw new Error('Cannot create a cross-account or cross-region rule with an imported resource'); + } + + // for cross-account or cross-region events, we require concrete accounts + if (!targetAccount || Token.isUnresolved(targetAccount)) { + throw new Error('You need to provide a concrete account for the target stack when using cross-account or cross-region events'); } if (Token.isUnresolved(sourceAccount)) { - throw new Error('You need to provide a concrete account for the source stack when using cross-account events'); + throw new Error('You need to provide a concrete account for the source stack when using cross-account or cross-region events'); } // and the target region has to be concrete as well - if (Token.isUnresolved(targetRegion)) { - throw new Error('You need to provide a concrete region for the target stack when using cross-account events'); + if (!targetRegion || Token.isUnresolved(targetRegion)) { + throw new Error('You need to provide a concrete region for the target stack when using cross-account or cross-region events'); } // the _actual_ target is just the event bus of the target's account - // make sure we only add it once per account - const exists = this.accountEventBusTargets[targetAccount]; - if (!exists) { - this.accountEventBusTargets[targetAccount] = true; + // make sure we only add it once per account per region + let targetAccountExists = false; + const accountKey = Object.keys(this.targetAccounts).find(account => account === targetAccount); + if (accountKey) { + targetAccountExists = this.targetAccounts[accountKey].has(targetRegion); + } + + if (!targetAccountExists) { + // add the current account-region pair to tracking structure + const regionsSet = this.targetAccounts[targetAccount]; + if (!regionsSet) { + this.targetAccounts[targetAccount] = new Set(); + } + this.targetAccounts[targetAccount].add(targetRegion); + + const eventBusArn = targetStack.formatArn({ + service: 'events', + resource: 'event-bus', + resourceName: 'default', + region: targetRegion, + account: targetAccount, + }); + this.targets.push({ id, - arn: targetStack.formatArn({ - service: 'events', - resource: 'event-bus', - resourceName: 'default', - region: targetRegion, - account: targetAccount, - }), + arn: eventBusArn, + // for cross-region we now require a role with PutEvents permissions + roleArn: roleArn ?? this.singletonEventRole(this, [this.putEventStatement(eventBusArn)]).roleArn, }); } - // Grant the source account permissions to publish events to the event bus of the target account. + // Grant the source account in the source region permissions to publish events to the event bus of the target account in the target region. // Do it in a separate stack instead of the target stack (which seems like the obvious place to put it), // because it needs to be deployed before the rule containing the above event-bus target in the source stack // (EventBridge verifies whether you have permissions to the targets on rule creation), @@ -224,33 +253,37 @@ export class Rule extends Resource implements IRule { // (that's the case with CodePipeline, for example) const sourceApp = this.node.root; if (!sourceApp || !App.isApp(sourceApp)) { - throw new Error('Event stack which uses cross-account targets must be part of a CDK app'); + throw new Error('Event stack which uses cross-account or cross-region targets must be part of a CDK app'); } const targetApp = Node.of(targetProps.targetResource).root; if (!targetApp || !App.isApp(targetApp)) { - throw new Error('Target stack which uses cross-account event targets must be part of a CDK app'); + throw new Error('Target stack which uses cross-account or cross-region event targets must be part of a CDK app'); } if (sourceApp !== targetApp) { throw new Error('Event stack and target stack must belong to the same CDK app'); } - const stackId = `EventBusPolicy-${sourceAccount}-${targetRegion}-${targetAccount}`; - let eventBusPolicyStack: Stack = sourceApp.node.tryFindChild(stackId) as Stack; - if (!eventBusPolicyStack) { - eventBusPolicyStack = new Stack(sourceApp, stackId, { - env: { - account: targetAccount, - region: targetRegion, - }, - stackName: `${targetStack.stackName}-EventBusPolicy-support-${targetRegion}-${sourceAccount}`, - }); - new CfnEventBusPolicy(eventBusPolicyStack, 'GivePermToOtherAccount', { - action: 'events:PutEvents', - statementId: `Allow-account-${sourceAccount}`, - principal: sourceAccount, - }); + + // if different accounts, we need to add the permissions to the target eventbus + if (!sameEnvDimension(sourceAccount, targetAccount)) { + const stackId = `EventBusPolicy-${sourceAccount}-${targetRegion}-${targetAccount}`; + let eventBusPolicyStack: Stack = sourceApp.node.tryFindChild(stackId) as Stack; + if (!eventBusPolicyStack) { + eventBusPolicyStack = new Stack(sourceApp, stackId, { + env: { + account: targetAccount, + region: targetRegion, + }, + stackName: `${targetStack.stackName}-EventBusPolicy-support-${targetRegion}-${sourceAccount}`, + }); + new CfnEventBusPolicy(eventBusPolicyStack, 'GivePermToOtherAccount', { + action: 'events:PutEvents', + statementId: `Allow-account-${sourceAccount}`, + principal: sourceAccount, + }); + } + // deploy the event bus permissions before the source stack + sourceStack.addDependency(eventBusPolicyStack); } - // deploy the event bus permissions before the source stack - sourceStack.addDependency(eventBusPolicyStack); // The actual rule lives in the target stack. // Other than the account, it's identical to this one @@ -258,6 +291,7 @@ export class Rule extends Resource implements IRule { // eventPattern is mutable through addEventPattern(), so we need to lazy evaluate it // but only Tokens can be lazy in the framework, so make a subclass instead const self = this; + class CopyRule extends Rule { public _renderEventPattern(): any { return self._renderEventPattern(); @@ -274,6 +308,7 @@ export class Rule extends Resource implements IRule { protected validate(): string[] { return []; } + } new CopyRule(targetStack, `${Names.uniqueId(this)}-${id}`, { @@ -287,6 +322,10 @@ export class Rule extends Resource implements IRule { } } + // Here only if the target does not have a targetResource defined. + // In such case we don't have to generate any extra component. + // Note that this can also be an imported resource (i.e: EventBus target) + this.targets.push({ id, arn: targetProps.arn, @@ -372,4 +411,34 @@ export class Rule extends Resource implements IRule { return this.targets; } + + /** + * Obtain the Role for the EventBridge event + * + * If a role already exists, it will be returned. This ensures that if multiple + * events have the same target, they will share a role. + * @internal + */ + private singletonEventRole(scope: IConstruct, policyStatements: PolicyStatement[]): IRole { + const id = 'EventsRole'; + const existing = scope.node.tryFindChild(id) as IRole; + if (existing) { return existing; } + + const role = new Role(scope as CoreConstruct, id, { + roleName: PhysicalName.GENERATE_IF_NEEDED, + assumedBy: new ServicePrincipal('events.amazonaws.com'), + }); + + policyStatements.forEach(role.addToPolicy.bind(role)); + + return role; + } + + private putEventStatement(eventBusArn: string) { + return new PolicyStatement({ + actions: ['events:PutEvents'], + resources: [eventBusArn], + }); + } } + diff --git a/packages/@aws-cdk/aws-events/lib/util.ts b/packages/@aws-cdk/aws-events/lib/util.ts index 84ae6713ef119..7c9325f5a94a7 100644 --- a/packages/@aws-cdk/aws-events/lib/util.ts +++ b/packages/@aws-cdk/aws-events/lib/util.ts @@ -1,3 +1,4 @@ +import { Token, TokenComparison } from '@aws-cdk/core'; import { EventPattern } from './event-pattern'; /** @@ -54,6 +55,17 @@ export function mergeEventPattern(dest: any, src: any) { } } +/** + * Whether two string probably contain the same environment dimension (region or account) + * + * Used to compare either accounts or regions, and also returns true if both + * are unresolved (in which case both are expted to be "current region" or "current account"). + * @internal + */ +export function sameEnvDimension(dim1: string, dim2: string) { + return [TokenComparison.SAME, TokenComparison.BOTH_UNRESOLVED].includes(Token.compareStrings(dim1, dim2)); +} + /** * Transform an eventPattern object into a valid Event Rule Pattern * by changing detailType into detail-type when present. diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index b4c1e15057fdf..352ee30132733 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -508,22 +508,6 @@ export = { test.done(); }, - 'rule and target must be in the same region'(test: Test) { - const app = new cdk.App(); - - const sourceStack = new cdk.Stack(app, 'SourceStack'); - const rule = new Rule(sourceStack, 'Rule'); - - const targetStack = new cdk.Stack(app, 'TargetStack', { env: { region: 'us-west-2' } }); - const resource = new cdk.Construct(targetStack, 'Resource'); - - test.throws(() => { - rule.addTarget(new SomeTarget('T', resource)); - }, /Rule and target must be in the same region/); - - test.done(); - }, - 'sqsParameters are generated when they are specified in target props'(test: Test) { const stack = new cdk.Stack(); const t1: IRuleTarget = { @@ -590,7 +574,38 @@ export = { test.done(); }, - 'for cross-account targets': { + 'allow an imported target if is in the same account and region'(test: Test) { + const app = new cdk.App(); + + const sourceAccount = '123456789012'; + const sourceRegion = 'us-west-2'; + const sourceStack = new cdk.Stack(app, 'SourceStack', { env: { account: sourceAccount, region: sourceRegion } }); + const rule = new Rule(sourceStack, 'Rule', { + eventPattern: { + source: ['some-event'], + }, + }); + + const resource = EventBus.fromEventBusArn(sourceStack, 'TargetEventBus', `arn:aws:events:${sourceRegion}:${sourceAccount}:event-bus/default`); + + rule.addTarget(new SomeTarget('T', resource)); + + expect(sourceStack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + 'Arn': 'ARN1', + 'Id': 'T', + 'KinesisParameters': { + 'PartitionKeyPath': 'partitionKeyPath', + }, + }, + ], + })); + + test.done(); + }, + + 'for cross-account and/or cross-region targets': { 'requires that the source stack specify a concrete account'(test: Test) { const app = new cdk.App(); @@ -603,7 +618,7 @@ export = { test.throws(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete account for the source stack when using cross-account events/); + }, /You need to provide a concrete account for the source stack when using cross-account or cross-region events/); test.done(); }, @@ -620,7 +635,7 @@ export = { test.throws(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete account for the target stack when using cross-account events/); + }, /You need to provide a concrete account for the target stack when using cross-account or cross-region events/); test.done(); }, @@ -638,7 +653,197 @@ export = { test.throws(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete region for the target stack when using cross-account events/); + }, /You need to provide a concrete region for the target stack when using cross-account or cross-region events/); + + test.done(); + }, + + 'creates cross-account targets if in the same region'(test: Test) { + const app = new cdk.App(); + + const sourceAccount = '123456789012'; + const sourceRegion = 'eu-west-2'; + const sourceStack = new cdk.Stack(app, 'SourceStack', { env: { account: sourceAccount, region: sourceRegion } }); + const rule = new Rule(sourceStack, 'Rule', { + eventPattern: { + source: ['some-event'], + }, + }); + + const targetAccount = '234567890123'; + const targetRegion = sourceRegion; + const targetStack = new cdk.Stack(app, 'TargetStack', { env: { account: targetAccount, region: targetRegion } }); + const resource = new cdk.Construct(targetStack, 'Resource'); + + rule.addTarget(new SomeTarget('T', resource)); + + expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + 'State': 'ENABLED', + 'Targets': [ + { + 'Id': 'T', + 'Arn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { 'Ref': 'AWS::Partition' }, + `:events:${targetRegion}:${targetAccount}:event-bus/default`, + ], + ], + }, + }, + ], + })); + + expect(targetStack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + 'Arn': 'ARN1', + 'Id': 'T', + 'KinesisParameters': { + 'PartitionKeyPath': 'partitionKeyPath', + }, + }, + ], + })); + + test.done(); + }, + + 'creates cross-region targets'(test: Test) { + const app = new cdk.App(); + + const sourceAccount = '123456789012'; + const sourceRegion = 'us-west-2'; + const sourceStack = new cdk.Stack(app, 'SourceStack', { env: { account: sourceAccount, region: sourceRegion } }); + const rule = new Rule(sourceStack, 'Rule', { + eventPattern: { + source: ['some-event'], + }, + }); + + const targetAccount = '234567890123'; + const targetRegion = 'us-east-1'; + const targetStack = new cdk.Stack(app, 'TargetStack', { env: { account: targetAccount, region: targetRegion } }); + const resource = new cdk.Construct(targetStack, 'Resource'); + + rule.addTarget(new SomeTarget('T', resource)); + + expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + 'State': 'ENABLED', + 'Targets': [ + { + 'Id': 'T', + 'Arn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { 'Ref': 'AWS::Partition' }, + `:events:${targetRegion}:${targetAccount}:event-bus/default`, + ], + ], + }, + }, + ], + })); + + expect(targetStack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + 'Arn': 'ARN1', + 'Id': 'T', + 'KinesisParameters': { + 'PartitionKeyPath': 'partitionKeyPath', + }, + }, + ], + })); + + test.done(); + }, + + 'do not create duplicated targets'(test: Test) { + const app = new cdk.App(); + + const sourceAccount = '123456789012'; + const sourceRegion = 'us-west-2'; + const sourceStack = new cdk.Stack(app, 'SourceStack', { env: { account: sourceAccount, region: sourceRegion } }); + const rule = new Rule(sourceStack, 'Rule', { + eventPattern: { + source: ['some-event'], + }, + }); + + const targetAccount = '234567890123'; + const targetRegion = 'us-east-1'; + const targetStack = new cdk.Stack(app, 'TargetStack', { env: { account: targetAccount, region: targetRegion } }); + const resource = new cdk.Construct(targetStack, 'Resource'); + + rule.addTarget(new SomeTarget('T', resource)); + // same target should be skipped + rule.addTarget(new SomeTarget('T1', resource)); + + expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + 'State': 'ENABLED', + 'Targets': [ + { + 'Id': 'T', + 'Arn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { 'Ref': 'AWS::Partition' }, + `:events:${targetRegion}:${targetAccount}:event-bus/default`, + ], + ], + }, + }, + ], + })); + + expect(sourceStack).notTo(haveResourceLike('AWS::Events::Rule', { + 'State': 'ENABLED', + 'Targets': [ + { + 'Id': 'T1', + 'Arn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { 'Ref': 'AWS::Partition' }, + `:events:${targetRegion}:${targetAccount}:event-bus/default`, + ], + ], + }, + }, + ], + })); + + test.done(); + }, + + 'requires that the target is not imported'(test: Test) { + const app = new cdk.App(); + + const sourceAccount = '123456789012'; + const sourceRegion = 'us-west-2'; + const sourceStack = new cdk.Stack(app, 'SourceStack', { env: { account: sourceAccount, region: sourceRegion } }); + const rule = new Rule(sourceStack, 'Rule', { + eventPattern: { + source: ['some-event'], + }, + }); + + const targetAccount = '123456789012'; + const targetRegion = 'us-west-1'; + const resource = EventBus.fromEventBusArn(sourceStack, 'TargetEventBus', `arn:aws:events:${targetRegion}:${targetAccount}:event-bus/default`); + test.throws(() => { + rule.addTarget(new SomeTarget('T', resource)); + }, /Cannot create a cross-account or cross-region rule with an imported resource/); test.done(); }, From e3ebf9bd3bcdfc6d7f91c4e13d9d7d25932a0aec Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Mon, 12 Jul 2021 12:47:38 +0000 Subject: [PATCH 042/105] chore(release): 1.113.0 --- CHANGELOG.md | 14 ++++++++++++++ version.v1.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b80ca0bd411d..fdc62b8761765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ 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.113.0](https://github.com/aws/aws-cdk/compare/v1.112.0...v1.113.0) (2021-07-12) + + +### Features + +* **assets:** docker images from tar file ([#15438](https://github.com/aws/aws-cdk/issues/15438)) ([76f06fc](https://github.com/aws/aws-cdk/commit/76f06fc2ae4404242e5854d2eeaf4f11b98f98f0)), closes [#15419](https://github.com/aws/aws-cdk/issues/15419) +* **codepipeline-actions:** support combining batch build artifacts in CodeBuildAction ([#15457](https://github.com/aws/aws-cdk/issues/15457)) ([0952f1f](https://github.com/aws/aws-cdk/commit/0952f1f2045f2dbdffeb840c4737522d12ea7df6)), closes [#15455](https://github.com/aws/aws-cdk/issues/15455) +* **events:** cross-region event rules ([#14731](https://github.com/aws/aws-cdk/issues/14731)) ([c62afe9](https://github.com/aws/aws-cdk/commit/c62afe905c0414d2feb38271b0b3df4907ac3caf)) + + +### Bug Fixes + +* **aws-ecs:** token is added to Options instead of SecretOptions in SplunkLogDriver ([#15408](https://github.com/aws/aws-cdk/issues/15408)) ([23abe22](https://github.com/aws/aws-cdk/commit/23abe22a173281361855efc8671529e4bae30ae5)) + ## [1.112.0](https://github.com/aws/aws-cdk/compare/v1.111.0...v1.112.0) (2021-07-09) diff --git a/version.v1.json b/version.v1.json index 7a6e0be682aad..edaba2d69702e 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.112.0" + "version": "1.113.0" } From eeeec5d14aa03dbaeeb08fc664c26e82a447f7da Mon Sep 17 00:00:00 2001 From: Seiya6329 Date: Mon, 12 Jul 2021 18:42:08 -0700 Subject: [PATCH 043/105] feat(appmesh): add Route matching on path, query parameters, metadata, and method name (#15470) Adding new match properties for `Route`. - For HTTP match, adding `path` and `queryParameters`. Remove `prefixPath`. - For gRPC match, adding `metadata` and `method name` BREAKING CHANGE: `prefixPath` property in `HttpRouteMatch` has been renamed to `path`, and its type changed from `string` to `HttpRoutePathMatch` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 52 +- .../aws-appmesh/lib/http-route-path-match.ts | 97 ++++ packages/@aws-cdk/aws-appmesh/lib/index.ts | 2 + .../@aws-cdk/aws-appmesh/lib/private/utils.ts | 31 +- .../aws-appmesh/lib/query-parameter-match.ts | 54 ++ .../@aws-cdk/aws-appmesh/lib/route-spec.ts | 87 +++- .../aws-appmesh/test/integ.mesh.expected.json | 96 ++++ .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 41 +- .../@aws-cdk/aws-appmesh/test/test.route.ts | 472 +++++++++++++++++- .../aws-appmesh/test/test.virtual-router.ts | 9 +- 10 files changed, 893 insertions(+), 48 deletions(-) create mode 100644 packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts create mode 100644 packages/@aws-cdk/aws-appmesh/lib/query-parameter-match.ts diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 9b228e77bbb86..af8d876f1fea3 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -406,6 +406,18 @@ A `route` is associated with a virtual router, and it's used to match requests f If your `route` matches a request, you can distribute traffic to one or more target virtual nodes with relative weighting. +The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs. +The `tcp()`, `http()`, `http2()`, and `grpc()` methods provide the spec necessary to define a protocol specific spec. + +For HTTP based routes, the match field can be used to match on +path (prefix, exact, or regex), HTTP method, scheme, HTTP headers, and query parameters. +By default, an HTTP based route will match all requests. + +For gRPC based routes, the match field can be used to match on service name, method name, and metadata. +When specifying the method name, service name must also be specified. + +For example, here's how to add an HTTP route that matches based on a prefix of the URL path: + ```ts router.addRoute('route-http', { routeSpec: appmesh.RouteSpec.http({ @@ -415,13 +427,14 @@ router.addRoute('route-http', { }, ], match: { - prefixPath: '/path-to-app', + // Path that is passed to this method must start with '/'. + path: appmesh.HttpRoutePathMatch.startsWith('/path-to-app'), }, }), }); ``` -Add an HTTP2 route that matches based on method, scheme and header: +Add an HTTP2 route that matches based on exact path, method, scheme, headers, and query parameters: ```ts router.addRoute('route-http2', { @@ -432,14 +445,18 @@ router.addRoute('route-http2', { }, ], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.exactly('/exact'), method: appmesh.HttpRouteMethod.POST, protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ // All specified headers must match for the route to match. appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), appmesh.HeaderMatch.valueIsNot('Content-Type', 'application/json'), - ] + ], + queryParameters: [ + // All specified query parameters must match for the route to match. + appmesh.QueryParameterMatch.valueIs('query-field', 'value') + ], }, }), }); @@ -461,7 +478,7 @@ router.addRoute('route-http', { }, ], match: { - prefixPath: '/path-to-app', + path: appmesh.HttpRoutePathMatch.startsWith('/path-to-app'), }, }), }); @@ -511,12 +528,27 @@ router.addRoute('route-grpc-retry', { }); ``` -The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs. -The `tcp()`, `http()` and `http2()` methods provide the spec necessary to define a protocol specific spec. +Add an gRPC route that matches based on method name and metadata: -For HTTP based routes, the match field can be used to match on a route prefix. -By default, an HTTP based route will match on `/`. All matches must start with a leading `/`. -The timeout field can also be specified for `idle` and `perRequest` timeouts. +```ts +router.addRoute('route-grpc-retry', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode: node }], + match: { + // When method name is specified, service name must be also specified. + methodName: 'methodname', + serviceName: 'servicename', + metadata: [ + // All specified metadata must match for the route to match. + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + ], + }, + }), +}); +``` + +Add a gRPC route with time out: ```ts router.addRoute('route-http', { diff --git a/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts b/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts new file mode 100644 index 0000000000000..819cc3821b429 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts @@ -0,0 +1,97 @@ +import { CfnRoute } from './appmesh.generated'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * The type returned from the `bind()` method in {@link HttpRoutePathMatch}. + */ +export interface HttpRoutePathMatchConfig { + /** + * Route configuration for matching on the complete URL path of the request. + * + * @default - no matching will be performed on the complete URL path + */ + readonly wholePathMatch?: CfnRoute.HttpPathMatchProperty; + + /** + * Route configuration for matching on the prefix of the URL path of the request. + * + * @default - no matching will be performed on the prefix of the URL path + */ + readonly prefixPathMatch?: string; +} + +/** + * Defines HTTP route matching based on the URL path of the request. + */ +export abstract class HttpRoutePathMatch { + /** + * The value of the path must match the specified value exactly. + * The provided `path` must start with the '/' character. + * + * @param path the exact path to match on + */ + public static exactly(path: string): HttpRoutePathMatch { + return new HttpRouteWholePathMatch({ exact: path }); + } + + /** + * The value of the path must match the specified regex. + * + * @param regex the regex used to match the path + */ + public static regex(regex: string): HttpRoutePathMatch { + return new HttpRouteWholePathMatch({ regex: regex }); + } + + /** + * The value of the path must match the specified prefix. + * + * @param prefix the value to use to match the beginning of the path part of the URL of the request. + * It must start with the '/' character. If provided as "/", matches all requests. + * For example, if your virtual service name is "my-service.local" + * and you want the route to match requests to "my-service.local/metrics", your prefix should be "/metrics". + */ + public static startsWith(prefix: string): HttpRoutePathMatch { + return new HttpRoutePrefixPathMatch(prefix); + } + + /** + * Returns the route path match configuration. + */ + public abstract bind(scope: Construct): HttpRoutePathMatchConfig; +} + +class HttpRoutePrefixPathMatch extends HttpRoutePathMatch { + constructor(private readonly prefix: string) { + super(); + + if (this.prefix && this.prefix[0] !== '/') { + throw new Error(`Prefix Path for the match must start with \'/\', got: ${this.prefix}`); + } + } + + bind(_scope: Construct): HttpRoutePathMatchConfig { + return { + prefixPathMatch: this.prefix, + }; + } +} + +class HttpRouteWholePathMatch extends HttpRoutePathMatch { + constructor(private readonly match: CfnRoute.HttpPathMatchProperty) { + super(); + + if (this.match.exact && this.match.exact[0] !== '/') { + throw new Error(`Exact Path for the match must start with \'/\', got: ${this.match.exact}`); + } + } + + bind(_scope: Construct): HttpRoutePathMatchConfig { + return { + wholePathMatch: this.match, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index dbdd9d79bb610..d5ea8fedb70d6 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -21,3 +21,5 @@ export * from './tls-validation'; export * from './tls-client-policy'; export * from './http-route-method'; export * from './header-match'; +export * from './query-parameter-match'; +export * from './http-route-path-match'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts index cb442ac34044f..6f07827aa78ff 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -2,6 +2,8 @@ import { Token, TokenComparison } from '@aws-cdk/core'; import { CfnVirtualNode } from '../appmesh.generated'; import { HeaderMatch } from '../header-match'; import { ListenerTlsOptions } from '../listener-tls-options'; +import { QueryParameterMatch } from '../query-parameter-match'; +import { GrpcRouteMatch } from '../route-spec'; import { TlsClientPolicy } from '../tls-client-policy'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -95,13 +97,38 @@ export function renderMeshOwner(resourceAccount: string, meshAccount: string) : } /** - * This is the helper method to validate the length of match array when it is specified. + * This is the helper method to validate the length of HTTP match array when it is specified. */ -export function validateMatchArrayLength(headers?: HeaderMatch[]) { +export function validateHttpMatchArrayLength(headers?: HeaderMatch[], queryParameters?: QueryParameterMatch[]) { const MIN_LENGTH = 1; const MAX_LENGTH = 10; if (headers && (headers.length < MIN_LENGTH || headers.length > MAX_LENGTH)) { throw new Error(`Number of headers provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${headers.length}`); } + + if (queryParameters && (queryParameters.length < MIN_LENGTH || queryParameters.length > MAX_LENGTH)) { + throw new Error(`Number of query parameters provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${queryParameters.length}`); + } +} + +/** + * This is the helper method to validate the length of gRPC match array when it is specified. + */ +export function validateGrpcMatchArrayLength(metadata?: HeaderMatch[]): void { + const MIN_LENGTH = 1; + const MAX_LENGTH = 10; + + if (metadata && (metadata.length < MIN_LENGTH || metadata.length > MAX_LENGTH)) { + throw new Error(`Number of metadata provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${metadata.length}`); + } +} + +/** + * This is the helper method to validate at least one of gRPC match type is defined. + */ +export function validateGrpcMatch(match: GrpcRouteMatch): void { + if (match.serviceName === undefined && match.metadata === undefined && match.methodName === undefined) { + throw new Error('At least one gRPC match property must be provided'); + } } diff --git a/packages/@aws-cdk/aws-appmesh/lib/query-parameter-match.ts b/packages/@aws-cdk/aws-appmesh/lib/query-parameter-match.ts new file mode 100644 index 0000000000000..585d810cef051 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/query-parameter-match.ts @@ -0,0 +1,54 @@ +import { CfnRoute } from './appmesh.generated'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Configuration for `QueryParameterMatch` + */ +export interface QueryParameterMatchConfig { + /** + * Route CFN configuration for route query parameter match. + */ + readonly queryParameterMatch: CfnRoute.QueryParameterProperty; +} + +/** + * Used to generate query parameter matching methods. + */ +export abstract class QueryParameterMatch { + /** + * The value of the query parameter with the given name in the request must match the + * specified value exactly. + * + * @param queryParameterName the name of the query parameter to match against + * @param queryParameterValue The exact value to test against + */ + static valueIs(queryParameterName: string, queryParameterValue: string): QueryParameterMatch { + return new QueryParameterMatchImpl(queryParameterName, { exact: queryParameterValue }); + } + + /** + * Returns the query parameter match configuration. + */ + public abstract bind(scope: Construct): QueryParameterMatchConfig; +} + +class QueryParameterMatchImpl extends QueryParameterMatch { + constructor( + private readonly queryParameterName: string, + private readonly matchProperty: CfnRoute.HttpQueryParameterMatchProperty, + ) { + super(); + } + + bind(_scope: Construct): QueryParameterMatchConfig { + return { + queryParameterMatch: { + match: this.matchProperty, + name: this.queryParameterName, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index bdaa8a5fc486b..0ff6bb8d88af1 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -2,7 +2,9 @@ import * as cdk from '@aws-cdk/core'; import { CfnRoute } from './appmesh.generated'; import { HeaderMatch } from './header-match'; import { HttpRouteMethod } from './http-route-method'; -import { validateMatchArrayLength } from './private/utils'; +import { HttpRoutePathMatch } from './http-route-path-match'; +import { validateGrpcMatch, validateGrpcMatchArrayLength, validateHttpMatchArrayLength } from './private/utils'; +import { QueryParameterMatch } from './query-parameter-match'; import { GrpcTimeout, HttpTimeout, Protocol, TcpTimeout } from './shared-interfaces'; import { IVirtualNode } from './virtual-node'; @@ -28,16 +30,15 @@ export interface WeightedTarget { } /** - * The criterion for determining a request match for this GatewayRoute + * The criterion for determining a request match for this Route */ export interface HttpRouteMatch { /** - * Specifies the path to match requests with. - * This parameter must always start with /, which by itself matches all requests to the virtual service name. - * You can also match for path-based routing of requests. For example, if your virtual service name is my-service.local - * and you want the route to match requests to my-service.local/metrics, your prefix should be /metrics. + * Specifies how is the request matched based on the path part of its URL. + * + * @default - matches requests with all paths */ - readonly prefixPath: string; + readonly path?: HttpRoutePathMatch; /** * Specifies the client request headers to match on. All specified headers @@ -60,6 +61,14 @@ export interface HttpRouteMatch { * @default - do not match on HTTP2 request protocol */ readonly protocol?: HttpRouteProtocol; + + /** + * The query parameters to match on. + * All specified query parameters must match for the route to match. + * + * @default - do not match on query parameters + */ + readonly queryParameters?: QueryParameterMatch[]; } /** @@ -78,13 +87,32 @@ export enum HttpRouteProtocol { } /** - * The criterion for determining a request match for this GatewayRoute + * The criterion for determining a request match for this Route. + * At least one match type must be selected. */ export interface GrpcRouteMatch { /** - * The fully qualified domain name for the service to match from the request + * Create service name based gRPC route match. + * + * @default - do not match on service name + */ + readonly serviceName?: string; + + /** + * Create metadata based gRPC route match. + * All specified metadata must match for the route to match. + * + * @default - do not match on metadata */ - readonly serviceName: string; + readonly metadata?: HeaderMatch[]; + + /** + * The method name to match from the request. + * If the method name is specified, service name must be also provided. + * + * @default - do not match on method name + */ + readonly methodName?: string; } /** @@ -297,7 +325,7 @@ export enum GrpcRetryEvent { } /** - * All Properties for GatewayRoute Specs + * All Properties for Route Specs */ export interface RouteSpecConfig { /** @@ -371,7 +399,7 @@ export abstract class RouteSpec { } /** - * Called when the GatewayRouteSpec type is initialized. Can be used to enforce + * Called when the RouteSpec type is initialized. Can be used to enforce * mutual exclusivity with future properties */ public abstract bind(scope: Construct): RouteSpecConfig; @@ -415,23 +443,25 @@ class HttpRouteSpec extends RouteSpec { } public bind(scope: Construct): RouteSpecConfig { - const prefixPath = this.match ? this.match.prefixPath : '/'; - if (prefixPath[0] != '/') { - throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); - } + const pathMatchConfig = (this.match?.path ?? HttpRoutePathMatch.startsWith('/')).bind(scope); + // Set prefix path match to '/' if none of path matches are defined. const headers = this.match?.headers; - validateMatchArrayLength(headers); + const queryParameters = this.match?.queryParameters; + + validateHttpMatchArrayLength(headers, queryParameters); const httpConfig: CfnRoute.HttpRouteProperty = { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), }, match: { - prefix: prefixPath, + prefix: pathMatchConfig.prefixPathMatch, + path: pathMatchConfig.wholePathMatch, headers: headers?.map(header => header.bind(scope).headerMatch), method: this.match?.method, scheme: this.match?.protocol, + queryParameters: queryParameters?.map(queryParameter => queryParameter.bind(scope).queryParameterMatch), }, timeout: renderTimeout(this.timeout), retryPolicy: this.retryPolicy ? renderHttpRetryPolicy(this.retryPolicy) : undefined, @@ -520,7 +550,18 @@ class GrpcRouteSpec extends RouteSpec { } } - public bind(_scope: Construct): RouteSpecConfig { + public bind(scope: Construct): RouteSpecConfig { + const serviceName = this.match.serviceName; + const methodName = this.match.methodName; + const metadata = this.match.metadata; + + validateGrpcMatch(this.match); + validateGrpcMatchArrayLength(metadata); + + if (methodName && !serviceName) { + throw new Error('If you specify a method name, you must also specify a service name'); + } + return { priority: this.priority, grpcRouteSpec: { @@ -528,7 +569,9 @@ class GrpcRouteSpec extends RouteSpec { weightedTargets: renderWeightedTargets(this.weightedTargets), }, match: { - serviceName: this.match.serviceName, + serviceName: serviceName, + methodName: methodName, + metadata: metadata?.map(singleMetadata => singleMetadata.bind(scope).headerMatch), }, timeout: renderTimeout(this.timeout), retryPolicy: this.retryPolicy ? renderGrpcRetryPolicy(this.retryPolicy) : undefined, @@ -538,8 +581,8 @@ class GrpcRouteSpec extends RouteSpec { } /** -* Utility method to add weighted route targets to an existing route -*/ + * Utility method to add weighted route targets to an existing route + */ function renderWeightedTargets(weightedTargets: WeightedTarget[]): CfnRoute.WeightedTargetProperty[] { const renderedTargets: CfnRoute.WeightedTargetProperty[] = []; for (const t of weightedTargets) { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index 3744fe7583cf1..faafaa4f96ea6 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -931,6 +931,102 @@ "RouteName": "route-grpc-retry" } }, + "meshrouterroute699804AE1": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode2092BA426", + "VirtualNodeName" + ] + }, + "Weight": 30 + } + ] + }, + "Match": { + "Path": { + "Regex": "regex" + }, + "QueryParameters": [ + { + "Match": { + "Exact": "value" + }, + "Name": "query-field" + } + ] + } + } + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-6" + } + }, + "meshrouterroute76C21E6E7": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "GrpcRoute": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode4AE87F692", + "VirtualNodeName" + ] + }, + "Weight": 20 + } + ] + }, + "Match": { + "Metadata": [ + { + "Invert": false, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + } + ], + "MethodName": "test-method", + "ServiceName": "test-service" + } + } + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-7" + } + }, "meshnode726C787D": { "Type": "AWS::AppMesh::VirtualNode", "Properties": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 45c1a2c4917fe..41a047415f5dc 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -55,7 +55,7 @@ router.addRoute('route-1', { }, ], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), }, timeout: { idle: cdk.Duration.seconds(10), @@ -158,7 +158,7 @@ router.addRoute('route-2', { }, ], match: { - prefixPath: '/path2', + path: appmesh.HttpRoutePathMatch.startsWith('/path2'), }, timeout: { idle: cdk.Duration.seconds(11), @@ -202,7 +202,7 @@ router.addRoute('route-matching', { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode: node3 }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), method: appmesh.HttpRouteMethod.POST, protocol: appmesh.HttpRouteProtocol.HTTPS, headers: [ @@ -254,6 +254,41 @@ router.addRoute('route-grpc-retry', { }), }); +router.addRoute('route-6', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [ + { + virtualNode: node2, + weight: 30, + }, + ], + match: { + path: appmesh.HttpRoutePathMatch.regex('regex'), + queryParameters: [ + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + ], + }, + }), +}); + +router.addRoute('route-7', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [ + { + virtualNode: node4, + weight: 20, + }, + ], + match: { + serviceName: 'test-service', + methodName: 'test-method', + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + ], + }, + }), +}); + const gateway = mesh.addVirtualGateway('gateway1', { accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), virtualGatewayName: 'gateway1', diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index 3e268fdbc8bd2..f28d443e35317 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -237,7 +237,7 @@ export = { }, ], match: { - prefixPath: '/node', + path: appmesh.HttpRoutePathMatch.startsWith('/node'), }, timeout: { idle: cdk.Duration.seconds(10), @@ -635,7 +635,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), headers: [ appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), @@ -749,7 +749,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), method: appmesh.HttpRouteMethod.GET, }, }), @@ -791,7 +791,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), protocol: appmesh.HttpRouteProtocol.HTTP, }, }), @@ -812,6 +812,281 @@ export = { test.done(); }, + 'should match routes based on metadata'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + new appmesh.Route(stack, 'test-grpc-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode }], + match: { + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + GrpcRoute: { + Match: { + Metadata: [ + { + Invert: false, + Match: { Exact: 'application/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Exact: 'text/html' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Prefix: 'application/' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Prefix: 'text/' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Suffix: '/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Suffix: '/json+foobar' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Regex: 'application/.*' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Regex: 'text/.*' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + { + Invert: true, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + ], + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based on path'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.http({ + weightedTargets: [{ virtualNode }], + match: { + path: appmesh.HttpRoutePathMatch.exactly('/exact'), + }, + }), + }); + + new appmesh.Route(stack, 'test-http2-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + path: appmesh.HttpRoutePathMatch.regex('regex'), + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + HttpRoute: { + Match: { + Path: { + Exact: '/exact', + }, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Http2Route: { + Match: { + Path: { + Regex: 'regex', + }, + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based query parameter'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.http({ + weightedTargets: [{ virtualNode }], + match: { + queryParameters: [appmesh.QueryParameterMatch.valueIs('query-field', 'value')], + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + HttpRoute: { + Match: { + QueryParameters: [ + { + Name: 'query-field', + Match: { + Exact: 'value', + }, + }, + ], + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based method name'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + serviceName: 'test', + methodName: 'testMethod', + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + GrpcRoute: { + Match: { + ServiceName: 'test', + MethodName: 'testMethod', + }, + }, + }, + })); + + test.done(); + }, + 'should throw an error with invalid number of headers'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -834,7 +1109,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), // Empty header headers: [], }, @@ -847,7 +1122,7 @@ export = { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode }], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), // 11 headers headers: [ appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), @@ -870,6 +1145,191 @@ export = { test.done(); }, + 'should throw an error with invalid number of query parameters'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + path: appmesh.HttpRoutePathMatch.startsWith('/'), + // Empty header + queryParameters: [], + }, + }), + }); + }, /Number of query parameters provided for matching must be between 1 and 10, got: 0/); + + test.throws(() => { + router.addRoute('route2', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + path: appmesh.HttpRoutePathMatch.startsWith('/'), + // 11 headers + queryParameters: [ + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + ], + }, + }), + }); + }, /Number of query parameters provided for matching must be between 1 and 10, got: 11/); + + test.done(); + }, + + 'should throw an error with invalid number of metadata'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + metadata: [], + }, + }), + }); + }, /Number of metadata provided for matching must be between 1 and 10, got: 0/); + + // WHEN + THEN + test.throws(() => { + new appmesh.Route(stack, 'test-http-route-1', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + }); + }, /Number of metadata provided for matching must be between 1 and 10, got: 11/); + + test.done(); + }, + + 'should throw an error if no gRPC match type is defined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: {}, + }), + }); + }, /At least one gRPC match property must be provided/); + + test.done(); + }, + + 'should throw an error if method name is specified without service name'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + THEN + test.throws(() => { + new appmesh.Route(stack, 'test-http-route', { + mesh: mesh, + virtualRouter: router, + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + methodName: 'test_method', + }, + }), + }); + }, /If you specify a method name, you must also specify a service name/); + + test.done(); + }, + 'should allow route priority'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts index 4999b4425a1c7..3f7070121db4e 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts @@ -1,7 +1,6 @@ import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; - import * as appmesh from '../lib'; export = { @@ -149,7 +148,7 @@ export = { }, ], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), }, }), }); @@ -236,7 +235,7 @@ export = { }, ], match: { - prefixPath: '/', + path: appmesh.HttpRoutePathMatch.startsWith('/'), }, }), }); @@ -250,7 +249,7 @@ export = { }, ], match: { - prefixPath: '/path2', + path: appmesh.HttpRoutePathMatch.startsWith('/path2'), }, }), }); @@ -264,7 +263,7 @@ export = { }, ], match: { - prefixPath: '/path3', + path: appmesh.HttpRoutePathMatch.startsWith('/path3'), }, }), }); From 3592b26c5806cc31cd6ad0ebba32cbf4d09b9abf Mon Sep 17 00:00:00 2001 From: Hari Ohm Prasath Date: Mon, 12 Jul 2021 21:00:20 -0700 Subject: [PATCH 044/105] feat(aws-ecs): New CDK constructs for ECS Anywhere task and service definitions (#14931) Here is how the user experience will looks like, when using the new constructs for provisioning the `ECS Anywhere` resources: ```typescript // Stack definition const stack = new cdk.Stack(); // ECS Task definition const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); // Main container const container = taskDefinition.addContainer('web', { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), memoryLimitMiB: 512, }); // Port mapping container.addPortMappings({ containerPort: 3000, }); // ECS Service definition new ecs.ExternalService(stack, 'ExternalService', { cluster, taskDefinition, }); ``` > Note: Currently ECS anywhere doesn't support autoscaling, load balancing, `AWS Cloudmap` discovery and attachment of volumes. So validation rules are created part of this pull request. **This is a follow up for the below PR:** [https://github.com/aws/aws-cdk/pull/14811](https://github.com/aws/aws-cdk/pull/14811) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs/README.md | 44 +- .../aws-ecs/lib/external/external-service.ts | 190 ++++++ .../lib/external/external-task-definition.ts | 91 +++ packages/@aws-cdk/aws-ecs/lib/index.ts | 3 + packages/@aws-cdk/aws-ecs/package.json | 1 + .../test/external/external-service.test.ts | 528 +++++++++++++++ .../external/external-task-definition.test.ts | 638 ++++++++++++++++++ 7 files changed, 1490 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/external/external-service.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 7edb8e0584370..9e8170f86d826 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -60,6 +60,7 @@ one to run tasks on AWS Fargate. - Use the `Ec2TaskDefinition` and `Ec2Service` constructs to run tasks on Amazon EC2 instances running in your account. - Use the `FargateTaskDefinition` and `FargateService` constructs to run tasks on instances that are managed for you by AWS. +- Use the `ExternalTaskDefinition` and `ExternalService` constructs to run AWS ECS Anywhere tasks on self-managed infrastructure. Here are the main differences: @@ -73,10 +74,12 @@ Here are the main differences: Application/Network Load Balancers. Only the AWS log driver is supported. Many host features are not supported such as adding kernel capabilities and mounting host devices/volumes inside the container. +- **AWS ECSAnywhere**: tasks are run and managed by AWS ECS Anywhere on infrastructure owned by the customer. Only Bridge networking mode is supported. Does not support autoscaling, load balancing, cloudmap or attachment of volumes. -For more information on Amazon EC2 vs AWS Fargate and networking see the AWS Documentation: -[AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) and -[Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html). +For more information on Amazon EC2 vs AWS Fargate, networking and ECS Anywhere see the AWS Documentation: +[AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html), +[Task Networking](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html), +[ECS Anywhere](https://aws.amazon.com/ecs/anywhere/) ## Clusters @@ -211,8 +214,8 @@ some supporting containers which are used to support the main container, doings things like upload logs or metrics to monitoring services. To run a task or service with Amazon EC2 launch type, use the `Ec2TaskDefinition`. For AWS Fargate tasks/services, use the -`FargateTaskDefinition`. These classes provide a simplified API that only contain -properties relevant for that specific launch type. +`FargateTaskDefinition`. For AWS ECS Anywhere use the `ExternalTaskDefinition`. These classes +provide simplified APIs that only contain properties relevant for each specific launch type. For a `FargateTaskDefinition`, specify the task size (`memoryLimitMiB` and `cpu`): @@ -248,6 +251,19 @@ const container = ec2TaskDefinition.addContainer("WebContainer", { }); ``` +For an `ExternalTaskDefinition`: + +```ts +const externalTaskDefinition = new ecs.ExternalTaskDefinition(this, 'TaskDef'); + +const container = externalTaskDefinition.addContainer("WebContainer", { + // Use an image from DockerHub + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024 + // ... other options here ... +}); +``` + You can specify container properties when you add them to the task definition, or with various methods, e.g.: To add a port mapping when adding a container to the task definition, specify the `portMappings` option: @@ -283,6 +299,8 @@ const volume = { const container = fargateTaskDefinition.addVolume("mydatavolume"); ``` +> Note: ECS Anywhere doesn't support volume attachments in the task definition. + To use a TaskDefinition that can be used with either Amazon EC2 or AWS Fargate launch types, use the `TaskDefinition` construct. @@ -360,6 +378,18 @@ const service = new ecs.FargateService(this, 'Service', { }); ``` +ECS Anywhere service definition looks like: + +```ts +const taskDefinition; + +const service = new ecs.ExternalService(this, 'Service', { + cluster, + taskDefinition, + desiredCount: 5 +}); +``` + `Services` by default will create a security group if not provided. If you'd like to specify which security groups to use you can override the `securityGroups` property. @@ -378,6 +408,8 @@ const service = new ecs.FargateService(stack, 'Service', { }); ``` +> Note: ECS Anywhere doesn't support deployment circuit breakers and rollback. + ### Include an application/network load balancer `Services` are load balancing targets and can be added to a target group, which will be attached to an application/network load balancers: @@ -402,6 +434,8 @@ const targetGroup2 = listener.addTargets('ECS2', { }); ``` +> Note: ECS Anywhere doesn't support application/network load balancers. + Note that in the example above, the default `service` only allows you to register the first essential container or the first mapped port on the container as a target and add it to a new target group. To have more control over which container and port to register as targets, you can use `service.loadBalancerTarget()` to return a load balancing target for a specific container and port. Alternatively, you can also create all load balancer targets to be registered in this service, add them to target groups, and attach target groups to listeners accordingly. diff --git a/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts b/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts new file mode 100644 index 0000000000000..741ba0a7b2b29 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/external/external-service.ts @@ -0,0 +1,190 @@ +import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cloudmap from '@aws-cdk/aws-servicediscovery'; +import { Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { AssociateCloudMapServiceOptions, BaseService, BaseServiceOptions, CloudMapOptions, DeploymentControllerType, EcsTarget, IBaseService, IEcsLoadBalancerTarget, IService, LaunchType, PropagatedTagSource } from '../base/base-service'; +import { fromServiceAtrributes } from '../base/from-service-attributes'; +import { ScalableTaskCount } from '../base/scalable-task-count'; +import { Compatibility, LoadBalancerTargetOptions, TaskDefinition } from '../base/task-definition'; +import { ICluster } from '../cluster'; +/** + * The properties for defining a service using the External launch type. + */ +export interface ExternalServiceProps extends BaseServiceOptions { + /** + * The task definition to use for tasks in the service. + * + * [disable-awslint:ref-via-interface] + */ + readonly taskDefinition: TaskDefinition; + + /** + * The security groups to associate with the service. If you do not specify a security group, the default security group for the VPC is used. + * + * + * @default - A new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; +} + +/** + * The interface for a service using the External launch type on an ECS cluster. + */ +export interface IExternalService extends IService { + +} + +/** + * The properties to import from the service using the External launch type. + */ +export interface ExternalServiceAttributes { + /** + * The cluster that hosts the service. + */ + readonly cluster: ICluster; + + /** + * The service ARN. + * + * @default - either this, or {@link serviceName}, is required + */ + readonly serviceArn?: string; + + /** + * The name of the service. + * + * @default - either this, or {@link serviceArn}, is required + */ + readonly serviceName?: string; +} + +/** + * This creates a service using the External launch type on an ECS cluster. + * + * @resource AWS::ECS::Service + */ +export class ExternalService extends BaseService implements IExternalService { + + /** + * Imports from the specified service ARN. + */ + public static fromExternalServiceArn(scope: Construct, id: string, externalServiceArn: string): IExternalService { + class Import extends Resource implements IExternalService { + public readonly serviceArn = externalServiceArn; + public readonly serviceName = Stack.of(scope).parseArn(externalServiceArn).resourceName as string; + } + return new Import(scope, id); + } + + /** + * Imports from the specified service attrributes. + */ + public static fromExternalServiceAttributes(scope: Construct, id: string, attrs: ExternalServiceAttributes): IBaseService { + return fromServiceAtrributes(scope, id, attrs); + } + + /** + * Constructs a new instance of the ExternalService class. + */ + constructor(scope: Construct, id: string, props: ExternalServiceProps) { + if (props.minHealthyPercent !== undefined && props.maxHealthyPercent !== undefined && props.minHealthyPercent >= props.maxHealthyPercent) { + throw new Error('Minimum healthy percent must be less than maximum healthy percent.'); + } + + if (props.taskDefinition.compatibility !== Compatibility.EXTERNAL) { + throw new Error('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); + } + + if (props.cluster.defaultCloudMapNamespace !== undefined) { + throw new Error (`Cloud map integration is not supported for External service ${props.cluster.defaultCloudMapNamespace}`); + } + + if (props.cloudMapOptions !== undefined) { + throw new Error ('Cloud map options are not supported for External service'); + } + + if (props.enableExecuteCommand !== undefined) { + throw new Error ('Enable Execute Command options are not supported for External service'); + } + + if (props.capacityProviderStrategies !== undefined) { + throw new Error ('Capacity Providers are not supported for External service'); + } + + const propagateTagsFromSource = props.propagateTags ?? PropagatedTagSource.NONE; + + super(scope, id, { + ...props, + desiredCount: props.desiredCount, + maxHealthyPercent: props.maxHealthyPercent === undefined ? 100 : props.maxHealthyPercent, + minHealthyPercent: props.minHealthyPercent === undefined ? 0 : props.minHealthyPercent, + launchType: LaunchType.EXTERNAL, + propagateTags: propagateTagsFromSource, + enableECSManagedTags: props.enableECSManagedTags, + }, + { + cluster: props.cluster.clusterName, + taskDefinition: props.deploymentController?.type === DeploymentControllerType.EXTERNAL ? undefined : props.taskDefinition.taskDefinitionArn, + }, props.taskDefinition); + + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); + + this.node.addValidation({ + validate: () => this.networkConfiguration !== undefined ? ['Network configurations not supported for an external service'] : [], + }); + } + + /** + * Overriden method to throw error as `attachToApplicationTargetGroup` is not supported for external service + */ + public attachToApplicationTargetGroup(_targetGroup: elbv2.IApplicationTargetGroup): elbv2.LoadBalancerTargetProps { + throw new Error ('Application load balancer cannot be attached to an external service'); + } + + /** + * Overriden method to throw error as `loadBalancerTarget` is not supported for external service + */ + public loadBalancerTarget(_options: LoadBalancerTargetOptions): IEcsLoadBalancerTarget { + throw new Error ('External service cannot be attached as load balancer targets'); + } + + /** + * Overriden method to throw error as `registerLoadBalancerTargets` is not supported for external service + */ + public registerLoadBalancerTargets(..._targets: EcsTarget[]) { + throw new Error ('External service cannot be registered as load balancer targets'); + } + + /** + * Overriden method to throw error as `configureAwsVpcNetworkingWithSecurityGroups` is not supported for external service + */ + // eslint-disable-next-line max-len, no-unused-vars + protected configureAwsVpcNetworkingWithSecurityGroups(_vpc: ec2.IVpc, _assignPublicIp?: boolean, _vpcSubnets?: ec2.SubnetSelection, _securityGroups?: ec2.ISecurityGroup[]) { + throw new Error ('Only Bridge network mode is supported for external service'); + } + + /** + * Overriden method to throw error as `autoScaleTaskCount` is not supported for external service + */ + public autoScaleTaskCount(_props: appscaling.EnableScalingProps): ScalableTaskCount { + throw new Error ('Autoscaling not supported for external service'); + } + + /** + * Overriden method to throw error as `enableCloudMap` is not supported for external service + */ + public enableCloudMap(_options: CloudMapOptions): cloudmap.Service { + throw new Error ('Cloud map integration not supported for an external service'); + } + + /** + * Overriden method to throw error as `associateCloudMapService` is not supported for external service + */ + public associateCloudMapService(_options: AssociateCloudMapServiceOptions): void { + throw new Error ('Cloud map service association is not supported for an external service'); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts new file mode 100644 index 0000000000000..de9fa8b87e9dc --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/external/external-task-definition.ts @@ -0,0 +1,91 @@ +import { Construct } from 'constructs'; +import { ImportedTaskDefinition } from '../../lib/base/_imported-task-definition'; +import { + CommonTaskDefinitionAttributes, + CommonTaskDefinitionProps, + Compatibility, + InferenceAccelerator, + ITaskDefinition, + NetworkMode, + TaskDefinition, + Volume, +} from '../base/task-definition'; + +/** + * The properties for a task definition run on an External cluster. + */ +export interface ExternalTaskDefinitionProps extends CommonTaskDefinitionProps { + +} + +/** + * The interface of a task definition run on an External cluster. + */ +export interface IExternalTaskDefinition extends ITaskDefinition { + +} + +/** + * Attributes used to import an existing External task definition + */ +export interface ExternalTaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + +} + +/** + * The details of a task definition run on an External cluster. + * + * @resource AWS::ECS::TaskDefinition + */ +export class ExternalTaskDefinition extends TaskDefinition implements IExternalTaskDefinition { + + /** + * Imports a task definition from the specified task definition ARN. + */ + public static fromEc2TaskDefinitionArn(scope: Construct, id: string, externalTaskDefinitionArn: string): IExternalTaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: externalTaskDefinitionArn, + }); + } + + /** + * Imports an existing External task definition from its attributes + */ + public static fromExternalTaskDefinitionAttributes( + scope: Construct, + id: string, + attrs: ExternalTaskDefinitionAttributes, + ): IExternalTaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: Compatibility.EXTERNAL, + networkMode: NetworkMode.BRIDGE, + taskRole: attrs.taskRole, + }); + } + + /** + * Constructs a new instance of the ExternalTaskDefinition class. + */ + constructor(scope: Construct, id: string, props: ExternalTaskDefinitionProps = {}) { + super(scope, id, { + ...props, + compatibility: Compatibility.EXTERNAL, + networkMode: NetworkMode.BRIDGE, + }); + } + + /** + * Overridden method to throw error, as volumes are not supported for external task definitions + */ + public addVolume(_volume: Volume) { + throw new Error('External task definitions doesnt support volumes'); + } + + /** + * Overriden method to throw error as interface accelerators are not supported for external tasks + */ + public addInferenceAccelerator(_inferenceAccelerator: InferenceAccelerator) { + throw new Error('Cannot use inference accelerators on tasks that run on External service'); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 7b16d7be07827..0c1cee2a56ff9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -15,6 +15,9 @@ export * from './ec2/ec2-task-definition'; export * from './fargate/fargate-service'; export * from './fargate/fargate-task-definition'; +export * from './external/external-service'; +export * from './external/external-task-definition'; + export * from './linux-parameters'; export * from './images/asset-image'; diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index dad900b0c039b..bf15c1f8e8101 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -151,6 +151,7 @@ "props-physical-name:@aws-cdk/aws-ecs.TaskDefinitionProps", "props-physical-name:@aws-cdk/aws-ecs.Ec2TaskDefinitionProps", "props-physical-name:@aws-cdk/aws-ecs.FargateTaskDefinitionProps", + "props-physical-name:@aws-cdk/aws-ecs.ExternalTaskDefinitionProps", "docs-public-apis:@aws-cdk/aws-ecs.GelfCompressionType.GZIP", "docs-public-apis:@aws-cdk/aws-ecs.WindowsOptimizedVersion.SERVER_2016", "docs-public-apis:@aws-cdk/aws-ecs.WindowsOptimizedVersion.SERVER_2019", diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts new file mode 100644 index 0000000000000..d01eaa14f11b9 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts @@ -0,0 +1,528 @@ +import '@aws-cdk/assert-internal/jest'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cloudmap from '@aws-cdk/aws-servicediscovery'; +import * as cdk from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as ecs from '../../lib'; +import { LaunchType } from '../../lib/base/base-service'; + +nodeunitShim({ + 'When creating an External Service': { + 'with only required properties set, it correctly sets default properties'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DeploymentConfiguration: { + MaximumPercent: 100, + MinimumHealthyPercent: 0, + }, + EnableECSManagedTags: false, + LaunchType: LaunchType.EXTERNAL, + }); + + test.notEqual(service.node.defaultChild, undefined); + + test.done(); + }, + }, + + 'with all properties set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // WHEN + new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + healthCheckGracePeriod: cdk.Duration.seconds(60), + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bob', + vpc, + })], + serviceName: 'bonjour', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 55, + }, + DesiredCount: 2, + LaunchType: LaunchType.EXTERNAL, + ServiceName: 'bonjour', + }); + + test.done(); + }, + + 'with cloudmap set on cluster, throw error'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + healthCheckGracePeriod: cdk.Duration.seconds(60), + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bob', + vpc, + })], + serviceName: 'bonjour', + })).toThrow('Cloud map integration is not supported for External service' ); + + test.done(); + }, + + 'with multiple security groups, it correctly updates the cfn template'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + const securityGroup1 = new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bingo', + vpc, + }); + const securityGroup2 = new ec2.SecurityGroup(stack, 'SecurityGroup2', { + allowAllOutbound: false, + description: 'Example', + securityGroupName: 'Rolly', + vpc, + }); + + // WHEN + new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + desiredCount: 2, + securityGroups: [securityGroup1, securityGroup2], + serviceName: 'bonjour', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'ExternalTaskDef6CCBDB87', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DesiredCount: 2, + LaunchType: LaunchType.EXTERNAL, + ServiceName: 'bonjour', + }); + + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Bingo', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + }); + + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Rolly', + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + FromPort: 252, + IpProtocol: 'icmp', + ToPort: 86, + }, + ], + }); + + test.done(); + }, + + 'throws when task definition is not External compatible'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.TaskDefinition(stack, 'FargateTaskDef', { + compatibility: ecs.Compatibility.FARGATE, + cpu: '256', + memoryMiB: '512', + }); + taskDefinition.addContainer('BaseContainer', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryReservationMiB: 10, + }); + + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + })).toThrow('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); + + test.done(); + }, + + 'errors if minimum not less than maximum'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('BaseContainer', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryReservationMiB: 10, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + minHealthyPercent: 100, + maxHealthyPercent: 100, + })).toThrow('Minimum healthy percent must be less than maximum healthy percent.'); + + test.done(); + }, + + 'error if cloudmap options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + cloudMapOptions: { + name: 'myApp', + }, + })).toThrow('Cloud map options are not supported for External service'); + + // THEN + test.done(); + }, + + 'error if enableExecuteCommand options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + enableExecuteCommand: true, + })).toThrow('Enable Execute Command options are not supported for External service'); + + // THEN + test.done(); + }, + + 'error if capacityProviderStrategies options provided with external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // WHEN + const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: new ec2.InstanceType('bogus'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), + }); + + const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + enableManagedTerminationProtection: false, + }); + + // THEN + expect(() => new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + capacityProviderStrategies: [{ + capacityProvider: capacityProvider.capacityProviderName, + }], + })).toThrow('Capacity Providers are not supported for External service'); + + // THEN + test.done(); + }, + + 'error when performing attachToApplicationTargetGroup to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + const targetGroup = listener.addTargets('target', { + port: 80, + }); + + // THEN + expect(() => service.attachToApplicationTargetGroup(targetGroup)).toThrow('Application load balancer cannot be attached to an external service'); + + // THEN + test.done(); + }, + + 'error when performing loadBalancerTarget to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.loadBalancerTarget({ + containerName: 'MainContainer', + })).toThrow('External service cannot be attached as load balancer targets'); + + // THEN + test.done(); + }, + + 'error when performing registerLoadBalancerTargets to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.registerLoadBalancerTargets( + { + containerName: 'MainContainer', + containerPort: 8000, + listener: ecs.ListenerConfig.applicationListener(listener), + newTargetGroupId: 'target1', + }, + )).toThrow('External service cannot be registered as load balancer targets'); + + // THEN + test.done(); + }, + + 'error when performing autoScaleTaskCount to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.autoScaleTaskCount({ + maxCapacity: 2, + minCapacity: 1, + })).toThrow('Autoscaling not supported for external service'); + + // THEN + test.done(); + }, + + 'error when performing enableCloudMap to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + // THEN + expect(() => service.enableCloudMap({})).toThrow('Cloud map integration not supported for an external service'); + + // THEN + test.done(); + }, + + 'error when performing associateCloudMapService to an external service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'TaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + const service = new ecs.ExternalService(stack, 'ExternalService', { + cluster, + taskDefinition, + }); + + const cloudMapNamespace = new cloudmap.PrivateDnsNamespace(stack, 'TestCloudMapNamespace', { + name: 'scorekeep.com', + vpc, + }); + + const cloudMapService = new cloudmap.Service(stack, 'Service', { + name: 'service-name', + namespace: cloudMapNamespace, + dnsRecordType: cloudmap.DnsRecordType.SRV, + }); + + // THEN + expect(() => service.associateCloudMapService({ + service: cloudMapService, + container: container, + containerPort: 8000, + })).toThrow('Cloud map service association is not supported for an external service'); + + // THEN + test.done(); + }, +}); diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts new file mode 100644 index 0000000000000..c5fd8f942f1f0 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts @@ -0,0 +1,638 @@ +import '@aws-cdk/assert-internal/jest'; +import * as path from 'path'; +import { Protocol } from '@aws-cdk/aws-ec2'; +import { Repository } from '@aws-cdk/aws-ecr'; +import * as iam from '@aws-cdk/aws-iam'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as ssm from '@aws-cdk/aws-ssm'; +import * as cdk from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as ecs from '../../lib'; + +nodeunitShim({ + 'When creating an External TaskDefinition': { + 'with only required properties set, it correctly sets default properties'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + }); + + test.done(); + }, + + 'with all properties set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', { + executionRole: new iam.Role(stack, 'ExecutionRole', { + path: '/', + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('ecs.amazonaws.com'), + new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + ), + }), + family: 'ecs-tasks', + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ExecutionRoleArn: { + 'Fn::GetAtt': [ + 'ExecutionRole605A040B', + 'Arn', + ], + }, + Family: 'ecs-tasks', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: [ + 'EXTERNAL', + ], + TaskRoleArn: { + 'Fn::GetAtt': [ + 'TaskRole30FC0FBB', + 'Arn', + ], + }, + }); + + test.done(); + }, + + 'correctly sets containers'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, // add validation? + }); + + container.addPortMappings({ + containerPort: 3000, + }); + + container.addUlimits({ + hardLimit: 128, + name: ecs.UlimitName.RSS, + softLimit: 128, + }); + + container.addToExecutionPolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['ecs:*'], + })); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: 'amazon/amazon-ecs-sample', + Name: 'web', + PortMappings: [{ + ContainerPort: 3000, + HostPort: 0, + Protocol: Protocol.TCP, + }], + Ulimits: [ + { + HardLimit: 128, + Name: 'rss', + SoftLimit: 128, + }, + ], + }], + }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'ecs:*', + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + + test.done(); + }, + + 'all container definition options defined'(test: Test) { + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/name', + version: 1, + }); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 2048, + cpu: 256, + disableNetworking: true, + command: ['CMD env'], + dnsSearchDomains: ['0.0.0.0'], + dnsServers: ['1.1.1.1'], + dockerLabels: { LABEL: 'label' }, + dockerSecurityOptions: ['ECS_SELINUX_CAPABLE=true'], + entryPoint: ['/app/node_modules/.bin/cdk'], + environment: { TEST_ENVIRONMENT_VARIABLE: 'test environment variable value' }, + environmentFiles: [ecs.EnvironmentFile.fromAsset(path.join(__dirname, '../demo-envfiles/test-envfile.env'))], + essential: true, + extraHosts: { EXTRAHOST: 'extra host' }, + healthCheck: { + command: ['curl localhost:8000'], + interval: cdk.Duration.seconds(20), + retries: 5, + startPeriod: cdk.Duration.seconds(10), + }, + hostname: 'webHost', + linuxParameters: new ecs.LinuxParameters(stack, 'LinuxParameters', { + initProcessEnabled: true, + sharedMemorySize: 1024, + }), + logging: new ecs.AwsLogDriver({ streamPrefix: 'prefix' }), + memoryReservationMiB: 1024, + secrets: { + SECRET: ecs.Secret.fromSecretsManager(secret), + PARAMETER: ecs.Secret.fromSsmParameter(parameter), + }, + user: 'amazon', + workingDirectory: 'app/', + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [ + { + Command: [ + 'CMD env', + ], + Cpu: 256, + DisableNetworking: true, + DnsSearchDomains: [ + '0.0.0.0', + ], + DnsServers: [ + '1.1.1.1', + ], + DockerLabels: { + LABEL: 'label', + }, + DockerSecurityOptions: [ + 'ECS_SELINUX_CAPABLE=true', + ], + EntryPoint: [ + '/app/node_modules/.bin/cdk', + ], + Environment: [ + { + Name: 'TEST_ENVIRONMENT_VARIABLE', + Value: 'test environment variable value', + }, + ], + EnvironmentFiles: [{ + Type: 's3', + Value: { + 'Fn::Join': [ + '', + [ + 'arn:aws:s3:::', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3Bucket7B2069B7', + }, + '/', + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3VersionKey40E12C15', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dS3VersionKey40E12C15', + }, + ], + }, + ], + }, + ], + ], + }, + }], + Essential: true, + ExtraHosts: [ + { + Hostname: 'EXTRAHOST', + IpAddress: 'extra host', + }, + ], + HealthCheck: { + Command: [ + 'CMD-SHELL', + 'curl localhost:8000', + ], + Interval: 20, + Retries: 5, + StartPeriod: 10, + Timeout: 5, + }, + Hostname: 'webHost', + Image: 'amazon/amazon-ecs-sample', + LinuxParameters: { + Capabilities: {}, + InitProcessEnabled: true, + SharedMemorySize: 1024, + }, + LogConfiguration: { + LogDriver: 'awslogs', + Options: { + 'awslogs-group': { + Ref: 'ExternalTaskDefwebLogGroup827719D6', + }, + 'awslogs-stream-prefix': 'prefix', + 'awslogs-region': { + Ref: 'AWS::Region', + }, + }, + }, + Memory: 2048, + MemoryReservation: 1024, + Name: 'web', + Secrets: [ + { + Name: 'SECRET', + ValueFrom: { + Ref: 'SecretA720EF05', + }, + }, + { + Name: 'PARAMETER', + ValueFrom: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter/name', + ], + ], + }, + }, + ], + User: 'amazon', + WorkingDirectory: 'app/', + }, + ], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using all props'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage', { + lifecycleRegistryId: '123456789101', + lifecycleRules: [{ + rulePriority: 10, + tagPrefixList: ['abc'], + maxImageCount: 1, + }], + removalPolicy: cdk.RemovalPolicy.DESTROY, + repositoryName: 'project-a/amazon-ecs-sample', + })), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECR::Repository', { + LifecyclePolicy: { + // eslint-disable-next-line max-len + LifecyclePolicyText: '{"rules":[{"rulePriority":10,"selection":{"tagStatus":"tagged","tagPrefixList":["abc"],"countType":"imageCountMoreThan","countNumber":1},"action":{"type":"expire"}}]}', + RegistryId: '123456789101', + }, + RepositoryName: 'project-a/amazon-ecs-sample', + }); + + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':latest', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + }, + + 'correctly sets containers from ECR repository using an image tag'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'myTag'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':myTag', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using an image digest'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + Family: 'ExternalTaskDef', + NetworkMode: ecs.NetworkMode.BRIDGE, + RequiresCompatibilities: ['EXTERNAL'], + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + '@sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE', + ], + ], + }, + Name: 'web', + }], + }); + + test.done(); + }, + + 'correctly sets containers from ECR repository using default props'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + + // WHEN + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage')), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECR::Repository', {}); + + test.done(); + }, + + 'warns when setting containers from ECR repository using fromRegistry method'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); + // WHEN + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY'), + memoryLimitMiB: 512, + }); + + // THEN + expect(container.node.metadata[0].data).toEqual("Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); + + test.done(); + }, + + 'correctly sets volumes from'(test: Test) { + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); + + // THEN + expect(() => taskDefinition.addVolume({ + host: { + sourcePath: '/tmp/cache', + }, + name: 'scratch', + })).toThrow('External task definitions doesnt support volumes' ); + + test.done(); + }, + + 'error when interferenceAccelerators set'(test: Test) { + const stack = new cdk.Stack(); + const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); + + // THEN + expect(() => taskDefinition.addInferenceAccelerator({ + deviceName: 'device1', + deviceType: 'eia2.medium', + })).toThrow('Cannot use inference accelerators on tasks that run on External service'); + + test.done(); + }, +}); \ No newline at end of file From 94f81c441f9e2cb8dc70eb2e772d2cd75e468b67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jul 2021 07:38:37 +0300 Subject: [PATCH 045/105] chore(deps): bump actions/setup-node from 2.1.5 to 2.2.0 (#15418) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.1.5 to 2.2.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v2.1.5...v2.2.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/yarn-upgrade.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/yarn-upgrade.yml b/.github/workflows/yarn-upgrade.yml index 1b0d2e66c0594..bc42c35967a1d 100644 --- a/.github/workflows/yarn-upgrade.yml +++ b/.github/workflows/yarn-upgrade.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: 10 From c98e40e963964ae01b6ad15898a6809687d6a5e3 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Wed, 14 Jul 2021 12:43:06 +0300 Subject: [PATCH 046/105] feat(cfnspec): cloudformation spec v39.5.0 (#15536) Co-authored-by: AWS CDK Team --- packages/@aws-cdk/cfnspec/CHANGELOG.md | 53 +++ packages/@aws-cdk/cfnspec/cfn.version | 2 +- ...0_CloudFormationResourceSpecification.json | 346 +++++++++++++++++- .../withRepoAndKinesisStream.ts-fixture | 23 ++ .../rosetta/withRepoAndSqsQueue.ts-fixture | 23 ++ .../rosetta/withRepoAndTopic.ts-fixture | 23 ++ 6 files changed, 458 insertions(+), 12 deletions(-) create mode 100644 packages/monocdk/rosetta/withRepoAndKinesisStream.ts-fixture create mode 100644 packages/monocdk/rosetta/withRepoAndSqsQueue.ts-fixture create mode 100644 packages/monocdk/rosetta/withRepoAndTopic.ts-fixture diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index ea32ea3126781..794305b7dd323 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,56 @@ +# CloudFormation Resource Specification v39.5.0 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + +* AWS::AmazonMQ::Broker MaintenanceWindowStartTime.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::CodeDeploy::Application Tags (__added__) +* AWS::CodeDeploy::DeploymentConfig ComputePlatform (__added__) +* AWS::CodeDeploy::DeploymentConfig TrafficRoutingConfig (__added__) +* AWS::CodeDeploy::DeploymentGroup BlueGreenDeploymentConfiguration (__added__) +* AWS::CodeDeploy::DeploymentGroup ECSServices (__added__) +* AWS::DataBrew::Job DataCatalogOutputs (__added__) +* AWS::ServiceDiscovery::HttpNamespace Description.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ServiceDiscovery::PrivateDnsNamespace Properties (__added__) +* AWS::ServiceDiscovery::PrivateDnsNamespace Description.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ServiceDiscovery::PublicDnsNamespace Properties (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace Description.UpdateType (__changed__) + * Old: Immutable + * New: Mutable + +## Property Type Changes + +* AWS::CodeDeploy::DeploymentConfig.TimeBasedCanary (__added__) +* AWS::CodeDeploy::DeploymentConfig.TimeBasedLinear (__added__) +* AWS::CodeDeploy::DeploymentConfig.TrafficRoutingConfig (__added__) +* AWS::CodeDeploy::DeploymentGroup.BlueGreenDeploymentConfiguration (__added__) +* AWS::CodeDeploy::DeploymentGroup.BlueInstanceTerminationOption (__added__) +* AWS::CodeDeploy::DeploymentGroup.DeploymentReadyOption (__added__) +* AWS::CodeDeploy::DeploymentGroup.ECSService (__added__) +* AWS::CodeDeploy::DeploymentGroup.GreenFleetProvisioningOption (__added__) +* AWS::DataBrew::Job.DataCatalogOutput (__added__) +* AWS::DataBrew::Job.DatabaseTableOutputOptions (__added__) +* AWS::DataBrew::Job.S3TableOutputOptions (__added__) +* AWS::ServiceDiscovery::PrivateDnsNamespace.PrivateDnsPropertiesMutable (__added__) +* AWS::ServiceDiscovery::PrivateDnsNamespace.Properties (__added__) +* AWS::ServiceDiscovery::PrivateDnsNamespace.SOA (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace.Properties (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace.PublicDnsPropertiesMutable (__added__) +* AWS::ServiceDiscovery::PublicDnsNamespace.SOA (__added__) +* AWS::ApiGatewayV2::DomainName.DomainNameConfiguration OwnershipVerificationCertificateArn (__deleted__) + + # CloudFormation Resource Specification v39.3.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index c4c1d5c055c32..21aa1374d7809 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -39.3.0 +39.5.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 2ef6d5f76a30e..5986caf0ad273 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -2175,12 +2175,6 @@ "Required": false, "UpdateType": "Mutable" }, - "OwnershipVerificationCertificateArn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-domainname-domainnameconfiguration.html#cfn-apigatewayv2-domainname-domainnameconfiguration-ownershipverificationcertificatearn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "SecurityPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-domainname-domainnameconfiguration.html#cfn-apigatewayv2-domainname-domainnameconfiguration-securitypolicy", "PrimitiveType": "String", @@ -12810,6 +12804,63 @@ } } }, + "AWS::CodeDeploy::DeploymentConfig.TimeBasedCanary": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedcanary.html", + "Properties": { + "CanaryInterval": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedcanary.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedcanary-canaryinterval", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "CanaryPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedcanary.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedcanary-canarypercentage", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::CodeDeploy::DeploymentConfig.TimeBasedLinear": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedlinear.html", + "Properties": { + "LinearInterval": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedlinear.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedlinear-linearinterval", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "LinearPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-timebasedlinear.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedlinear-linearpercentage", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::CodeDeploy::DeploymentConfig.TrafficRoutingConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-trafficroutingconfig.html", + "Properties": { + "TimeBasedCanary": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-trafficroutingconfig.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedcanary", + "Required": false, + "Type": "TimeBasedCanary", + "UpdateType": "Mutable" + }, + "TimeBasedLinear": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-trafficroutingconfig.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-timebasedlinear", + "Required": false, + "Type": "TimeBasedLinear", + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentconfig-trafficroutingconfig.html#cfn-properties-codedeploy-deploymentconfig-trafficroutingconfig-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.Alarm": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-alarm.html", "Properties": { @@ -12865,6 +12916,46 @@ } } }, + "AWS::CodeDeploy::DeploymentGroup.BlueGreenDeploymentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-bluegreendeploymentconfiguration.html", + "Properties": { + "DeploymentReadyOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-bluegreendeploymentconfiguration.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-deploymentreadyoption", + "Required": false, + "Type": "DeploymentReadyOption", + "UpdateType": "Mutable" + }, + "GreenFleetProvisioningOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-bluegreendeploymentconfiguration.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-greenfleetprovisioningoption", + "Required": false, + "Type": "GreenFleetProvisioningOption", + "UpdateType": "Mutable" + }, + "TerminateBlueInstancesOnDeploymentSuccess": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-bluegreendeploymentconfiguration.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-terminateblueinstancesondeploymentsuccess", + "Required": false, + "Type": "BlueInstanceTerminationOption", + "UpdateType": "Mutable" + } + } + }, + "AWS::CodeDeploy::DeploymentGroup.BlueInstanceTerminationOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-blueinstanceterminationoption.html", + "Properties": { + "Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-blueinstanceterminationoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-blueinstanceterminationoption-action", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "TerminationWaitTimeInMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-blueinstanceterminationoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-blueinstanceterminationoption-terminationwaittimeinminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.Deployment": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deployment.html", "Properties": { @@ -12888,6 +12979,23 @@ } } }, + "AWS::CodeDeploy::DeploymentGroup.DeploymentReadyOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deploymentreadyoption.html", + "Properties": { + "ActionOnTimeout": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deploymentreadyoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-deploymentreadyoption-actionontimeout", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "WaitTimeInMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deploymentreadyoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-deploymentreadyoption-waittimeinminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.DeploymentStyle": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-deploymentstyle.html", "Properties": { @@ -12954,6 +13062,23 @@ } } }, + "AWS::CodeDeploy::DeploymentGroup.ECSService": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-ecsservice.html", + "Properties": { + "ClusterName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-ecsservice.html#cfn-codedeploy-deploymentgroup-ecsservice-clustername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ServiceName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-ecsservice.html#cfn-codedeploy-deploymentgroup-ecsservice-servicename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.ELBInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-elbinfo.html", "Properties": { @@ -12982,6 +13107,17 @@ } } }, + "AWS::CodeDeploy::DeploymentGroup.GreenFleetProvisioningOption": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-greenfleetprovisioningoption.html", + "Properties": { + "Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-greenfleetprovisioningoption.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration-greenfleetprovisioningoption-action", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::CodeDeploy::DeploymentGroup.LoadBalancerInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-loadbalancerinfo.html", "Properties": { @@ -16485,6 +16621,64 @@ } } }, + "AWS::DataBrew::Job.DataCatalogOutput": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html", + "Properties": { + "CatalogId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-catalogid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DatabaseName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-databasename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "DatabaseOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-databaseoptions", + "Required": false, + "Type": "DatabaseTableOutputOptions", + "UpdateType": "Mutable" + }, + "Overwrite": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-overwrite", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "S3Options": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-s3options", + "Required": false, + "Type": "S3TableOutputOptions", + "UpdateType": "Mutable" + }, + "TableName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-datacatalogoutput.html#cfn-databrew-job-datacatalogoutput-tablename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::DataBrew::Job.DatabaseTableOutputOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-databasetableoutputoptions.html", + "Properties": { + "TableName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-databasetableoutputoptions.html#cfn-databrew-job-databasetableoutputoptions-tablename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "TempDirectory": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-databasetableoutputoptions.html#cfn-databrew-job-databasetableoutputoptions-tempdirectory", + "Required": false, + "Type": "S3Location", + "UpdateType": "Mutable" + } + } + }, "AWS::DataBrew::Job.JobSample": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-jobsample.html", "Properties": { @@ -16607,6 +16801,17 @@ } } }, + "AWS::DataBrew::Job.S3TableOutputOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-s3tableoutputoptions.html", + "Properties": { + "Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-job-s3tableoutputoptions.html#cfn-databrew-job-s3tableoutputoptions-location", + "Required": true, + "Type": "S3Location", + "UpdateType": "Mutable" + } + } + }, "AWS::DataBrew::Project.Sample": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-databrew-project-sample.html", "Properties": { @@ -57973,6 +58178,72 @@ } } }, + "AWS::ServiceDiscovery::PrivateDnsNamespace.PrivateDnsPropertiesMutable": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-privatednspropertiesmutable.html", + "Properties": { + "SOA": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-privatednspropertiesmutable.html#cfn-servicediscovery-privatednsnamespace-privatednspropertiesmutable-soa", + "Required": false, + "Type": "SOA", + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PrivateDnsNamespace.Properties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-properties.html", + "Properties": { + "DnsProperties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-properties.html#cfn-servicediscovery-privatednsnamespace-properties-dnsproperties", + "Required": false, + "Type": "PrivateDnsPropertiesMutable", + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PrivateDnsNamespace.SOA": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-soa.html", + "Properties": { + "TTL": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-privatednsnamespace-soa.html#cfn-servicediscovery-privatednsnamespace-soa-ttl", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PublicDnsNamespace.Properties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-properties.html", + "Properties": { + "DnsProperties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-properties.html#cfn-servicediscovery-publicdnsnamespace-properties-dnsproperties", + "Required": false, + "Type": "PublicDnsPropertiesMutable", + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PublicDnsNamespace.PublicDnsPropertiesMutable": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-publicdnspropertiesmutable.html", + "Properties": { + "SOA": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-publicdnspropertiesmutable.html#cfn-servicediscovery-publicdnsnamespace-publicdnspropertiesmutable-soa", + "Required": false, + "Type": "SOA", + "UpdateType": "Mutable" + } + } + }, + "AWS::ServiceDiscovery::PublicDnsNamespace.SOA": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-soa.html", + "Properties": { + "TTL": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-publicdnsnamespace-soa.html#cfn-servicediscovery-publicdnsnamespace-soa-ttl", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::ServiceDiscovery::Service.DnsConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicediscovery-service-dnsconfig.html", "Properties": { @@ -60661,7 +60932,7 @@ } } }, - "ResourceSpecificationVersion": "39.3.0", + "ResourceSpecificationVersion": "39.5.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -60959,7 +61230,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amazonmq-broker.html#cfn-amazonmq-broker-maintenancewindowstarttime", "Required": false, "Type": "MaintenanceWindow", - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "PubliclyAccessible": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amazonmq-broker.html#cfn-amazonmq-broker-publiclyaccessible", @@ -67907,12 +68178,26 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-application.html#cfn-codedeploy-application-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" } } }, "AWS::CodeDeploy::DeploymentConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentconfig.html", "Properties": { + "ComputePlatform": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentconfig.html#cfn-codedeploy-deploymentconfig-computeplatform", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "DeploymentConfigName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentconfig.html#cfn-codedeploy-deploymentconfig-deploymentconfigname", "PrimitiveType": "String", @@ -67924,6 +68209,12 @@ "Required": false, "Type": "MinimumHealthyHosts", "UpdateType": "Immutable" + }, + "TrafficRoutingConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentconfig.html#cfn-codedeploy-deploymentconfig-trafficroutingconfig", + "Required": false, + "Type": "TrafficRoutingConfig", + "UpdateType": "Immutable" } } }, @@ -67956,6 +68247,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "BlueGreenDeploymentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentgroup.html#cfn-codedeploy-deploymentgroup-bluegreendeploymentconfiguration", + "Required": false, + "Type": "BlueGreenDeploymentConfiguration", + "UpdateType": "Mutable" + }, "Deployment": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentgroup.html#cfn-codedeploy-deploymentgroup-deployment", "Required": false, @@ -67980,6 +68277,14 @@ "Type": "DeploymentStyle", "UpdateType": "Mutable" }, + "ECSServices": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentgroup.html#cfn-codedeploy-deploymentgroup-ecsservices", + "DuplicatesAllowed": false, + "ItemType": "ECSService", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "Ec2TagFilters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codedeploy-deploymentgroup.html#cfn-codedeploy-deploymentgroup-ec2tagfilters", "DuplicatesAllowed": false, @@ -70434,6 +70739,13 @@ "AWS::DataBrew::Job": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html", "Properties": { + "DataCatalogOutputs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html#cfn-databrew-job-datacatalogoutputs", + "ItemType": "DataCatalogOutput", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "DatasetName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html#cfn-databrew-job-datasetname", "PrimitiveType": "String", @@ -96768,7 +97080,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-httpnamespace.html#cfn-servicediscovery-httpnamespace-description", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-httpnamespace.html#cfn-servicediscovery-httpnamespace-name", @@ -96823,7 +97135,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-description", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-name", @@ -96831,6 +97143,12 @@ "Required": true, "UpdateType": "Immutable" }, + "Properties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-properties", + "Required": false, + "Type": "Properties", + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-tags", "ItemType": "Tag", @@ -96861,7 +97179,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-publicdnsnamespace.html#cfn-servicediscovery-publicdnsnamespace-description", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-publicdnsnamespace.html#cfn-servicediscovery-publicdnsnamespace-name", @@ -96869,6 +97187,12 @@ "Required": true, "UpdateType": "Immutable" }, + "Properties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-publicdnsnamespace.html#cfn-servicediscovery-publicdnsnamespace-properties", + "Required": false, + "Type": "Properties", + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-publicdnsnamespace.html#cfn-servicediscovery-publicdnsnamespace-tags", "ItemType": "Tag", diff --git a/packages/monocdk/rosetta/withRepoAndKinesisStream.ts-fixture b/packages/monocdk/rosetta/withRepoAndKinesisStream.ts-fixture new file mode 100644 index 0000000000000..115e1ece7e254 --- /dev/null +++ b/packages/monocdk/rosetta/withRepoAndKinesisStream.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const stream = new kinesis.Stream(this, 'MyStream'); + + /// here + } +} diff --git a/packages/monocdk/rosetta/withRepoAndSqsQueue.ts-fixture b/packages/monocdk/rosetta/withRepoAndSqsQueue.ts-fixture new file mode 100644 index 0000000000000..98d029d8d8283 --- /dev/null +++ b/packages/monocdk/rosetta/withRepoAndSqsQueue.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const queue = new sqs.Queue(this, 'MyQueue'); + + /// here + } +} diff --git a/packages/monocdk/rosetta/withRepoAndTopic.ts-fixture b/packages/monocdk/rosetta/withRepoAndTopic.ts-fixture new file mode 100644 index 0000000000000..30c1f29cc331b --- /dev/null +++ b/packages/monocdk/rosetta/withRepoAndTopic.ts-fixture @@ -0,0 +1,23 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +import * as targets from '@aws-cdk/aws-events-targets'; +import * as events from '@aws-cdk/aws-events'; +import * as sns from '@aws-cdk/aws-sns'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const repository = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-events', + }); + + const topic = new sns.Topic(this, 'MyTopic'); + + /// here + } +} From cafdd3c0a619be69c9b6af08664af8e641d4c69b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 14 Jul 2021 13:39:51 +0200 Subject: [PATCH 047/105] feat(bootstrap): widen lookup role permissions for future extension (#15423) The lookup role permissions are currently scoped to support exactly the context lookups supported by CDK right now. This means that if we add more lookups in the future, we will have to do a complicated dance that involves updating the bootstrap stack and version checking. Instead, increase the scope of the lookup role to be able to read everything in the target account except for secrets (anything that's encrypted by KMS cannot be read). People that currently have built their own lookup extensions to CDK can use this role to implement them. **NOTE**: this updates the bootstrap stack, but nothing in the CDK framework currently requires these changes. You can update at your leisure. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/api/bootstrap/bootstrap-template.yaml | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index 2927360d7b3e2..36182c083fa55 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -281,29 +281,21 @@ Resources: - Ref: AWS::NoValue RoleName: Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region} + ManagedPolicyArns: + - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" Policies: - PolicyDocument: Statement: - - Action: - - ec2:DescribeVpcs - - ec2:DescribeAvailabilityZones - - ec2:DescribeSubnets - - ec2:DescribeRouteTables - - ec2:DescribeVpnGateways - - ec2:DescribeImages - - ec2:DescribeVpcEndpointServices - - ec2:DescribeSecurityGroups - - elasticloadbalancing:DescribeLoadBalancers - - elasticloadbalancing:DescribeTags - - elasticloadbalancing:DescribeListeners - - route53:ListHostedZonesByName - - route53:GetHostedZone - - ssm:GetParameter + - Sid: DontReadSecrets + Effect: Deny + Action: + - kms:Decrypt Resource: "*" - Effect: Allow Version: '2012-10-17' - PolicyName: - Fn::Sub: cdk-${Qualifier}-lookup-role-default-policy-${AWS::AccountId}-${AWS::Region} + PolicyName: LookupRolePolicy + Tags: + - Key: aws-cdk:bootstrap-role + Value: lookup FilePublishingRoleDefaultPolicy: Type: AWS::IAM::Policy Properties: @@ -498,7 +490,7 @@ Resources: Type: String Name: Fn::Sub: '/cdk-bootstrap/${Qualifier}/version' - Value: '7' + Value: '8' Outputs: BucketName: Description: The name of the S3 bucket owned by the CDK toolkit stack From 28a9ac0c662941be0a75c159609d7bbed88f6c05 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 14 Jul 2021 15:01:50 +0200 Subject: [PATCH 048/105] chore(cli): improve error message when bootstrap can't find stacks (#15538) The current error message is kind of confusing. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/lib/cdk-toolkit.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 6c4a2ee64d457..55ca1c6e24903 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -338,17 +338,23 @@ export class CdkToolkit { * all stacks are implicitly selected. * @param toolkitStackName the name to be used for the CDK Toolkit stack. */ - public async bootstrap(environmentSpecs: string[], bootstrapper: Bootstrapper, options: BootstrapEnvironmentOptions): Promise { + public async bootstrap(userEnvironmentSpecs: string[], bootstrapper: Bootstrapper, options: BootstrapEnvironmentOptions): Promise { // If there is an '--app' argument and an environment looks like a glob, we // select the environments from the app. Otherwise use what the user said. // By default glob for everything - environmentSpecs = environmentSpecs.length > 0 ? environmentSpecs : ['**']; + const environmentSpecs = userEnvironmentSpecs.length > 0 ? [...userEnvironmentSpecs] : ['**']; // Partition into globs and non-globs (this will mutate environmentSpecs). const globSpecs = partition(environmentSpecs, looksLikeGlob); if (globSpecs.length > 0 && !this.props.cloudExecutable.hasApp) { - throw new Error(`'${globSpecs}' is not an environment name. Run in app directory to glob or specify an environment name like \'aws://123456789012/us-east-1\'.`); + if (userEnvironmentSpecs.length > 0) { + // User did request this glob + throw new Error(`'${globSpecs}' is not an environment name. Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json' to use wildcards.`); + } else { + // User did not request anything + throw new Error('Specify an environment name like \'aws://123456789012/us-east-1\', or run in a directory with \'cdk.json\'.'); + } } const environments: cxapi.Environment[] = [ From 6047f796843ff2dfc757006aeb95755a15aa00dc Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Wed, 14 Jul 2021 17:29:51 +0300 Subject: [PATCH 049/105] chore: npm-check-updates && yarn upgrade (#15539) Ran npm-check-updates and yarn upgrade to keep the `yarn.lock` file up-to-date. --- package.json | 8 +- .../ecs-service-extensions/package.json | 2 +- packages/@aws-cdk/alexa-ask/package.json | 2 +- packages/@aws-cdk/app-delivery/package.json | 2 +- .../@aws-cdk/assert-internal/package.json | 2 +- packages/@aws-cdk/assert/package.json | 2 +- packages/@aws-cdk/assertions/package.json | 2 +- packages/@aws-cdk/assets/package.json | 2 +- .../@aws-cdk/aws-accessanalyzer/package.json | 2 +- packages/@aws-cdk/aws-acmpca/package.json | 2 +- packages/@aws-cdk/aws-amazonmq/package.json | 2 +- packages/@aws-cdk/aws-amplify/package.json | 2 +- packages/@aws-cdk/aws-apigateway/package.json | 2 +- .../aws-apigatewayv2-authorizers/package.json | 4 +- .../package.json | 4 +- .../@aws-cdk/aws-apigatewayv2/package.json | 2 +- packages/@aws-cdk/aws-appconfig/package.json | 2 +- packages/@aws-cdk/aws-appflow/package.json | 2 +- .../@aws-cdk/aws-appintegrations/package.json | 2 +- .../aws-applicationautoscaling/package.json | 2 +- .../aws-applicationinsights/package.json | 2 +- packages/@aws-cdk/aws-appmesh/package.json | 2 +- packages/@aws-cdk/aws-apprunner/package.json | 2 +- packages/@aws-cdk/aws-appstream/package.json | 2 +- packages/@aws-cdk/aws-appsync/package.json | 2 +- packages/@aws-cdk/aws-athena/package.json | 2 +- .../@aws-cdk/aws-auditmanager/package.json | 2 +- .../aws-autoscaling-common/package.json | 2 +- .../aws-autoscaling-hooktargets/package.json | 2 +- .../@aws-cdk/aws-autoscaling/package.json | 2 +- .../aws-autoscalingplans/package.json | 2 +- packages/@aws-cdk/aws-backup/package.json | 2 +- packages/@aws-cdk/aws-batch/package.json | 2 +- packages/@aws-cdk/aws-budgets/package.json | 2 +- packages/@aws-cdk/aws-cassandra/package.json | 2 +- packages/@aws-cdk/aws-ce/package.json | 2 +- .../package.json | 6 +- .../aws-certificatemanager/package.json | 2 +- packages/@aws-cdk/aws-chatbot/package.json | 2 +- packages/@aws-cdk/aws-cloud9/package.json | 2 +- .../@aws-cdk/aws-cloudformation/package.json | 4 +- .../aws-cloudfront-origins/package.json | 2 +- packages/@aws-cdk/aws-cloudfront/package.json | 2 +- packages/@aws-cdk/aws-cloudtrail/package.json | 2 +- .../aws-cloudwatch-actions/package.json | 2 +- packages/@aws-cdk/aws-cloudwatch/package.json | 2 +- .../@aws-cdk/aws-codeartifact/package.json | 2 +- packages/@aws-cdk/aws-codebuild/package.json | 2 +- packages/@aws-cdk/aws-codecommit/package.json | 2 +- packages/@aws-cdk/aws-codedeploy/package.json | 2 +- .../aws-codeguruprofiler/package.json | 2 +- .../aws-codegurureviewer/package.json | 2 +- .../aws-codepipeline-actions/package.json | 4 +- .../@aws-cdk/aws-codepipeline/package.json | 4 +- packages/@aws-cdk/aws-codestar/package.json | 2 +- .../aws-codestarconnections/package.json | 2 +- .../aws-codestarnotifications/package.json | 2 +- packages/@aws-cdk/aws-cognito/package.json | 2 +- packages/@aws-cdk/aws-config/package.json | 2 +- packages/@aws-cdk/aws-connect/package.json | 2 +- packages/@aws-cdk/aws-cur/package.json | 2 +- .../aws-customerprofiles/package.json | 2 +- packages/@aws-cdk/aws-databrew/package.json | 2 +- .../@aws-cdk/aws-datapipeline/package.json | 2 +- packages/@aws-cdk/aws-datasync/package.json | 2 +- packages/@aws-cdk/aws-dax/package.json | 2 +- packages/@aws-cdk/aws-detective/package.json | 2 +- packages/@aws-cdk/aws-devopsguru/package.json | 2 +- .../aws-directoryservice/package.json | 2 +- packages/@aws-cdk/aws-dlm/package.json | 2 +- packages/@aws-cdk/aws-dms/package.json | 2 +- packages/@aws-cdk/aws-docdb/package.json | 2 +- .../aws-global-table-coordinator/package.json | 4 +- .../@aws-cdk/aws-dynamodb-global/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 4 +- packages/@aws-cdk/aws-ec2/package.json | 4 +- packages/@aws-cdk/aws-ecr-assets/package.json | 2 +- packages/@aws-cdk/aws-ecr/package.json | 2 +- .../@aws-cdk/aws-ecs-patterns/package.json | 2 +- packages/@aws-cdk/aws-ecs/package.json | 4 +- packages/@aws-cdk/aws-efs/package.json | 2 +- packages/@aws-cdk/aws-eks-legacy/package.json | 2 +- packages/@aws-cdk/aws-eks/package.json | 4 +- .../@aws-cdk/aws-elasticache/package.json | 2 +- .../aws-elasticbeanstalk/package.json | 2 +- .../aws-elasticloadbalancing/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../aws-elasticloadbalancingv2/package.json | 2 +- .../@aws-cdk/aws-elasticsearch/package.json | 2 +- packages/@aws-cdk/aws-emr/package.json | 2 +- .../@aws-cdk/aws-emrcontainers/package.json | 2 +- .../@aws-cdk/aws-events-targets/package.json | 2 +- packages/@aws-cdk/aws-events/package.json | 2 +- .../@aws-cdk/aws-eventschemas/package.json | 2 +- packages/@aws-cdk/aws-finspace/package.json | 2 +- packages/@aws-cdk/aws-fis/package.json | 2 +- packages/@aws-cdk/aws-fms/package.json | 2 +- .../@aws-cdk/aws-frauddetector/package.json | 2 +- packages/@aws-cdk/aws-fsx/package.json | 2 +- packages/@aws-cdk/aws-gamelift/package.json | 2 +- .../package.json | 2 +- .../aws-globalaccelerator/package.json | 2 +- packages/@aws-cdk/aws-glue/package.json | 4 +- packages/@aws-cdk/aws-greengrass/package.json | 2 +- .../@aws-cdk/aws-greengrassv2/package.json | 2 +- .../@aws-cdk/aws-groundstation/package.json | 2 +- packages/@aws-cdk/aws-guardduty/package.json | 2 +- packages/@aws-cdk/aws-iam/package.json | 4 +- .../@aws-cdk/aws-imagebuilder/package.json | 2 +- packages/@aws-cdk/aws-inspector/package.json | 2 +- packages/@aws-cdk/aws-iot/package.json | 2 +- packages/@aws-cdk/aws-iot1click/package.json | 2 +- .../@aws-cdk/aws-iotanalytics/package.json | 2 +- .../aws-iotcoredeviceadvisor/package.json | 2 +- packages/@aws-cdk/aws-iotevents/package.json | 2 +- .../@aws-cdk/aws-iotfleethub/package.json | 2 +- .../@aws-cdk/aws-iotsitewise/package.json | 2 +- .../@aws-cdk/aws-iotthingsgraph/package.json | 2 +- .../@aws-cdk/aws-iotwireless/package.json | 2 +- packages/@aws-cdk/aws-ivs/package.json | 2 +- packages/@aws-cdk/aws-kendra/package.json | 2 +- packages/@aws-cdk/aws-kinesis/package.json | 2 +- .../aws-kinesisanalytics-flink/package.json | 2 +- .../aws-kinesisanalytics/package.json | 2 +- .../@aws-cdk/aws-kinesisfirehose/package.json | 2 +- packages/@aws-cdk/aws-kms/package.json | 2 +- .../@aws-cdk/aws-lakeformation/package.json | 2 +- .../aws-lambda-destinations/package.json | 2 +- .../aws-lambda-event-sources/package.json | 2 +- packages/@aws-cdk/aws-lambda-go/package.json | 2 +- .../@aws-cdk/aws-lambda-nodejs/package.json | 4 +- .../@aws-cdk/aws-lambda-python/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 6 +- .../@aws-cdk/aws-licensemanager/package.json | 2 +- packages/@aws-cdk/aws-location/package.json | 2 +- .../aws-logs-destinations/package.json | 2 +- packages/@aws-cdk/aws-logs/package.json | 6 +- .../@aws-cdk/aws-lookoutmetrics/package.json | 2 +- .../@aws-cdk/aws-lookoutvision/package.json | 2 +- packages/@aws-cdk/aws-macie/package.json | 2 +- .../aws-managedblockchain/package.json | 2 +- .../@aws-cdk/aws-mediaconnect/package.json | 2 +- .../@aws-cdk/aws-mediaconvert/package.json | 2 +- packages/@aws-cdk/aws-medialive/package.json | 2 +- .../@aws-cdk/aws-mediapackage/package.json | 2 +- packages/@aws-cdk/aws-mediastore/package.json | 2 +- packages/@aws-cdk/aws-msk/package.json | 2 +- packages/@aws-cdk/aws-mwaa/package.json | 2 +- packages/@aws-cdk/aws-neptune/package.json | 2 +- .../@aws-cdk/aws-networkfirewall/package.json | 2 +- .../@aws-cdk/aws-networkmanager/package.json | 2 +- .../@aws-cdk/aws-nimblestudio/package.json | 2 +- packages/@aws-cdk/aws-opsworks/package.json | 2 +- packages/@aws-cdk/aws-opsworkscm/package.json | 2 +- packages/@aws-cdk/aws-pinpoint/package.json | 2 +- .../@aws-cdk/aws-pinpointemail/package.json | 2 +- packages/@aws-cdk/aws-qldb/package.json | 2 +- packages/@aws-cdk/aws-quicksight/package.json | 2 +- packages/@aws-cdk/aws-ram/package.json | 2 +- packages/@aws-cdk/aws-rds/package.json | 2 +- packages/@aws-cdk/aws-redshift/package.json | 2 +- .../@aws-cdk/aws-resourcegroups/package.json | 2 +- packages/@aws-cdk/aws-robomaker/package.json | 2 +- .../aws-route53-patterns/package.json | 2 +- .../@aws-cdk/aws-route53-targets/package.json | 2 +- packages/@aws-cdk/aws-route53/package.json | 6 +- .../@aws-cdk/aws-route53resolver/package.json | 2 +- packages/@aws-cdk/aws-s3-assets/package.json | 2 +- .../@aws-cdk/aws-s3-deployment/package.json | 2 +- .../aws-s3-notifications/package.json | 2 +- packages/@aws-cdk/aws-s3/package.json | 4 +- .../@aws-cdk/aws-s3objectlambda/package.json | 2 +- packages/@aws-cdk/aws-s3outposts/package.json | 2 +- packages/@aws-cdk/aws-sagemaker/package.json | 2 +- packages/@aws-cdk/aws-sam/package.json | 2 +- packages/@aws-cdk/aws-sdb/package.json | 2 +- .../@aws-cdk/aws-secretsmanager/package.json | 2 +- .../@aws-cdk/aws-securityhub/package.json | 2 +- .../@aws-cdk/aws-servicecatalog/package.json | 2 +- .../package.json | 2 +- .../aws-servicediscovery/package.json | 2 +- .../@aws-cdk/aws-ses-actions/package.json | 2 +- packages/@aws-cdk/aws-ses/package.json | 4 +- packages/@aws-cdk/aws-signer/package.json | 2 +- .../aws-sns-subscriptions/package.json | 2 +- packages/@aws-cdk/aws-sns/package.json | 2 +- packages/@aws-cdk/aws-sqs/package.json | 2 +- packages/@aws-cdk/aws-ssm/package.json | 2 +- .../@aws-cdk/aws-ssmcontacts/package.json | 2 +- .../@aws-cdk/aws-ssmincidents/package.json | 2 +- packages/@aws-cdk/aws-sso/package.json | 2 +- .../aws-stepfunctions-tasks/package.json | 2 +- .../@aws-cdk/aws-stepfunctions/package.json | 2 +- packages/@aws-cdk/aws-synthetics/package.json | 2 +- packages/@aws-cdk/aws-timestream/package.json | 2 +- packages/@aws-cdk/aws-transfer/package.json | 2 +- packages/@aws-cdk/aws-waf/package.json | 2 +- .../@aws-cdk/aws-wafregional/package.json | 2 +- packages/@aws-cdk/aws-wafv2/package.json | 2 +- packages/@aws-cdk/aws-workspaces/package.json | 2 +- packages/@aws-cdk/aws-xray/package.json | 2 +- .../@aws-cdk/cdk-assets-schema/package.json | 2 +- packages/@aws-cdk/cfnspec/package.json | 6 +- .../cloud-assembly-schema/package.json | 6 +- .../@aws-cdk/cloudformation-diff/package.json | 2 +- .../cloudformation-include/package.json | 2 +- packages/@aws-cdk/core/package.json | 10 +- .../@aws-cdk/custom-resources/package.json | 8 +- packages/@aws-cdk/cx-api/package.json | 6 +- .../example-construct-library/package.json | 2 +- .../@aws-cdk/lambda-layer-awscli/package.json | 2 +- .../lambda-layer-kubectl/package.json | 2 +- packages/@aws-cdk/pipelines/package.json | 2 +- packages/@aws-cdk/region-info/package.json | 4 +- packages/@aws-cdk/yaml-cfn/package.json | 2 +- .../@monocdk-experiment/assert/package.json | 2 +- .../rewrite-imports/package.json | 4 +- packages/aws-cdk-lib/package.json | 2 +- packages/aws-cdk-migration/package.json | 4 +- packages/aws-cdk/package.json | 24 +- packages/awslint/package.json | 16 +- packages/cdk-assets/package.json | 10 +- packages/cdk-dasm/package.json | 4 +- packages/decdk/package.json | 10 +- packages/monocdk/package.json | 2 +- tools/cdk-build-tools/package.json | 20 +- tools/cdk-integ-tools/package.json | 4 +- tools/cdk-release/package.json | 6 +- tools/cfn2ts/package.json | 8 +- tools/eslint-plugin-cdk/package.json | 10 +- tools/nodeunit-shim/package.json | 2 +- tools/pkglint/package.json | 16 +- tools/pkgtools/package.json | 4 +- tools/prlint/package.json | 6 +- tools/ubergen/package.json | 2 +- tools/yarn-cling/package.json | 6 +- yarn.lock | 975 ++++++++---------- 238 files changed, 744 insertions(+), 905 deletions(-) diff --git a/package.json b/package.json index 17695602e3758..73af824089036 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "fs-extra": "^9.1.0", "graceful-fs": "^4.2.6", "jest-junit": "^12.2.0", - "jsii-diff": "^1.30.0", - "jsii-pacmak": "^1.30.0", - "jsii-reflect": "^1.30.0", - "jsii-rosetta": "^1.30.0", + "jsii-diff": "^1.31.0", + "jsii-pacmak": "^1.31.0", + "jsii-reflect": "^1.31.0", + "jsii-rosetta": "^1.31.0", "lerna": "^4.0.0", "patch-package": "^6.4.7", "standard-version": "^9.3.0", diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/package.json b/packages/@aws-cdk-containers/ecs-service-extensions/package.json index 8ffd15583803a..0d99dadbedb02 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/package.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/package.json @@ -37,7 +37,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/alexa-ask/package.json b/packages/@aws-cdk/alexa-ask/package.json index e9749a1ab0054..6462fd3fcc4ee 100644 --- a/packages/@aws-cdk/alexa-ask/package.json +++ b/packages/@aws-cdk/alexa-ask/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 8ebce872fa08a..45694d1f602c0 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -61,7 +61,7 @@ }, "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^2.17.0", diff --git a/packages/@aws-cdk/assert-internal/package.json b/packages/@aws-cdk/assert-internal/package.json index 4bb9686cb61e9..4d9ae5a6a5ec8 100644 --- a/packages/@aws-cdk/assert-internal/package.json +++ b/packages/@aws-cdk/assert-internal/package.json @@ -24,7 +24,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 43c1d1215a61f..92a9fd2f88bb8 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -36,7 +36,7 @@ "license": "Apache-2.0", "devDependencies": { "aws-cdk-migration": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "constructs": "^3.3.69", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/assertions/package.json b/packages/@aws-cdk/assertions/package.json index d8ea3c6435f45..952063b3423f6 100644 --- a/packages/@aws-cdk/assertions/package.json +++ b/packages/@aws-cdk/assertions/package.json @@ -61,7 +61,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "@aws-cdk/cfnspec": "0.0.0", "constructs": "^3.3.69", diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index f2dc2b033ecdd..f3d8de9b44039 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -69,7 +69,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "@types/sinon": "^9.0.11", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-accessanalyzer/package.json b/packages/@aws-cdk/aws-accessanalyzer/package.json index 6d69476663ea4..dd207f1859f38 100644 --- a/packages/@aws-cdk/aws-accessanalyzer/package.json +++ b/packages/@aws-cdk/aws-accessanalyzer/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-acmpca/package.json b/packages/@aws-cdk/aws-acmpca/package.json index a27f60a2a3ca8..771c64edeb0a8 100644 --- a/packages/@aws-cdk/aws-acmpca/package.json +++ b/packages/@aws-cdk/aws-acmpca/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-amazonmq/package.json b/packages/@aws-cdk/aws-amazonmq/package.json index da7a1697fd0d4..4cdda66e3dbf4 100644 --- a/packages/@aws-cdk/aws-amazonmq/package.json +++ b/packages/@aws-cdk/aws-amazonmq/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-amplify/package.json b/packages/@aws-cdk/aws-amplify/package.json index f8ac33c11dd36..35a569393b9c0 100644 --- a/packages/@aws-cdk/aws-amplify/package.json +++ b/packages/@aws-cdk/aws-amplify/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 1d3d39eda18fd..414d689d1a6e3 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json index 12b16c9674dcc..23690776093aa 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/aws-lambda": "^8.10.77", + "@types/jest": "^26.0.24", + "@types/aws-lambda": "^8.10.78", "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json index 500719dd8a600..e9d7dfdf5b8fc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json @@ -71,8 +71,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/nodeunit": "^0.0.31", + "@types/jest": "^26.0.24", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 31688bfcc0c90..20d03097c4ae7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -78,7 +78,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-appconfig/package.json b/packages/@aws-cdk/aws-appconfig/package.json index 9bdfa88e6bfe2..7ae2918e7a24a 100644 --- a/packages/@aws-cdk/aws-appconfig/package.json +++ b/packages/@aws-cdk/aws-appconfig/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-appflow/package.json b/packages/@aws-cdk/aws-appflow/package.json index 43533eb7a1f22..d3892bc22a18e 100644 --- a/packages/@aws-cdk/aws-appflow/package.json +++ b/packages/@aws-cdk/aws-appflow/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-appintegrations/package.json b/packages/@aws-cdk/aws-appintegrations/package.json index bcde772da06db..cc372cc4fb1f7 100644 --- a/packages/@aws-cdk/aws-appintegrations/package.json +++ b/packages/@aws-cdk/aws-appintegrations/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 9005625667352..a48c9b098229f 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "fast-check": "^2.17.0", diff --git a/packages/@aws-cdk/aws-applicationinsights/package.json b/packages/@aws-cdk/aws-applicationinsights/package.json index b5bc3d711ac9b..0dcc760a0ef2b 100644 --- a/packages/@aws-cdk/aws-applicationinsights/package.json +++ b/packages/@aws-cdk/aws-applicationinsights/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index cd4af5016fb1a..6ce89ff68a1e6 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-apprunner/package.json b/packages/@aws-cdk/aws-apprunner/package.json index 9585624e0d57c..20db35c7a3264 100644 --- a/packages/@aws-cdk/aws-apprunner/package.json +++ b/packages/@aws-cdk/aws-apprunner/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-appstream/package.json b/packages/@aws-cdk/aws-appstream/package.json index 6e46c47abd07a..2bdd6def0c6f8 100644 --- a/packages/@aws-cdk/aws-appstream/package.json +++ b/packages/@aws-cdk/aws-appstream/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index ba8b8145f799e..13267c04844cc 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-athena/package.json b/packages/@aws-cdk/aws-athena/package.json index 7b536cc646622..0170da3267a71 100644 --- a/packages/@aws-cdk/aws-athena/package.json +++ b/packages/@aws-cdk/aws-athena/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit-shim": "0.0.0", diff --git a/packages/@aws-cdk/aws-auditmanager/package.json b/packages/@aws-cdk/aws-auditmanager/package.json index bff117d2e2073..e1c4d99497507 100644 --- a/packages/@aws-cdk/aws-auditmanager/package.json +++ b/packages/@aws-cdk/aws-auditmanager/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index 3d5837beabb85..59f8594332346 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^2.17.0", diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json index 42c9c30f26923..08a1b4120aee1 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index a14de1d88510d..c2ece82fdb8b0 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-autoscalingplans/package.json b/packages/@aws-cdk/aws-autoscalingplans/package.json index bba4866c26bc3..e665150f6b980 100644 --- a/packages/@aws-cdk/aws-autoscalingplans/package.json +++ b/packages/@aws-cdk/aws-autoscalingplans/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-backup/package.json b/packages/@aws-cdk/aws-backup/package.json index 5f16b236e39e4..3c906382aa9b2 100644 --- a/packages/@aws-cdk/aws-backup/package.json +++ b/packages/@aws-cdk/aws-backup/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-batch/package.json b/packages/@aws-cdk/aws-batch/package.json index 0581557a70e1b..158edc9ff76ff 100644 --- a/packages/@aws-cdk/aws-batch/package.json +++ b/packages/@aws-cdk/aws-batch/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-budgets/package.json b/packages/@aws-cdk/aws-budgets/package.json index 7eec1b16415a8..b3d03d5d5d194 100644 --- a/packages/@aws-cdk/aws-budgets/package.json +++ b/packages/@aws-cdk/aws-budgets/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-cassandra/package.json b/packages/@aws-cdk/aws-cassandra/package.json index 825c114741063..94d5a49670f90 100644 --- a/packages/@aws-cdk/aws-cassandra/package.json +++ b/packages/@aws-cdk/aws-cassandra/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-ce/package.json b/packages/@aws-cdk/aws-ce/package.json index f4eda68f8d287..1e0345dc6fb9c 100644 --- a/packages/@aws-cdk/aws-ce/package.json +++ b/packages/@aws-cdk/aws-ce/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json index b91db6bcc680f..e3ab68c8a7dd6 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json @@ -29,12 +29,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", + "@types/aws-lambda": "^8.10.78", "@types/sinon": "^9.0.11", "cdk-build-tools": "0.0.0", "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.2.1", - "eslint": "^7.29.0", + "eslint": "^7.30.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", @@ -43,7 +43,7 @@ "jest": "^26.6.3", "lambda-tester": "^3.6.0", "sinon": "^9.2.4", - "nock": "^13.1.0", + "nock": "^13.1.1", "ts-jest": "^26.5.6" } } diff --git a/packages/@aws-cdk/aws-certificatemanager/package.json b/packages/@aws-cdk/aws-certificatemanager/package.json index c903bb46c9181..f84f775f108de 100644 --- a/packages/@aws-cdk/aws-certificatemanager/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-chatbot/package.json b/packages/@aws-cdk/aws-chatbot/package.json index e7db76bc38cf2..9fcdfedfcda21 100644 --- a/packages/@aws-cdk/aws-chatbot/package.json +++ b/packages/@aws-cdk/aws-chatbot/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index 077748fc3b08b..8aac346d87398 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-codecommit": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index cdd4583356787..f0d492aace535 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -74,8 +74,8 @@ "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", - "@types/aws-lambda": "^8.10.77", - "@types/nodeunit": "^0.0.31", + "@types/aws-lambda": "^8.10.78", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudfront-origins/package.json b/packages/@aws-cdk/aws-cloudfront-origins/package.json index 4a7e3e697fb06..e7ef1eb01a94d 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/package.json +++ b/packages/@aws-cdk/aws-cloudfront-origins/package.json @@ -71,7 +71,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 676bf3bfe68d9..cd8e8f17f9b5d 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 2ab7e02127c41..526442259cddb 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/package.json b/packages/@aws-cdk/aws-cloudwatch-actions/package.json index 3afd447c24a5d..0869c60941c8f 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/package.json +++ b/packages/@aws-cdk/aws-cloudwatch-actions/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index 85ccf7516fe87..c57fc64d6f8a0 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codeartifact/package.json b/packages/@aws-cdk/aws-codeartifact/package.json index 4fe7c94427035..b1d8e76ef79d4 100644 --- a/packages/@aws-cdk/aws-codeartifact/package.json +++ b/packages/@aws-cdk/aws-codeartifact/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index db40c989dfd01..e7591a3ab4fbb 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -78,7 +78,7 @@ "devDependencies": { "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index cefdffbc2655c..6d622f85d076a 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-sns": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 89546a6116ae1..89f9706dd9f3e 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codeguruprofiler/package.json b/packages/@aws-cdk/aws-codeguruprofiler/package.json index 7249bcb919a38..1c72b538d5056 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/package.json +++ b/packages/@aws-cdk/aws-codeguruprofiler/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codegurureviewer/package.json b/packages/@aws-cdk/aws-codegurureviewer/package.json index 5b976bab70db6..f7a900d2aef8b 100644 --- a/packages/@aws-cdk/aws-codegurureviewer/package.json +++ b/packages/@aws-cdk/aws-codegurureviewer/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 9cd001d3d3bba..fafc221e72351 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -68,11 +68,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-cloudtrail": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "@types/lodash": "^4.14.170", + "@types/lodash": "^4.14.171", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "lodash": "^4.17.21", diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 82c75be1eea46..95b30ad58ae03 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -78,9 +78,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit-shim": "0.0.0", diff --git a/packages/@aws-cdk/aws-codestar/package.json b/packages/@aws-cdk/aws-codestar/package.json index 69ebd5ad2897a..8b943f821668d 100644 --- a/packages/@aws-cdk/aws-codestar/package.json +++ b/packages/@aws-cdk/aws-codestar/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codestarconnections/package.json b/packages/@aws-cdk/aws-codestarconnections/package.json index ab3ea1d507011..d45abecfe6ccd 100644 --- a/packages/@aws-cdk/aws-codestarconnections/package.json +++ b/packages/@aws-cdk/aws-codestarconnections/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-codestarnotifications/package.json b/packages/@aws-cdk/aws-codestarnotifications/package.json index 8e96eef73d458..81460dd341e68 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/package.json +++ b/packages/@aws-cdk/aws-codestarnotifications/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index bf68fec018127..56cd465f17680 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/punycode": "^2.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index 92f17b421cfd9..c03852d4321e1 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -73,7 +73,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-events-targets": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-connect/package.json b/packages/@aws-cdk/aws-connect/package.json index c63ca8004bdee..02e16855e466b 100644 --- a/packages/@aws-cdk/aws-connect/package.json +++ b/packages/@aws-cdk/aws-connect/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.22", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cur/package.json b/packages/@aws-cdk/aws-cur/package.json index 31b36c3bf5612..ec2fb98a0d60c 100644 --- a/packages/@aws-cdk/aws-cur/package.json +++ b/packages/@aws-cdk/aws-cur/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-customerprofiles/package.json b/packages/@aws-cdk/aws-customerprofiles/package.json index 9a6e23405a069..387a792b7a373 100644 --- a/packages/@aws-cdk/aws-customerprofiles/package.json +++ b/packages/@aws-cdk/aws-customerprofiles/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert-internal": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/aws-databrew/package.json b/packages/@aws-cdk/aws-databrew/package.json index 12d47c939b79f..9f36870538f10 100644 --- a/packages/@aws-cdk/aws-databrew/package.json +++ b/packages/@aws-cdk/aws-databrew/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-datapipeline/package.json b/packages/@aws-cdk/aws-datapipeline/package.json index b60950c929ac4..f368c68baa360 100644 --- a/packages/@aws-cdk/aws-datapipeline/package.json +++ b/packages/@aws-cdk/aws-datapipeline/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-datasync/package.json b/packages/@aws-cdk/aws-datasync/package.json index b1212ec1789e6..d63f373cfeef8 100644 --- a/packages/@aws-cdk/aws-datasync/package.json +++ b/packages/@aws-cdk/aws-datasync/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-dax/package.json b/packages/@aws-cdk/aws-dax/package.json index c0023d1cf329b..bd4d0c94dc3d3 100644 --- a/packages/@aws-cdk/aws-dax/package.json +++ b/packages/@aws-cdk/aws-dax/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-detective/package.json b/packages/@aws-cdk/aws-detective/package.json index 110d3dd70dc26..b5bd1c2a903df 100644 --- a/packages/@aws-cdk/aws-detective/package.json +++ b/packages/@aws-cdk/aws-detective/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-devopsguru/package.json b/packages/@aws-cdk/aws-devopsguru/package.json index 329cf5f069bd5..6fc91c263db06 100644 --- a/packages/@aws-cdk/aws-devopsguru/package.json +++ b/packages/@aws-cdk/aws-devopsguru/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-directoryservice/package.json b/packages/@aws-cdk/aws-directoryservice/package.json index 5c58ea6187881..179e72a6a4a2a 100644 --- a/packages/@aws-cdk/aws-directoryservice/package.json +++ b/packages/@aws-cdk/aws-directoryservice/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-dlm/package.json b/packages/@aws-cdk/aws-dlm/package.json index 4db4be0d3e694..09d10c116da40 100644 --- a/packages/@aws-cdk/aws-dlm/package.json +++ b/packages/@aws-cdk/aws-dlm/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-dms/package.json b/packages/@aws-cdk/aws-dms/package.json index 41b7a99b024b8..7e06f8859e421 100644 --- a/packages/@aws-cdk/aws-dms/package.json +++ b/packages/@aws-cdk/aws-dms/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-docdb/package.json b/packages/@aws-cdk/aws-docdb/package.json index 401e97ae696b3..472f2a229215c 100644 --- a/packages/@aws-cdk/aws-docdb/package.json +++ b/packages/@aws-cdk/aws-docdb/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json index 907e490b7625b..5df277b6064ca 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json @@ -31,7 +31,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.2.1", - "eslint": "^7.29.0", + "eslint": "^7.30.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", @@ -39,6 +39,6 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.1.0" + "nock": "^13.1.1" } } diff --git a/packages/@aws-cdk/aws-dynamodb-global/package.json b/packages/@aws-cdk/aws-dynamodb-global/package.json index 7f095891c0375..77e92e5d5aa17 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/package.json @@ -56,7 +56,7 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 0523bd4157aa9..a9ffc922c528e 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 83ce458230cb0..52dede95bb318 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecr-assets/package.json b/packages/@aws-cdk/aws-ecr-assets/package.json index 828f489e6d7b0..edf101cdf28b2 100644 --- a/packages/@aws-cdk/aws-ecr-assets/package.json +++ b/packages/@aws-cdk/aws-ecr-assets/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/proxyquire": "^1.3.28", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index 5134e382edd90..d2eecfc93b1ba 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert-internal": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index 259a800e92d8d..2bed9fe6b9cc8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index bf15c1f8e8101..1348aef9b9d62 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -73,10 +73,10 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-s3-deployment": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "@types/proxyquire": "^1.3.28", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-efs/package.json b/packages/@aws-cdk/aws-efs/package.json index 4efbcf3fec5d1..7a68d2200311a 100644 --- a/packages/@aws-cdk/aws-efs/package.json +++ b/packages/@aws-cdk/aws-efs/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks-legacy/package.json b/packages/@aws-cdk/aws-eks-legacy/package.json index cca6fa5402fde..862a69da0f289 100644 --- a/packages/@aws-cdk/aws-eks-legacy/package.json +++ b/packages/@aws-cdk/aws-eks-legacy/package.json @@ -70,7 +70,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 685674d96decb..bf23bff54c899 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -72,9 +72,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", + "@types/aws-lambda": "^8.10.78", "@types/sinon": "^9.0.11", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "@types/yaml": "1.9.6", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticache/package.json b/packages/@aws-cdk/aws-elasticache/package.json index 5b36458012dd2..c0b500a42f22d 100644 --- a/packages/@aws-cdk/aws-elasticache/package.json +++ b/packages/@aws-cdk/aws-elasticache/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/package.json b/packages/@aws-cdk/aws-elasticbeanstalk/package.json index bf69f8a117aa0..80d5033ecc931 100644 --- a/packages/@aws-cdk/aws-elasticbeanstalk/package.json +++ b/packages/@aws-cdk/aws-elasticbeanstalk/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/package.json b/packages/@aws-cdk/aws-elasticloadbalancing/package.json index 6c5fe1525c125..e20392648d2dd 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json index 6f667ad580497..053caf2ce49a5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json index 296b208c75600..e25ccb5ccd96f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json index afda57b575d74..cf411cbd32c88 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index 4e185fab1c424..37c35b69393b2 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-emr/package.json b/packages/@aws-cdk/aws-emr/package.json index 357fc85567b2f..f216a69bdf839 100644 --- a/packages/@aws-cdk/aws-emr/package.json +++ b/packages/@aws-cdk/aws-emr/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-emrcontainers/package.json b/packages/@aws-cdk/aws-emrcontainers/package.json index d98724f001660..bf2a7fbe9a627 100644 --- a/packages/@aws-cdk/aws-emrcontainers/package.json +++ b/packages/@aws-cdk/aws-emrcontainers/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 558262fea08fd..3658b64701804 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index 10bcc9bea5d43..2d962235e0227 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "nodeunit": "^0.11.3", diff --git a/packages/@aws-cdk/aws-eventschemas/package.json b/packages/@aws-cdk/aws-eventschemas/package.json index f08e6d978c6e2..0e16baa12d4f4 100644 --- a/packages/@aws-cdk/aws-eventschemas/package.json +++ b/packages/@aws-cdk/aws-eventschemas/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-finspace/package.json b/packages/@aws-cdk/aws-finspace/package.json index 54c59e4e3c7ec..8b70bc5443455 100644 --- a/packages/@aws-cdk/aws-finspace/package.json +++ b/packages/@aws-cdk/aws-finspace/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-fis/package.json b/packages/@aws-cdk/aws-fis/package.json index 73af76ee270c0..7412ed44a10fc 100644 --- a/packages/@aws-cdk/aws-fis/package.json +++ b/packages/@aws-cdk/aws-fis/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-fms/package.json b/packages/@aws-cdk/aws-fms/package.json index a488662c1649a..5ed725a88b4cc 100644 --- a/packages/@aws-cdk/aws-fms/package.json +++ b/packages/@aws-cdk/aws-fms/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-frauddetector/package.json b/packages/@aws-cdk/aws-frauddetector/package.json index 40f62328af688..fbf05f78a7640 100644 --- a/packages/@aws-cdk/aws-frauddetector/package.json +++ b/packages/@aws-cdk/aws-frauddetector/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-fsx/package.json b/packages/@aws-cdk/aws-fsx/package.json index b86b3cb7497f9..1370bae1727de 100644 --- a/packages/@aws-cdk/aws-fsx/package.json +++ b/packages/@aws-cdk/aws-fsx/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-gamelift/package.json b/packages/@aws-cdk/aws-gamelift/package.json index 5f8977e82cde1..07774ebf11293 100644 --- a/packages/@aws-cdk/aws-gamelift/package.json +++ b/packages/@aws-cdk/aws-gamelift/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json b/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json index cf27864c3ab81..532b01de2fbe9 100644 --- a/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json +++ b/packages/@aws-cdk/aws-globalaccelerator-endpoints/package.json @@ -70,7 +70,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", diff --git a/packages/@aws-cdk/aws-globalaccelerator/package.json b/packages/@aws-cdk/aws-globalaccelerator/package.json index 3c62075d2b299..b4b0e90197370 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/package.json +++ b/packages/@aws-cdk/aws-globalaccelerator/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "cdk-integ-tools": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-glue/package.json b/packages/@aws-cdk/aws-glue/package.json index 15fb49b3bc56b..f489c1955478e 100644 --- a/packages/@aws-cdk/aws-glue/package.json +++ b/packages/@aws-cdk/aws-glue/package.json @@ -73,9 +73,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-greengrass/package.json b/packages/@aws-cdk/aws-greengrass/package.json index b6fe1c7e2e46a..9fa8de9238ee2 100644 --- a/packages/@aws-cdk/aws-greengrass/package.json +++ b/packages/@aws-cdk/aws-greengrass/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-greengrassv2/package.json b/packages/@aws-cdk/aws-greengrassv2/package.json index 02307786f2482..8096e3269ac30 100644 --- a/packages/@aws-cdk/aws-greengrassv2/package.json +++ b/packages/@aws-cdk/aws-greengrassv2/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-groundstation/package.json b/packages/@aws-cdk/aws-groundstation/package.json index d699b2aa0bb2f..f337bfd5dc79e 100644 --- a/packages/@aws-cdk/aws-groundstation/package.json +++ b/packages/@aws-cdk/aws-groundstation/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-guardduty/package.json b/packages/@aws-cdk/aws-guardduty/package.json index 1346c0f58b9e4..671cd3e85044c 100644 --- a/packages/@aws-cdk/aws-guardduty/package.json +++ b/packages/@aws-cdk/aws-guardduty/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index ed0bf8cbd9845..2cbf971197ef3 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", "@types/sinon": "^9.0.11", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-imagebuilder/package.json b/packages/@aws-cdk/aws-imagebuilder/package.json index d99105d35eb51..0f37220e9e269 100644 --- a/packages/@aws-cdk/aws-imagebuilder/package.json +++ b/packages/@aws-cdk/aws-imagebuilder/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-inspector/package.json b/packages/@aws-cdk/aws-inspector/package.json index 4111ea49c0f74..489f584c5b6e8 100644 --- a/packages/@aws-cdk/aws-inspector/package.json +++ b/packages/@aws-cdk/aws-inspector/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index 450c7917fe426..1e83190d4b532 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iot1click/package.json b/packages/@aws-cdk/aws-iot1click/package.json index 8350be2212a4e..b1821fe309430 100644 --- a/packages/@aws-cdk/aws-iot1click/package.json +++ b/packages/@aws-cdk/aws-iot1click/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotanalytics/package.json b/packages/@aws-cdk/aws-iotanalytics/package.json index 60a7a417efb74..97acc44632e9b 100644 --- a/packages/@aws-cdk/aws-iotanalytics/package.json +++ b/packages/@aws-cdk/aws-iotanalytics/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json b/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json index e896446f705a2..ca500daa21d26 100644 --- a/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json +++ b/packages/@aws-cdk/aws-iotcoredeviceadvisor/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotevents/package.json b/packages/@aws-cdk/aws-iotevents/package.json index a2461f74bcd72..e6e819bba2aed 100644 --- a/packages/@aws-cdk/aws-iotevents/package.json +++ b/packages/@aws-cdk/aws-iotevents/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotfleethub/package.json b/packages/@aws-cdk/aws-iotfleethub/package.json index 8296a0853dad2..f873c33b08634 100644 --- a/packages/@aws-cdk/aws-iotfleethub/package.json +++ b/packages/@aws-cdk/aws-iotfleethub/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotsitewise/package.json b/packages/@aws-cdk/aws-iotsitewise/package.json index dbb8b922d178a..81750ea6c4221 100644 --- a/packages/@aws-cdk/aws-iotsitewise/package.json +++ b/packages/@aws-cdk/aws-iotsitewise/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotthingsgraph/package.json b/packages/@aws-cdk/aws-iotthingsgraph/package.json index 80c594cae7f4f..39dee778b632f 100644 --- a/packages/@aws-cdk/aws-iotthingsgraph/package.json +++ b/packages/@aws-cdk/aws-iotthingsgraph/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-iotwireless/package.json b/packages/@aws-cdk/aws-iotwireless/package.json index 43fdd6db8741a..176941dd97bb4 100644 --- a/packages/@aws-cdk/aws-iotwireless/package.json +++ b/packages/@aws-cdk/aws-iotwireless/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-ivs/package.json b/packages/@aws-cdk/aws-ivs/package.json index 2c37af7ef1515..31a2a63490f4d 100644 --- a/packages/@aws-cdk/aws-ivs/package.json +++ b/packages/@aws-cdk/aws-ivs/package.json @@ -85,7 +85,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-kendra/package.json b/packages/@aws-cdk/aws-kendra/package.json index 43af58a860116..c78e48ea705e7 100644 --- a/packages/@aws-cdk/aws-kendra/package.json +++ b/packages/@aws-cdk/aws-kendra/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesis/package.json b/packages/@aws-cdk/aws-kinesis/package.json index b2684052ff3c9..51697ec9931df 100644 --- a/packages/@aws-cdk/aws-kinesis/package.json +++ b/packages/@aws-cdk/aws-kinesis/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json index 9adae1521da0e..02ffcb62439e7 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json @@ -66,7 +66,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-kinesisanalytics/package.json b/packages/@aws-cdk/aws-kinesisanalytics/package.json index 1d5f1fdf248be..3a3933d62d655 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics/package.json @@ -76,7 +76,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesisfirehose/package.json b/packages/@aws-cdk/aws-kinesisfirehose/package.json index e3aaff7954822..fc949c6290e88 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-kms/package.json b/packages/@aws-cdk/aws-kms/package.json index b61313f395a88..d157f85c4e67d 100644 --- a/packages/@aws-cdk/aws-kms/package.json +++ b/packages/@aws-cdk/aws-kms/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lakeformation/package.json b/packages/@aws-cdk/aws-lakeformation/package.json index 8434842b8e653..a374c0d9627da 100644 --- a/packages/@aws-cdk/aws-lakeformation/package.json +++ b/packages/@aws-cdk/aws-lakeformation/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-destinations/package.json b/packages/@aws-cdk/aws-lambda-destinations/package.json index 9c266b3909ec2..4156149551b5b 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/package.json +++ b/packages/@aws-cdk/aws-lambda-destinations/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index 178e9a4ce5bc7..8a1897a9fc7a5 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", diff --git a/packages/@aws-cdk/aws-lambda-go/package.json b/packages/@aws-cdk/aws-lambda-go/package.json index 2cc20f8b38d92..9f83f91c15963 100644 --- a/packages/@aws-cdk/aws-lambda-go/package.json +++ b/packages/@aws-cdk/aws-lambda-go/package.json @@ -67,7 +67,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert-internal": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index 04f9eca6babf7..ce16b825a12fc 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -64,12 +64,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "delay": "5.0.0", - "esbuild": "^0.12.9", + "esbuild": "^0.12.15", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-lambda-python/package.json b/packages/@aws-cdk/aws-lambda-python/package.json index fb320c9dcb5c0..814ec44de6c69 100644 --- a/packages/@aws-cdk/aws-lambda-python/package.json +++ b/packages/@aws-cdk/aws-lambda-python/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index e83aaf00c6e10..da9f2ff159fd6 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -77,9 +77,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/aws-lambda": "^8.10.77", - "@types/lodash": "^4.14.170", + "@types/jest": "^26.0.24", + "@types/aws-lambda": "^8.10.78", + "@types/lodash": "^4.14.171", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-licensemanager/package.json b/packages/@aws-cdk/aws-licensemanager/package.json index 0847cc63de9f8..eb66089eae974 100644 --- a/packages/@aws-cdk/aws-licensemanager/package.json +++ b/packages/@aws-cdk/aws-licensemanager/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-location/package.json b/packages/@aws-cdk/aws-location/package.json index da398d6e16269..dedff67266046 100644 --- a/packages/@aws-cdk/aws-location/package.json +++ b/packages/@aws-cdk/aws-location/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-logs-destinations/package.json b/packages/@aws-cdk/aws-logs-destinations/package.json index 31b963bcaebe9..5d13936533fcc 100644 --- a/packages/@aws-cdk/aws-logs-destinations/package.json +++ b/packages/@aws-cdk/aws-logs-destinations/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index 226ed0a05dd1b..a7838115e50db 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -72,15 +72,15 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", - "@types/aws-lambda": "^8.10.77", + "@types/nodeunit": "^0.0.32", + "@types/aws-lambda": "^8.10.78", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nock": "^13.1.0", + "nock": "^13.1.1", "nodeunit": "^0.11.3", "pkglint": "0.0.0", "sinon": "^9.2.4", diff --git a/packages/@aws-cdk/aws-lookoutmetrics/package.json b/packages/@aws-cdk/aws-lookoutmetrics/package.json index 29ed950656e67..93c04870c0f5d 100644 --- a/packages/@aws-cdk/aws-lookoutmetrics/package.json +++ b/packages/@aws-cdk/aws-lookoutmetrics/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lookoutvision/package.json b/packages/@aws-cdk/aws-lookoutvision/package.json index c8e46589f578e..d55a122cb7624 100644 --- a/packages/@aws-cdk/aws-lookoutvision/package.json +++ b/packages/@aws-cdk/aws-lookoutvision/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-macie/package.json b/packages/@aws-cdk/aws-macie/package.json index f891fda3ecd68..1cbdb49f09399 100644 --- a/packages/@aws-cdk/aws-macie/package.json +++ b/packages/@aws-cdk/aws-macie/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-managedblockchain/package.json b/packages/@aws-cdk/aws-managedblockchain/package.json index 6590e8af3da6e..675de2f297c20 100644 --- a/packages/@aws-cdk/aws-managedblockchain/package.json +++ b/packages/@aws-cdk/aws-managedblockchain/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediaconnect/package.json b/packages/@aws-cdk/aws-mediaconnect/package.json index e035ebdeb643d..b67436e9224aa 100644 --- a/packages/@aws-cdk/aws-mediaconnect/package.json +++ b/packages/@aws-cdk/aws-mediaconnect/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediaconvert/package.json b/packages/@aws-cdk/aws-mediaconvert/package.json index 1dda1afe88d23..4808bc6dda2ff 100644 --- a/packages/@aws-cdk/aws-mediaconvert/package.json +++ b/packages/@aws-cdk/aws-mediaconvert/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-medialive/package.json b/packages/@aws-cdk/aws-medialive/package.json index b82ef361febb4..3e1e7088f1f0c 100644 --- a/packages/@aws-cdk/aws-medialive/package.json +++ b/packages/@aws-cdk/aws-medialive/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediapackage/package.json b/packages/@aws-cdk/aws-mediapackage/package.json index fbc58672183b7..84077872af4d5 100644 --- a/packages/@aws-cdk/aws-mediapackage/package.json +++ b/packages/@aws-cdk/aws-mediapackage/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-mediastore/package.json b/packages/@aws-cdk/aws-mediastore/package.json index 440c6c934bccf..20e9b4057dbe5 100644 --- a/packages/@aws-cdk/aws-mediastore/package.json +++ b/packages/@aws-cdk/aws-mediastore/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-msk/package.json b/packages/@aws-cdk/aws-msk/package.json index 14d07f5bb7ebb..7309920e959ac 100644 --- a/packages/@aws-cdk/aws-msk/package.json +++ b/packages/@aws-cdk/aws-msk/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-mwaa/package.json b/packages/@aws-cdk/aws-mwaa/package.json index 7cd2aa2a15c27..07fbbab350909 100644 --- a/packages/@aws-cdk/aws-mwaa/package.json +++ b/packages/@aws-cdk/aws-mwaa/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index 93e43835652ca..dfc50d27ac70d 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-networkfirewall/package.json b/packages/@aws-cdk/aws-networkfirewall/package.json index 6a29d30433858..27b3aa04a7136 100644 --- a/packages/@aws-cdk/aws-networkfirewall/package.json +++ b/packages/@aws-cdk/aws-networkfirewall/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-networkmanager/package.json b/packages/@aws-cdk/aws-networkmanager/package.json index 29071ca7dae1b..f0666a1e676cc 100644 --- a/packages/@aws-cdk/aws-networkmanager/package.json +++ b/packages/@aws-cdk/aws-networkmanager/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-nimblestudio/package.json b/packages/@aws-cdk/aws-nimblestudio/package.json index 83fe8f795b276..5f9f77d2f58b2 100644 --- a/packages/@aws-cdk/aws-nimblestudio/package.json +++ b/packages/@aws-cdk/aws-nimblestudio/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-opsworks/package.json b/packages/@aws-cdk/aws-opsworks/package.json index 9c35474f64721..978dd2b2b07c5 100644 --- a/packages/@aws-cdk/aws-opsworks/package.json +++ b/packages/@aws-cdk/aws-opsworks/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-opsworkscm/package.json b/packages/@aws-cdk/aws-opsworkscm/package.json index 2dfddff960cf0..42c0e014301cb 100644 --- a/packages/@aws-cdk/aws-opsworkscm/package.json +++ b/packages/@aws-cdk/aws-opsworkscm/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-pinpoint/package.json b/packages/@aws-cdk/aws-pinpoint/package.json index 48fa3dbeac296..0f5b7b48a84cf 100644 --- a/packages/@aws-cdk/aws-pinpoint/package.json +++ b/packages/@aws-cdk/aws-pinpoint/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-pinpointemail/package.json b/packages/@aws-cdk/aws-pinpointemail/package.json index ac26c0fbcccc0..7659ce4025b4a 100644 --- a/packages/@aws-cdk/aws-pinpointemail/package.json +++ b/packages/@aws-cdk/aws-pinpointemail/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-qldb/package.json b/packages/@aws-cdk/aws-qldb/package.json index 6b902b32ece98..ac50a35b7c1aa 100644 --- a/packages/@aws-cdk/aws-qldb/package.json +++ b/packages/@aws-cdk/aws-qldb/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-quicksight/package.json b/packages/@aws-cdk/aws-quicksight/package.json index 5b1b626100d37..3d55f0052512b 100644 --- a/packages/@aws-cdk/aws-quicksight/package.json +++ b/packages/@aws-cdk/aws-quicksight/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-ram/package.json b/packages/@aws-cdk/aws-ram/package.json index f5d96286a4def..6383a8a7ce1a1 100644 --- a/packages/@aws-cdk/aws-ram/package.json +++ b/packages/@aws-cdk/aws-ram/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 4dfa680fe5abe..6144a8f13493b 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/cx-api": "0.0.0", diff --git a/packages/@aws-cdk/aws-redshift/package.json b/packages/@aws-cdk/aws-redshift/package.json index 4663e68c63c86..5ccccb2f4f2df 100644 --- a/packages/@aws-cdk/aws-redshift/package.json +++ b/packages/@aws-cdk/aws-redshift/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-resourcegroups/package.json b/packages/@aws-cdk/aws-resourcegroups/package.json index a8fb1b9692a1b..611c01ef93ae8 100644 --- a/packages/@aws-cdk/aws-resourcegroups/package.json +++ b/packages/@aws-cdk/aws-resourcegroups/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-robomaker/package.json b/packages/@aws-cdk/aws-robomaker/package.json index c6c5e5116fd17..8f4f5c4350cc7 100644 --- a/packages/@aws-cdk/aws-robomaker/package.json +++ b/packages/@aws-cdk/aws-robomaker/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-patterns/package.json b/packages/@aws-cdk/aws-route53-patterns/package.json index e4e4b00ea0679..4692f1c25903d 100644 --- a/packages/@aws-cdk/aws-route53-patterns/package.json +++ b/packages/@aws-cdk/aws-route53-patterns/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-targets/package.json b/packages/@aws-cdk/aws-route53-targets/package.json index 780d3eaa5ae5e..0d42c4d83462f 100644 --- a/packages/@aws-cdk/aws-route53-targets/package.json +++ b/packages/@aws-cdk/aws-route53-targets/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 564fce910f60e..4d72b2ed05616 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -73,9 +73,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", - "@types/nodeunit": "^0.0.31", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", + "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index 0adddf59c4e03..ab96545ce6345 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-assets/package.json b/packages/@aws-cdk/aws-s3-assets/package.json index 84586ad299090..987039fd3ab4b 100644 --- a/packages/@aws-cdk/aws-s3-assets/package.json +++ b/packages/@aws-cdk/aws-s3-assets/package.json @@ -71,7 +71,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index b3e459bd47261..c5ae455fc2928 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -80,7 +80,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/cx-api": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-s3-notifications/package.json b/packages/@aws-cdk/aws-s3-notifications/package.json index 9ccc7aeef64c4..62602ff1b6ce4 100644 --- a/packages/@aws-cdk/aws-s3-notifications/package.json +++ b/packages/@aws-cdk/aws-s3-notifications/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 866f2bfb6c7df..06e4248c943e7 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -73,8 +73,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/jest": "^26.0.23", + "@types/aws-lambda": "^8.10.78", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3objectlambda/package.json b/packages/@aws-cdk/aws-s3objectlambda/package.json index e9d370457a7ee..7818c261a21bc 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/package.json +++ b/packages/@aws-cdk/aws-s3objectlambda/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3outposts/package.json b/packages/@aws-cdk/aws-s3outposts/package.json index efbb11baea3e7..4645c65189333 100644 --- a/packages/@aws-cdk/aws-s3outposts/package.json +++ b/packages/@aws-cdk/aws-s3outposts/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-sagemaker/package.json b/packages/@aws-cdk/aws-sagemaker/package.json index aebaaba2813c2..3f2fa9d8ab857 100644 --- a/packages/@aws-cdk/aws-sagemaker/package.json +++ b/packages/@aws-cdk/aws-sagemaker/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index b7452bfb3ba08..0fa097ffba069 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-sdb/package.json b/packages/@aws-cdk/aws-sdb/package.json index 7786f7b50037b..b1773cf5a3907 100644 --- a/packages/@aws-cdk/aws-sdb/package.json +++ b/packages/@aws-cdk/aws-sdb/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index 1a51fdba46872..8e465f8d54c77 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -74,7 +74,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-securityhub/package.json b/packages/@aws-cdk/aws-securityhub/package.json index b49e9244b5fe7..ad2e1abf1e401 100644 --- a/packages/@aws-cdk/aws-securityhub/package.json +++ b/packages/@aws-cdk/aws-securityhub/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicecatalog/package.json b/packages/@aws-cdk/aws-servicecatalog/package.json index 2c7ea4a279712..e448e5aa5b559 100644 --- a/packages/@aws-cdk/aws-servicecatalog/package.json +++ b/packages/@aws-cdk/aws-servicecatalog/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json index e29e277575c45..ecf6b6990c3ad 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert-internal": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-servicediscovery/package.json b/packages/@aws-cdk/aws-servicediscovery/package.json index aecfddd3e5f08..6f336ff681706 100644 --- a/packages/@aws-cdk/aws-servicediscovery/package.json +++ b/packages/@aws-cdk/aws-servicediscovery/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ses-actions/package.json b/packages/@aws-cdk/aws-ses-actions/package.json index c27f63ff9e472..37ead111df047 100644 --- a/packages/@aws-cdk/aws-ses-actions/package.json +++ b/packages/@aws-cdk/aws-ses-actions/package.json @@ -65,7 +65,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ses/package.json b/packages/@aws-cdk/aws-ses/package.json index 7234e62501204..36c7fca9442e3 100644 --- a/packages/@aws-cdk/aws-ses/package.json +++ b/packages/@aws-cdk/aws-ses/package.json @@ -72,8 +72,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/nodeunit": "^0.0.31", + "@types/aws-lambda": "^8.10.78", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-signer/package.json b/packages/@aws-cdk/aws-signer/package.json index be95656d5aa57..e0318b1bcf011 100644 --- a/packages/@aws-cdk/aws-signer/package.json +++ b/packages/@aws-cdk/aws-signer/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-sns-subscriptions/package.json b/packages/@aws-cdk/aws-sns-subscriptions/package.json index 1b5da65d03a19..9d0b4db805c67 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/package.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/package.json @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index 09727cd171086..9ec8371fc3a6a 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -76,7 +76,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index 135d09b8ca4a1..7daec64f522f5 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -73,7 +73,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index cc2322ca7c5b9..dcf9ddfe31a17 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.31", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ssmcontacts/package.json b/packages/@aws-cdk/aws-ssmcontacts/package.json index 92baed5d4991f..fb7c8defd4291 100644 --- a/packages/@aws-cdk/aws-ssmcontacts/package.json +++ b/packages/@aws-cdk/aws-ssmcontacts/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-ssmincidents/package.json b/packages/@aws-cdk/aws-ssmincidents/package.json index 88d9c8a1aad01..1ef3f6cc287f7 100644 --- a/packages/@aws-cdk/aws-ssmincidents/package.json +++ b/packages/@aws-cdk/aws-ssmincidents/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-sso/package.json b/packages/@aws-cdk/aws-sso/package.json index b9e21e04cb25b..9eae6cfdc23a0 100644 --- a/packages/@aws-cdk/aws-sso/package.json +++ b/packages/@aws-cdk/aws-sso/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index bbf272be4873a..dc297c9d37bfc 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -71,7 +71,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-glue": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index f4d4079b6995d..485a879fee653 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-synthetics/package.json b/packages/@aws-cdk/aws-synthetics/package.json index 7c963a3d8dcd9..7febc7e87b686 100644 --- a/packages/@aws-cdk/aws-synthetics/package.json +++ b/packages/@aws-cdk/aws-synthetics/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-timestream/package.json b/packages/@aws-cdk/aws-timestream/package.json index b6d1d0921e26d..7a00e4fcb5c84 100644 --- a/packages/@aws-cdk/aws-timestream/package.json +++ b/packages/@aws-cdk/aws-timestream/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-transfer/package.json b/packages/@aws-cdk/aws-transfer/package.json index c761976d1404b..45fc2b0164fc1 100644 --- a/packages/@aws-cdk/aws-transfer/package.json +++ b/packages/@aws-cdk/aws-transfer/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-waf/package.json b/packages/@aws-cdk/aws-waf/package.json index 886bb0e3e35fd..ec2e3ec379265 100644 --- a/packages/@aws-cdk/aws-waf/package.json +++ b/packages/@aws-cdk/aws-waf/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-wafregional/package.json b/packages/@aws-cdk/aws-wafregional/package.json index a79a5b841661f..4003f4ff7b0f0 100644 --- a/packages/@aws-cdk/aws-wafregional/package.json +++ b/packages/@aws-cdk/aws-wafregional/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-wafv2/package.json b/packages/@aws-cdk/aws-wafv2/package.json index 1f8afbc0c51ae..f7c5ae3b14798 100644 --- a/packages/@aws-cdk/aws-wafv2/package.json +++ b/packages/@aws-cdk/aws-wafv2/package.json @@ -75,7 +75,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-workspaces/package.json b/packages/@aws-cdk/aws-workspaces/package.json index d50fb86e1c262..ab45c38ed0fbd 100644 --- a/packages/@aws-cdk/aws-workspaces/package.json +++ b/packages/@aws-cdk/aws-workspaces/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-xray/package.json b/packages/@aws-cdk/aws-xray/package.json index 22780d98fc754..1d63e8fa740b3 100644 --- a/packages/@aws-cdk/aws-xray/package.json +++ b/packages/@aws-cdk/aws-xray/package.json @@ -77,7 +77,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/assert-internal": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/cdk-assets-schema/package.json b/packages/@aws-cdk/cdk-assets-schema/package.json index ffc53ed507cd4..f4e5ef6cb1b5f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/package.json +++ b/packages/@aws-cdk/cdk-assets-schema/package.json @@ -52,7 +52,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/cfnspec/package.json b/packages/@aws-cdk/cfnspec/package.json index cc391710f7112..f9c34feb21e7a 100644 --- a/packages/@aws-cdk/cfnspec/package.json +++ b/packages/@aws-cdk/cfnspec/package.json @@ -26,9 +26,9 @@ "main": "lib/index.js", "types": "lib/index.d.ts", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/md5": "^2.3.0", - "@types/nodeunit": "^0.0.31", + "@types/fs-extra": "^8.1.2", + "@types/md5": "^2.3.1", + "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "fast-json-patch": "^2.2.1", "fs-extra": "^9.1.0", diff --git a/packages/@aws-cdk/cloud-assembly-schema/package.json b/packages/@aws-cdk/cloud-assembly-schema/package.json index f4ea4f798a06b..98689e858a5f8 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/package.json +++ b/packages/@aws-cdk/cloud-assembly-schema/package.json @@ -60,9 +60,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/mock-fs": "^4.13.0", - "@types/semver": "^7.3.6", + "@types/jest": "^26.0.24", + "@types/mock-fs": "^4.13.1", + "@types/semver": "^7.3.7", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "mock-fs": "^4.14.0", diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index c34b91e1cff3b..1fe5f51e907de 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -32,7 +32,7 @@ "table": "^6.7.1" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/string-width": "^4.0.1", "cdk-build-tools": "0.0.0", "fast-check": "^2.17.0", diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 926dd3d13b5d0..94c805c24cafd 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -400,7 +400,7 @@ "constructs": "^3.3.69" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 9abf3365cc3d2..80033471fbdc5 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -169,11 +169,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.77", - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/lodash": "^4.14.170", - "@types/minimatch": "^3.0.4", + "@types/aws-lambda": "^8.10.78", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/lodash": "^4.14.171", + "@types/minimatch": "^3.0.5", "@types/node": "^10.17.60", "@types/sinon": "^9.0.11", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 1eccaf9f810aa..67f181d8844f6 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,12 +73,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", - "@types/aws-lambda": "^8.10.77", - "@types/fs-extra": "^8.1.1", + "@types/aws-lambda": "^8.10.78", + "@types/fs-extra": "^8.1.2", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", @@ -86,7 +86,7 @@ "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "fs-extra": "^9.1.0", - "nock": "^13.1.0", + "nock": "^13.1.1", "pkglint": "0.0.0", "sinon": "^9.2.4", "@aws-cdk/assert-internal": "0.0.0" diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index 129d89f96e4a2..8dd8ed3b24ff5 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -66,9 +66,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", - "@types/mock-fs": "^4.13.0", - "@types/semver": "^7.3.6", + "@types/jest": "^26.0.24", + "@types/mock-fs": "^4.13.1", + "@types/semver": "^7.3.7", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "mock-fs": "^4.14.0", diff --git a/packages/@aws-cdk/example-construct-library/package.json b/packages/@aws-cdk/example-construct-library/package.json index a0f66c55abeeb..9eb248eba276e 100644 --- a/packages/@aws-cdk/example-construct-library/package.json +++ b/packages/@aws-cdk/example-construct-library/package.json @@ -66,7 +66,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/lambda-layer-awscli/package.json b/packages/@aws-cdk/lambda-layer-awscli/package.json index 896310c5a8842..582ff94d18b4f 100644 --- a/packages/@aws-cdk/lambda-layer-awscli/package.json +++ b/packages/@aws-cdk/lambda-layer-awscli/package.json @@ -66,7 +66,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/lambda-layer-kubectl/package.json b/packages/@aws-cdk/lambda-layer-kubectl/package.json index d02b93e717a39..514cc7071c299 100644 --- a/packages/@aws-cdk/lambda-layer-kubectl/package.json +++ b/packages/@aws-cdk/lambda-layer-kubectl/package.json @@ -66,7 +66,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 7c24bb5b83505..4963206e1d21f 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -32,7 +32,7 @@ "organization": true }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/region-info/package.json b/packages/@aws-cdk/region-info/package.json index ac5e03de5364b..c86fae71c220a 100644 --- a/packages/@aws-cdk/region-info/package.json +++ b/packages/@aws-cdk/region-info/package.json @@ -56,8 +56,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "fs-extra": "^9.1.0", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/yaml-cfn/package.json b/packages/@aws-cdk/yaml-cfn/package.json index c8219f94b9d38..ed33f49235536 100644 --- a/packages/@aws-cdk/yaml-cfn/package.json +++ b/packages/@aws-cdk/yaml-cfn/package.json @@ -69,7 +69,7 @@ "yaml": "1.10.2" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/yaml": "^1.9.7", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index 3b0b3312742f3..fa3ae197cad03 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -34,7 +34,7 @@ "license": "Apache-2.0", "devDependencies": { "@monocdk-experiment/rewrite-imports": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "constructs": "^3.3.69", diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index 0e8c3e2c3d042..71e1e67dbb146 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -38,8 +38,8 @@ "typescript": "~3.9.10" }, "devDependencies": { - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 8761fbbf8e44e..1a609044fbbdb 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -324,7 +324,7 @@ "@aws-cdk/lambda-layer-kubectl": "0.0.0", "@aws-cdk/pipelines": "0.0.0", "@aws-cdk/region-info": "0.0.0", - "@types/fs-extra": "^8.1.1", + "@types/fs-extra": "^8.1.2", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "constructs": "^3.3.69", diff --git a/packages/aws-cdk-migration/package.json b/packages/aws-cdk-migration/package.json index 873f3771f341b..714dd26493897 100644 --- a/packages/aws-cdk-migration/package.json +++ b/packages/aws-cdk-migration/package.json @@ -38,8 +38,8 @@ "typescript": "~3.9.10" }, "devDependencies": { - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 547268562516c..71d15e6442fdf 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -41,27 +41,27 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/core": "0.0.0", - "@octokit/rest": "^18.6.0", - "@types/archiver": "^5.1.0", - "@types/fs-extra": "^8.1.1", - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", - "@types/minimatch": "^3.0.4", - "@types/mockery": "^1.4.29", + "@octokit/rest": "^18.6.7", + "@types/archiver": "^5.3.0", + "@types/fs-extra": "^8.1.2", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", + "@types/minimatch": "^3.0.5", + "@types/mockery": "^1.4.30", "@types/node": "^10.17.60", - "@types/promptly": "^3.0.1", - "@types/semver": "^7.3.6", + "@types/promptly": "^3.0.2", + "@types/semver": "^7.3.7", "@types/sinon": "^9.0.11", "@types/table": "^6.0.0", - "@types/uuid": "^8.3.0", + "@types/uuid": "^8.3.1", "@types/wrap-ansi": "^3.0.0", - "@types/yargs": "^15.0.13", + "@types/yargs": "^15.0.14", "aws-sdk-mock": "^5.2.1", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "make-runnable": "^1.3.10", "mockery": "^2.1.0", - "nock": "^13.1.0", + "nock": "^13.1.1", "pkglint": "0.0.0", "sinon": "^9.2.4", "ts-jest": "^26.5.6", diff --git a/packages/awslint/package.json b/packages/awslint/package.json index bb1ed6687f630..da5906149f506 100644 --- a/packages/awslint/package.json +++ b/packages/awslint/package.json @@ -18,22 +18,22 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.30.0", + "@jsii/spec": "^1.31.0", "camelcase": "^6.2.0", "colors": "^1.4.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.30.0", + "jsii-reflect": "^1.31.0", "yargs": "^16.2.0" }, "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", "pkglint": "0.0.0", "typescript": "~3.9.10", - "@typescript-eslint/eslint-plugin": "^4.28.0", - "@typescript-eslint/parser": "^4.28.0", - "eslint": "^7.29.0", + "@typescript-eslint/eslint-plugin": "^4.28.3", + "@typescript-eslint/parser": "^4.28.3", + "eslint": "^7.30.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 2ba132a9149f7..6faafe1d543e2 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -33,14 +33,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/archiver": "^5.1.0", - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", + "@types/archiver": "^5.3.0", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", "@types/jszip": "^3.4.1", "@types/mime": "^2.0.3", - "@types/mock-fs": "^4.13.0", + "@types/mock-fs": "^4.13.1", "@types/node": "^10.17.60", - "@types/yargs": "^15.0.13", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "jszip": "^3.6.0", diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index 14ea4a05f00ed..72b65b88e9070 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -28,11 +28,11 @@ }, "license": "Apache-2.0", "dependencies": { - "codemaker": "^1.30.0", + "codemaker": "^1.31.0", "yaml": "1.10.2" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/yaml": "1.9.7", "jest": "^26.6.3", "typescript": "~3.9.10" diff --git a/packages/decdk/package.json b/packages/decdk/package.json index d3db39216e69b..bb1a848f9c335 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -233,18 +233,18 @@ "@aws-cdk/region-info": "0.0.0", "constructs": "^3.3.69", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.30.0", + "jsii-reflect": "^1.31.0", "jsonschema": "^1.4.0", "yaml": "1.10.2", "yargs": "^16.2.0" }, "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", "@types/yaml": "1.9.7", - "@types/yargs": "^15.0.13", + "@types/yargs": "^15.0.14", "jest": "^26.6.3", - "jsii": "^1.30.0" + "jsii": "^1.31.0" }, "keywords": [ "aws", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index a075a584e92bd..dc27ea678f33c 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -326,7 +326,7 @@ "@aws-cdk/pipelines": "0.0.0", "@aws-cdk/region-info": "0.0.0", "@aws-cdk/yaml-cfn": "0.0.0", - "@types/fs-extra": "^8.1.1", + "@types/fs-extra": "^8.1.2", "@types/node": "^10.17.60", "cdk-build-tools": "0.0.0", "constructs": "^3.3.69", diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 2eeede55c8548..3443058c4ea6b 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -35,18 +35,18 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/yargs": "^15.0.13", - "@types/semver": "^7.3.6", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", + "@types/semver": "^7.3.7", "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^4.28.0", - "@typescript-eslint/parser": "^4.28.0", + "@typescript-eslint/eslint-plugin": "^4.28.3", + "@typescript-eslint/parser": "^4.28.3", "awslint": "0.0.0", "colors": "^1.4.0", - "eslint": "^7.29.0", + "eslint": "^7.30.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", @@ -55,9 +55,9 @@ "fs-extra": "^9.1.0", "jest": "^26.6.3", "jest-junit": "^11.1.0", - "jsii": "^1.30.0", - "jsii-pacmak": "^1.30.0", - "jsii-reflect": "^1.30.0", + "jsii": "^1.31.0", + "jsii-pacmak": "^1.31.0", + "jsii-reflect": "^1.31.0", "markdownlint-cli": "^0.27.1", "nodeunit": "^0.11.3", "nyc": "^15.1.0", diff --git a/tools/cdk-integ-tools/package.json b/tools/cdk-integ-tools/package.json index a7aeb12c4cb92..6491ed0c2cec0 100644 --- a/tools/cdk-integ-tools/package.json +++ b/tools/cdk-integ-tools/package.json @@ -31,8 +31,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/tools/cdk-release/package.json b/tools/cdk-release/package.json index 3608c4f212378..1f69eefdebddc 100644 --- a/tools/cdk-release/package.json +++ b/tools/cdk-release/package.json @@ -28,9 +28,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0" diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index 2aa13012c7cc8..0caf46a5aa4fe 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -32,15 +32,15 @@ "license": "Apache-2.0", "dependencies": { "@aws-cdk/cfnspec": "0.0.0", - "codemaker": "^1.30.0", + "codemaker": "^1.31.0", "fast-json-patch": "^3.0.0-1", "fs-extra": "^9.1.0", "yargs": "^16.2.0" }, "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0" diff --git a/tools/eslint-plugin-cdk/package.json b/tools/eslint-plugin-cdk/package.json index 3d05756fc8c3b..b075f070208d0 100644 --- a/tools/eslint-plugin-cdk/package.json +++ b/tools/eslint-plugin-cdk/package.json @@ -14,9 +14,9 @@ "build+extract": "npm run build" }, "devDependencies": { - "@types/eslint": "^7.2.13", - "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.23", + "@types/eslint": "^7.28.0", + "@types/fs-extra": "^8.1.2", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "@types/estree": "*", "eslint-plugin-rulesdir": "^0.2.0", @@ -24,8 +24,8 @@ "typescript": "~3.9.10" }, "dependencies": { - "@typescript-eslint/parser": "^4.28.0", - "eslint": "^7.29.0", + "@typescript-eslint/parser": "^4.28.3", + "eslint": "^7.30.0", "fs-extra": "^9.1.0" }, "jest": { diff --git a/tools/nodeunit-shim/package.json b/tools/nodeunit-shim/package.json index 8bcd8ba63b77a..2c2814cc08a08 100644 --- a/tools/nodeunit-shim/package.json +++ b/tools/nodeunit-shim/package.json @@ -14,7 +14,7 @@ "build+extract": "npm run build" }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", "typescript": "~3.9.10" }, diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index 157f5faabddb4..9f6f3446c31b3 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -37,14 +37,14 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", - "@types/semver": "^7.3.6", - "@types/yargs": "^15.0.13", - "@typescript-eslint/eslint-plugin": "^4.28.0", - "@typescript-eslint/parser": "^4.28.0", - "eslint": "^7.29.0", + "@types/fs-extra": "^8.1.2", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", + "@types/semver": "^7.3.7", + "@types/yargs": "^15.0.14", + "@typescript-eslint/eslint-plugin": "^4.28.3", + "@typescript-eslint/parser": "^4.28.3", + "eslint": "^7.30.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", diff --git a/tools/pkgtools/package.json b/tools/pkgtools/package.json index c7550c41d272a..cf03f974a879a 100644 --- a/tools/pkgtools/package.json +++ b/tools/pkgtools/package.json @@ -31,8 +31,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", - "@types/yargs": "^15.0.13", + "@types/fs-extra": "^8.1.2", + "@types/yargs": "^15.0.14", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/tools/prlint/package.json b/tools/prlint/package.json index b850329eb15e6..88c3d70fb7393 100644 --- a/tools/prlint/package.json +++ b/tools/prlint/package.json @@ -19,9 +19,9 @@ "glob": "^7.1.7" }, "devDependencies": { - "@types/fs-extra": "^9.0.11", - "@types/glob": "^7.1.3", - "@types/jest": "^26.0.23", + "@types/fs-extra": "^9.0.12", + "@types/glob": "^7.1.4", + "@types/jest": "^26.0.24", "jest": "^26.6.3", "make-runnable": "^1.3.10", "typescript": "~3.9.10" diff --git a/tools/ubergen/package.json b/tools/ubergen/package.json index 0a15079209468..8cd1057e51ffd 100644 --- a/tools/ubergen/package.json +++ b/tools/ubergen/package.json @@ -29,7 +29,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/fs-extra": "^8.1.1", + "@types/fs-extra": "^8.1.2", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/tools/yarn-cling/package.json b/tools/yarn-cling/package.json index 248222d13cf0a..52501bf50f83e 100644 --- a/tools/yarn-cling/package.json +++ b/tools/yarn-cling/package.json @@ -40,10 +40,10 @@ ] }, "devDependencies": { - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "@types/node": "^10.17.60", - "@types/yarnpkg__lockfile": "^1.1.4", - "@types/semver": "^7.3.6", + "@types/yarnpkg__lockfile": "^1.1.5", + "@types/semver": "^7.3.7", "jest": "^26.6.3", "pkglint": "0.0.0", "typescript": "~3.9.10" diff --git a/yarn.lock b/yarn.lock index c1e70ddfc3533..51786b613db78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -351,6 +351,25 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" + integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + +"@hutson/parse-repository-url@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" + integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -538,10 +557,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jsii/spec@^1.30.0": - version "1.30.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.30.0.tgz#e5b2381b2be0b9c0839190f9f45d0a038654c73d" - integrity sha512-oXIwvZyHHc/TrwA/3pzQ3gkqBe916EWBvaexNI3rnKZujlHZT4vVVHMCjQ/kUJhcR0GEaahvwlNhiPTu6roC2g== +"@jsii/spec@^1.31.0": + version "1.31.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.31.0.tgz#9298dc163fdae0bab4006b817592235a29922871" + integrity sha512-qpJqZ+xj4lnKfk/HJYdYURDmHzh9aBIVOTgwd314AxKmwubDAajlAup+D2F9z9kylAB7GsQiva/SXgUlFjBeQw== dependencies: jsonschema "^1.4.0" @@ -1017,7 +1036,7 @@ npmlog "^4.1.2" upath "^2.0.1" -"@lerna/project@4.0.0": +"@lerna/project@4.0.0", "@lerna/project@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@lerna/project/-/project-4.0.0.tgz#ff84893935833533a74deff30c0e64ddb7f0ba6b" integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== @@ -1230,9 +1249,9 @@ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz#94c23db18ee4653e129abd26fb06f870ac9e1ee2" - integrity sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA== + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" @@ -1242,7 +1261,7 @@ resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a" integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q== -"@npmcli/git@^2.0.1": +"@npmcli/git@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== @@ -1333,10 +1352,10 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^7.3.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-7.3.2.tgz#065ce49b338043ec7f741316ce06afd4d459d944" - integrity sha512-oJhK/yhl9Gt430OrZOzAl2wJqR0No9445vmZ9Ey8GjUZUpwuu/vmEFP0TDhDXdpGDoxD6/EIFHJEcY8nHXpDTA== +"@octokit/openapi-types@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-8.3.0.tgz#8bc912edae8c03e002882cf1e29b595b7da9b441" + integrity sha512-ZFyQ30tNpoATI7o+Z9MWFUzUgWisB8yduhcky7S4UYsRijgIGSnwUKzPBDGzf/Xkx1DuvUtqzvmuFlDSqPJqmQ== "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" @@ -1351,11 +1370,11 @@ "@octokit/types" "^2.0.1" "@octokit/plugin-paginate-rest@^2.6.2": - version "2.13.5" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.5.tgz#e459f9b5dccbe0a53f039a355d5b80c0a2b0dc57" - integrity sha512-3WSAKBLa1RaR/7GG+LQR/tAZ9fp9H9waE9aPXallidyci9oZsfgsLn5M836d3LuDC6Fcym+2idRTBpssHZePVg== + version "2.14.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.14.0.tgz#f469cb4a908792fb44679c5973d8bba820c88b0f" + integrity sha512-S2uEu2uHeI7Vf+Lvj8tv3O5/5TCAa8GHS0dUQN7gdM7vKA6ZHAbR6HkAVm5yMb1mbedLEbxOuQ+Fa0SQ7tCDLA== dependencies: - "@octokit/types" "^6.13.0" + "@octokit/types" "^6.18.0" "@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.2": version "1.0.4" @@ -1370,12 +1389,12 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.3.1.tgz#deddce769b4ec3179170709ab42e4e9e6195aaa9" - integrity sha512-3B2iguGmkh6bQQaVOtCsS0gixrz8Lg0v4JuXPqBcFqLKuJtxAUf3K88RxMEf/naDOI73spD+goJ/o7Ie7Cvdjg== +"@octokit/plugin-rest-endpoint-methods@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.4.1.tgz#540ec90bb753dcaa682ee9f2cd6efdde9132fa90" + integrity sha512-Nx0g7I5ayAYghsLJP4Q1Ch2W9jYYM0FlWWWZocUro8rNxVwuZXGfFd7Rcqi9XDWepSXjg1WByiNJnZza2hIOvQ== dependencies: - "@octokit/types" "^6.16.2" + "@octokit/types" "^6.18.1" deprecation "^2.3.1" "@octokit/request-error@^1.0.2": @@ -1430,15 +1449,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^18.1.0", "@octokit/rest@^18.6.0": - version "18.6.0" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.6.0.tgz#9a8457374c78c2773d3ab3f50aaffc62f3ed4f76" - integrity sha512-MdHuXHDJM7e5sUBe3K9tt7th0cs4csKU5Bb52LRi2oHAeIMrMZ4XqaTrEv660HoUPoM1iDlnj27Ab/Nh3MtwlA== +"@octokit/rest@^18.1.0", "@octokit/rest@^18.6.7": + version "18.6.7" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.6.7.tgz#89b8ecd13edd9603f00453640d1fb0b4175d4b31" + integrity sha512-Kn6WrI2ZvmAztdx+HEaf88RuJn+LK72S8g6OpciE4kbZddAN84fu4fiPGxcEu052WmqKVnA/cnQsbNlrYC6rqQ== dependencies: "@octokit/core" "^3.5.0" "@octokit/plugin-paginate-rest" "^2.6.2" "@octokit/plugin-request-log" "^1.0.2" - "@octokit/plugin-rest-endpoint-methods" "5.3.1" + "@octokit/plugin-rest-endpoint-methods" "5.4.1" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -1447,12 +1466,12 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^6.0.3", "@octokit/types@^6.13.0", "@octokit/types@^6.16.1", "@octokit/types@^6.16.2": - version "6.16.4" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.16.4.tgz#d24f5e1bacd2fe96d61854b5bda0e88cf8288dfe" - integrity sha512-UxhWCdSzloULfUyamfOg4dJxV9B+XjgrIZscI0VCbp4eNrjmorGEw+4qdwcpTsu6DIrm9tQsFQS2pK5QkqQ04A== +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.18.0", "@octokit/types@^6.18.1": + version "6.19.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.19.0.tgz#e2b6fedb10c8b53cf4574aa5d1a8a5611295297a" + integrity sha512-9wdZFiJfonDyU6DjIgDHxAIn92vdSUBOwAXbO2F9rOFt6DJwuAkyGLu1CvdJPphCbPBoV9iSDMX7y4fu0v6AtA== dependencies: - "@octokit/openapi-types" "^7.3.2" + "@octokit/openapi-types" "^8.3.0" "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1", "@sinonjs/commons@^1.8.3": version "1.8.3" @@ -1503,22 +1522,22 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@types/archiver@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.1.0.tgz#869f4ce4028e49cf9a0243cf914415f4cc3d1f3d" - integrity sha512-baFOhanb/hxmcOd1Uey2TfFg43kTSmM6py1Eo7Rjbv/ivcl7PXLhY0QgXGf50Hx/eskGCFqPfhs/7IZLb15C5g== +"@types/archiver@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.0.tgz#2b34ba56d4d7102d256b922c7e91e09eab79db6f" + integrity sha512-qJ79qsmq7O/k9FYwsF6O1xVA1PeLV+9Bh3TYkVCu3VzMR6vN9JQkgEOh/rrQ0R+F4Ta+R3thHGewxQtFglwVfg== dependencies: "@types/glob" "*" -"@types/aws-lambda@^8.10.77": - version "8.10.77" - resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.77.tgz#04c4e3a06ab5552f2fa80816f8adca54b6bb9671" - integrity sha512-n0EMFJU/7u3KvHrR83l/zrKOVURXl5pUJPNED/Bzjah89QKCHwCiKCBoVUXRwTGRfCYGIDdinJaAlKDHZdp/Ng== +"@types/aws-lambda@^8.10.78": + version "8.10.78" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.78.tgz#dbb509837b6082962d6e7bc19f814e067ac9f5a2" + integrity sha512-+lZ8NuHT0qKEEpiZR4bF1G24SLrLwzdu0i9Cjdc3BGq6XJU6gBBYS5I0RJ8RdDCtgqgGdW8sOwsiZGHrC6mp0Q== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": - version "7.1.14" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" - integrity sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g== + version "7.1.15" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024" + integrity sha512-bxlMKPDbY8x5h6HBwVzEOk2C8fb6SLfYQ5Jw3uBYuYF1lfWk/kbLd81la82vrIkBb0l+JdmrZaDikPrNxpS/Ew== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -1527,58 +1546,58 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" - integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ== + version "7.6.3" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.3.tgz#f456b4b2ce79137f768aa130d2423d2f0ccfaba5" + integrity sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.0.tgz#0c888dd70b3ee9eebb6e4f200e809da0076262be" - integrity sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A== + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.11.1" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.1.tgz#654f6c4f67568e24c23b367e947098c6206fa639" - integrity sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw== + version "7.14.2" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" + integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== dependencies: "@babel/types" "^7.3.0" -"@types/eslint@^7.2.13": - version "7.2.13" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.13.tgz#e0ca7219ba5ded402062ad6f926d491ebb29dd53" - integrity sha512-LKmQCWAlnVHvvXq4oasNUMTJJb2GwSyTY8+1C7OH5ILR8mPLaljv1jxL1bXW3xB3jFbQxTKxJAvI8PyjB09aBg== +"@types/eslint@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a" + integrity sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A== dependencies: "@types/estree" "*" "@types/json-schema" "*" "@types/estree@*": - version "0.0.48" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.48.tgz#18dc8091b285df90db2f25aa7d906cfc394b7f74" - integrity sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew== + version "0.0.50" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== -"@types/fs-extra@^8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" - integrity sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w== +"@types/fs-extra@^8.1.2": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.2.tgz#7125cc2e4bdd9bd2fc83005ffdb1d0ba00cca61f" + integrity sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg== dependencies: "@types/node" "*" -"@types/fs-extra@^9.0.11": - version "9.0.11" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.11.tgz#8cc99e103499eab9f347dbc6ca4e99fb8d2c2b87" - integrity sha512-mZsifGG4QeQ7hlkhO56u7zt/ycBgGxSVsFI/6lGTU34VtwkiqrrSDgw0+ygs8kFGWcXnFQWMrzF2h7TtDFNixA== +"@types/fs-extra@^9.0.12": + version "9.0.12" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.12.tgz#9b8f27973df8a7a3920e8461517ebf8a7d4fdfaf" + integrity sha512-I+bsBr67CurCGnSenZZ7v94gd3tc3+Aj2taxMT4yu4ABLuOgOjeFxX3dokG24ztSRg5tnT00sL8BszO7gSMoIw== dependencies: "@types/node" "*" -"@types/glob@*", "@types/glob@^7.1.3": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== +"@types/glob@*", "@types/glob@^7.1.4": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" + integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA== dependencies: "@types/minimatch" "*" "@types/node" "*" @@ -1609,23 +1628,18 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^26.0.22", "@types/jest@^26.0.23": - version "26.0.23" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7" - integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA== +"@types/jest@^26.0.24": + version "26.0.24" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a" + integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w== dependencies: jest-diff "^26.0.0" pretty-format "^26.0.0" "@types/json-schema@*", "@types/json-schema@^7.0.7": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + version "7.0.8" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818" + integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg== "@types/jszip@^3.4.1": version "3.4.1" @@ -1634,15 +1648,15 @@ dependencies: jszip "*" -"@types/lodash@^4.14.170": - version "4.14.170" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" - integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== +"@types/lodash@^4.14.171": + version "4.14.171" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.171.tgz#f01b3a5fe3499e34b622c362a46a609fdb23573b" + integrity sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg== -"@types/md5@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.0.tgz#3b6a623091160f4dc75be3173e25f2110dc3fa1f" - integrity sha512-556YJ7ejzxIqSSxzyGGpctuZOarNZJt/zlEkhmmDc1f/slOEANHuwu2ZX7YaZ40rMiWoxt8GvAhoDpW1cmSy6A== +"@types/md5@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.1.tgz#010bcf3bb50a2cff3a574cb1c0b4051a9c67d6bc" + integrity sha512-OK3oe+ALIoPSo262lnhAYwpqFNXbiwH2a+0+Z5YBnkQEwWD8fk5+PIeRhYA48PzvX9I4SGNpWy+9bLj8qz92RQ== dependencies: "@types/node" "*" @@ -1651,32 +1665,32 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== -"@types/minimatch@*", "@types/minimatch@^3.0.3", "@types/minimatch@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" - integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== +"@types/minimatch@*", "@types/minimatch@^3.0.3", "@types/minimatch@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== "@types/minimist@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" - integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/mock-fs@^4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.0.tgz#b8b01cd2db588668b2532ecd21b1babd3fffb2c0" - integrity sha512-FUqxhURwqFtFBCuUj3uQMp7rPSQs//b3O9XecAVxhqS9y4/W8SIJEZFq2mmpnFVZBXwR/2OyPLE97CpyYiB8Mw== +"@types/mock-fs@^4.13.1": + version "4.13.1" + resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.1.tgz#9201554ceb23671badbfa8ac3f1fa9e0706305be" + integrity sha512-m6nFAJ3lBSnqbvDZioawRvpLXSaPyn52Srf7OfzjubYbYX8MTUdIgDxQl0wEapm4m/pNYSd9TXocpQ0TvZFlYA== dependencies: "@types/node" "*" -"@types/mockery@^1.4.29": - version "1.4.29" - resolved "https://registry.yarnpkg.com/@types/mockery/-/mockery-1.4.29.tgz#9ba22df37f07e3780fff8531d1a38e633f9457a5" - integrity sha1-m6It838H43gP/4Ux0aOOYz+UV6U= +"@types/mockery@^1.4.30": + version "1.4.30" + resolved "https://registry.yarnpkg.com/@types/mockery/-/mockery-1.4.30.tgz#25f07fa7340371c7ee0fb9239511a34e0a19d5b7" + integrity sha512-uv53RrNdhbkV/3VmVCtfImfYCWC3GTTRn3R11Whni3EJ+gb178tkZBVNj2edLY5CMrB749dQi+SJkg87jsN8UQ== "@types/node@*", "@types/node@>= 8": - version "15.12.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26" - integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA== + version "16.3.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.3.2.tgz#655432817f83b51ac869c2d51dd8305fb8342e16" + integrity sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw== "@types/node@^10.17.60": version "10.17.60" @@ -1684,19 +1698,19 @@ integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== "@types/node@^14.14.33": - version "14.17.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.4.tgz#218712242446fc868d0e007af29a4408c7765bc0" - integrity sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A== + version "14.17.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.5.tgz#b59daf6a7ffa461b5648456ca59050ba8e40ed54" + integrity sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA== -"@types/nodeunit@^0.0.31": - version "0.0.31" - resolved "https://registry.yarnpkg.com/@types/nodeunit/-/nodeunit-0.0.31.tgz#67eb52ad22326c7d1d9febe99d553f33b166126d" - integrity sha512-gZvDnqhHmp2IFzvQ59VJioI84/A+FZxGbp3OqoGhvQRfFQgbCqnK+SsYMWKfXODHpJfDbTnjvgoD+xeW05fQjg== +"@types/nodeunit@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/nodeunit/-/nodeunit-0.0.32.tgz#a41a76b0da07a2a79882e613f4b9fb4c4d123cc1" + integrity sha512-9n61KESiLGaKPpgp6ccSkpx0HVPe+ZNqxVdLMF2BaiQfbJIi0HIwboxmE3OxwgYqH7xsjuk/iCpSE4VVqC4w+Q== "@types/normalize-package-data@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" - integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== "@types/parse-json@^4.0.0": version "4.0.0" @@ -1704,14 +1718,14 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prettier@^2.0.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.0.tgz#2e8332cc7363f887d32ec5496b207d26ba8052bb" - integrity sha512-hkc1DATxFLQo4VxPDpMH1gCkPpBbpOoJ/4nhuXw4n63/0R6bCpQECj4+K226UJ4JO/eJQz+1mC2I7JsWanAdQw== + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.2.tgz#fc8c2825e4ed2142473b4a81064e6e081463d1b3" + integrity sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog== -"@types/promptly@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/promptly/-/promptly-3.0.1.tgz#206e29ebe55e2360f3e96067d4563efc8c29d8c7" - integrity sha512-NZkHlbRnB3ktYY9+dG38OpvXc04+eDMyFxiAr/LMLjD0bbDY9pW3HBctrXxLZUH0Tq6BkxWB6aMJJvaxQX36oA== +"@types/promptly@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/promptly/-/promptly-3.0.2.tgz#598674d4b78b3dffcb2d756b344f28a2cf7459f8" + integrity sha512-cJFwE7d8GlraY+DJoZ0NhpoJ55slkcbNsGIKMY0H+5h0xaGqXBqXz9zeu+Ey9KfN1UiHQXiIT0GroxyPYMPP/w== dependencies: "@types/node" "*" @@ -1725,10 +1739,10 @@ resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83" integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g== -"@types/semver@^7.3.6": - version "7.3.6" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.6.tgz#e9831776f4512a7ba6da53e71c26e5fb67882d63" - integrity sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw== +"@types/semver@^7.3.7": + version "7.3.7" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.7.tgz#b9eb89d7dfa70d5d1ce525bc1411a35347f533a3" + integrity sha512-4g1jrL98mdOIwSOUh6LTlB0Cs9I0dQPwINUhBg7C6pN4HLr8GS8xsksJxilW6S6dQHVi2K/o+lQuQcg7LroCnw== "@types/sinon@^9.0.11": version "9.0.11" @@ -1738,14 +1752,14 @@ "@types/sinonjs__fake-timers" "*" "@types/sinonjs__fake-timers@*": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" - integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.3.tgz#79df6f358ae8f79e628fe35a63608a0ea8e7cf08" + integrity sha512-E1dU4fzC9wN2QK2Cr1MLCfyHM8BoNnRFvuf45LYMPNDA+WqbNzC45S4UzPxvp1fFJ1rvSGU0bPvdd35VLmXG8g== "@types/stack-utils@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" - integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== "@types/string-width@^4.0.1": version "4.0.1" @@ -1761,10 +1775,10 @@ dependencies: table "*" -"@types/uuid@^8.3.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" - integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== +"@types/uuid@^8.3.1": + version "8.3.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" + integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== "@types/wrap-ansi@^3.0.0": version "3.0.0" @@ -1786,89 +1800,89 @@ yaml "*" "@types/yargs-parser@*": - version "20.2.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" - integrity sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA== + version "20.2.1" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" + integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== -"@types/yargs@^15.0.0", "@types/yargs@^15.0.13": - version "15.0.13" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.13.tgz#34f7fec8b389d7f3c1fd08026a5763e072d3c6dc" - integrity sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ== +"@types/yargs@^15.0.0", "@types/yargs@^15.0.14": + version "15.0.14" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" + integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== dependencies: "@types/yargs-parser" "*" -"@types/yarnpkg__lockfile@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" - integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== +"@types/yarnpkg__lockfile@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.5.tgz#9639020e1fb65120a2f4387db8f1e8b63efdf229" + integrity sha512-8NYnGOctzsI4W0ApsP/BIHD/LnxpJ6XaGf2AZmz4EyDYJMxtprN4279dLNI1CPZcwC9H18qYcaFv4bXi0wmokg== -"@typescript-eslint/eslint-plugin@^4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz#1a66f03b264844387beb7dc85e1f1d403bd1803f" - integrity sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ== +"@typescript-eslint/eslint-plugin@^4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.3.tgz#36cdcd9ca6f9e5cb49b9f61b970b1976708d084b" + integrity sha512-jW8sEFu1ZeaV8xzwsfi6Vgtty2jf7/lJmQmDkDruBjYAbx5DA8JtbcMnP0rNPUG+oH5GoQBTSp+9613BzuIpYg== dependencies: - "@typescript-eslint/experimental-utils" "4.28.0" - "@typescript-eslint/scope-manager" "4.28.0" + "@typescript-eslint/experimental-utils" "4.28.3" + "@typescript-eslint/scope-manager" "4.28.3" debug "^4.3.1" functional-red-black-tree "^1.0.1" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.28.0", "@typescript-eslint/experimental-utils@^4.0.1": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz#13167ed991320684bdc23588135ae62115b30ee0" - integrity sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ== +"@typescript-eslint/experimental-utils@4.28.3", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.3.tgz#976f8c1191b37105fd06658ed57ddfee4be361ca" + integrity sha512-zZYl9TnrxwEPi3FbyeX0ZnE8Hp7j3OCR+ELoUfbwGHGxWnHg9+OqSmkw2MoCVpZksPCZYpQzC559Ee9pJNHTQw== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.28.0" - "@typescript-eslint/types" "4.28.0" - "@typescript-eslint/typescript-estree" "4.28.0" + "@typescript-eslint/scope-manager" "4.28.3" + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/typescript-estree" "4.28.3" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa" - integrity sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A== +"@typescript-eslint/parser@^4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.3.tgz#95f1d475c08268edffdcb2779993c488b6434b44" + integrity sha512-ZyWEn34bJexn/JNYvLQab0Mo5e+qqQNhknxmc8azgNd4XqspVYR5oHq9O11fLwdZMRcj4by15ghSlIEq+H5ltQ== dependencies: - "@typescript-eslint/scope-manager" "4.28.0" - "@typescript-eslint/types" "4.28.0" - "@typescript-eslint/typescript-estree" "4.28.0" + "@typescript-eslint/scope-manager" "4.28.3" + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/typescript-estree" "4.28.3" debug "^4.3.1" -"@typescript-eslint/scope-manager@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz#6a3009d2ab64a30fc8a1e257a1a320067f36a0ce" - integrity sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg== +"@typescript-eslint/scope-manager@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.3.tgz#c32ad4491b3726db1ba34030b59ea922c214e371" + integrity sha512-/8lMisZ5NGIzGtJB+QizQ5eX4Xd8uxedFfMBXOKuJGP0oaBBVEMbJVddQKDXyyB0bPlmt8i6bHV89KbwOelJiQ== dependencies: - "@typescript-eslint/types" "4.28.0" - "@typescript-eslint/visitor-keys" "4.28.0" + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/visitor-keys" "4.28.3" -"@typescript-eslint/types@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0" - integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA== +"@typescript-eslint/types@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.3.tgz#8fffd436a3bada422c2c1da56060a0566a9506c7" + integrity sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA== -"@typescript-eslint/typescript-estree@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf" - integrity sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ== +"@typescript-eslint/typescript-estree@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz#253d7088100b2a38aefe3c8dd7bd1f8232ec46fb" + integrity sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w== dependencies: - "@typescript-eslint/types" "4.28.0" - "@typescript-eslint/visitor-keys" "4.28.0" + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/visitor-keys" "4.28.3" debug "^4.3.1" globby "^11.0.3" is-glob "^4.0.1" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434" - integrity sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw== +"@typescript-eslint/visitor-keys@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz#26ac91e84b23529968361045829da80a4e5251c4" + integrity sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg== dependencies: - "@typescript-eslint/types" "4.28.0" + "@typescript-eslint/types" "4.28.3" eslint-visitor-keys "^2.0.0" "@yarnpkg/lockfile@^1.1.0": @@ -1903,9 +1917,9 @@ acorn-globals@^6.0.0: acorn-walk "^7.1.1" acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^7.1.1: version "7.2.0" @@ -1918,9 +1932,9 @@ acorn@^7.1.1, acorn@^7.4.0: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.2.4: - version "8.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.0.tgz#af53266e698d7cffa416714b503066a82221be60" - integrity sha512-ULr0LDaEqQrMFGyQ3bhJkLsbtrQ8QibAseGZeaSUiT/6zb9IvIkomWHJIvgvwad+hinRAgsI51JcWk2yvwyL+w== + version "8.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" + integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== add-stream@^1.0.0: version "1.0.0" @@ -1962,9 +1976,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: uri-js "^4.2.2" ajv@^8.0.1: - version "8.6.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.0.tgz#60cc45d9c46a477d80d92c48076d972c342e5720" - integrity sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ== + version "8.6.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.1.tgz#ae65764bf1edde8cd861281cda5057852364a295" + integrity sha512-42VLtQUOLefAvKFAQIxIZDaThq6om/PrfP0CYk3/vn+y4BMNkKnbli8ON2QCiHov4KkzOSJ/xSoBJdayiiYvVQ== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -2136,11 +2150,6 @@ array-differ@^3.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -2260,9 +2269,9 @@ aws-sdk-mock@^5.2.1: traverse "^0.6.6" aws-sdk@^2.596.0, aws-sdk@^2.848.0, aws-sdk@^2.928.0: - version "2.932.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.932.0.tgz#43da32ab6de58a0eac6c7976feb6c9879fe09e7c" - integrity sha512-U6MWUtFD0npWa+ReVEgm0fCIM0fMOYahFp14GLv8fC+BWOTvh5Iwt/gF8NrLomx42bBjA1Abaw6yhmiaSJDQHQ== + version "2.945.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.945.0.tgz#ebd90832a664a192b12edf755af31be70dc18909" + integrity sha512-tkcoFAUol7c+9ZBnXsBTKfsj9bNckJ7uzj7FdD/a8AMt/6/18LlEISCiuHFl9qr8MItcON7UgnphJdFCTV7zBw== dependencies: buffer "4.9.2" events "1.1.1" @@ -2590,14 +2599,6 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -2607,11 +2608,6 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -2623,9 +2619,9 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001219: - version "1.0.30001239" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz#66e8669985bb2cb84ccb10f68c25ce6dd3e4d2b8" - integrity sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ== + version "1.0.30001245" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz#45b941bbd833cb0fa53861ff2bae746b3c6ca5d4" + integrity sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA== capture-exit@^2.0.0: version "2.0.0" @@ -2814,10 +2810,10 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= -codemaker@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.30.0.tgz#c718a5178e5bdd06d6ab2ddb629edc64de80cb51" - integrity sha512-yntR55JhhVlZTfR4CPV6IrCULovPDrk3z0yQR7/ygEtNxEOQrHhX17djJ0rVmIwCJUawv+ODTJ1ipJY9CbxJQw== +codemaker@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.31.0.tgz#1987d8d2dcb39883844134d50c85f33f29f0cb62" + integrity sha512-gyWhtZ4YU5b+pIijCfOZkGrH0DCkUQXyRG3BQtDlnwFJuXyJnDoz+dpM5ErkJuDD9w6Qns4aryyG/bU78huaSg== dependencies: camelcase "^6.2.0" decamelize "^5.0.0" @@ -2962,9 +2958,9 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= constructs@^3.3.69: - version "3.3.87" - resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.87.tgz#374d8660c6a40148af6d9c0125918405ab1366f1" - integrity sha512-/SQrY1RP9KQsSyyVeyBTSMIiC5yOJO635YVqPH6XBsS5MMkBHd4gsPOX4FkpkDLMC1shAoK0FSTLatgxFiS4+A== + version "3.3.97" + resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.97.tgz#751cc8955ee29381da9ee05f39141f02d6164aee" + integrity sha512-KDemmmUBgTDd2OKVOZkVEJM1LwP/bzm+cs2l/v1UYctIUl2X4LW+MrK7Ajd8blKkS5Vp6edkQSTSHUJnR/413w== conventional-changelog-angular@^5.0.12: version "5.0.12" @@ -2999,7 +2995,7 @@ conventional-changelog-codemirror@^2.0.8: dependencies: q "^1.5.1" -conventional-changelog-config-spec@2.1.0: +conventional-changelog-config-spec@2.1.0, conventional-changelog-config-spec@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz#874a635287ef8b581fd8558532bf655d4fb59f2d" integrity sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ== @@ -3023,15 +3019,15 @@ conventional-changelog-conventionalcommits@^4.5.0: q "^1.5.1" conventional-changelog-core@^4.2.1, conventional-changelog-core@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.2.tgz#f0897df6d53b5d63dec36b9442bd45354f8b3ce5" - integrity sha512-7pDpRUiobQDNkwHyJG7k9f6maPo9tfPzkSWbRq97GGiZqisElhnvUZSvyQH20ogfOjntB5aadvv6NNcKL1sReg== + version "4.2.3" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.3.tgz#ce44d4bbba4032e3dc14c00fcd5b53fc00b66433" + integrity sha512-MwnZjIoMRL3jtPH5GywVNqetGILC7g6RQFvdb8LRU/fA/338JbeWAku3PZ8yQ+mtVRViiISqJlb0sOz0htBZig== dependencies: add-stream "^1.0.0" - conventional-changelog-writer "^4.0.18" + conventional-changelog-writer "^5.0.0" conventional-commits-parser "^3.2.0" dateformat "^3.0.0" - get-pkg-repo "^1.0.0" + get-pkg-repo "^4.0.0" git-raw-commits "^2.0.8" git-remote-origin-url "^2.0.0" git-semver-tags "^4.1.1" @@ -3040,7 +3036,6 @@ conventional-changelog-core@^4.2.1, conventional-changelog-core@^4.2.2: q "^1.5.1" read-pkg "^3.0.0" read-pkg-up "^3.0.0" - shelljs "^0.8.3" through2 "^4.0.0" conventional-changelog-ember@^2.0.9: @@ -3084,7 +3079,7 @@ conventional-changelog-preset-loader@^2.3.4: resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== -conventional-changelog-writer@^4.0.18: +conventional-changelog-writer@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" integrity sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw== @@ -3100,6 +3095,21 @@ conventional-changelog-writer@^4.0.18: split "^1.0.0" through2 "^4.0.0" +conventional-changelog-writer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz#c4042f3f1542f2f41d7d2e0d6cad23aba8df8eec" + integrity sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g== + dependencies: + conventional-commits-filter "^2.0.7" + dateformat "^3.0.0" + handlebars "^4.7.6" + json-stringify-safe "^5.0.1" + lodash "^4.17.15" + meow "^8.0.0" + semver "^6.0.0" + split "^1.0.0" + through2 "^4.0.0" + conventional-changelog@3.1.24, conventional-changelog@^3.1.24: version "3.1.24" resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.24.tgz#ebd180b0fd1b2e1f0095c4b04fd088698348a464" @@ -3181,9 +3191,9 @@ cosmiconfig@^7.0.0: yaml "^1.10.0" coveralls@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.1.0.tgz#13c754d5e7a2dd8b44fe5269e21ca394fb4d615b" - integrity sha512-sHxOu2ELzW8/NC1UP5XVLbZDzO4S3VxfFye3XYCznopHy02YjNkHcj5bKaVw2O7hVaBdBjEdQGpie4II1mWhuQ== + version "3.1.1" + resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.1.1.tgz#f5d4431d8b5ae69c5079c8f8ca00d64ac77cf081" + integrity sha512-+dxnG2NHncSD1NrqbSM3dn/lE57O6Qf/koe9+I7c+wzkqRmEvcp0kgJdxKInzYzkICKkFMZsX3Vct3++tsF9ww== dependencies: js-yaml "^3.13.1" lcov-parse "^1.0.0" @@ -3273,13 +3283,6 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -3322,9 +3325,9 @@ dateformat@^3.0.0: integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" @@ -3355,7 +3358,7 @@ decamelize-keys@^1.1.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.0, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -3366,9 +3369,9 @@ decamelize@^5.0.0: integrity sha512-U75DcT5hrio3KNtvdULAWnLiAPbFUC4191ldxMmj4FA/mRuBnmDwU0boNfPyFRhnan+Jm+haLeSn3P0afcBn4w== decimal.js@^10.2.1: - version "10.3.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.0.tgz#96fd481189818e0d5810c18ac147824b9e4c0026" - integrity sha512-MrQRs2gyD//7NeHi9TtsfClkf+cFAewDz+PZHR8ILKglLmBMyVX3ymQ+oeznE3tjrS7beTN+6JXb2C3JDHm7ug== + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== decode-uri-component@^0.2.0: version "0.2.0" @@ -3505,7 +3508,7 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= -detect-indent@^6.0.0: +detect-indent@^6.0.0, detect-indent@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== @@ -3641,9 +3644,9 @@ ejs@^2.5.2: integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== electron-to-chromium@^1.3.723: - version "1.3.755" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.755.tgz#4b6101f13de910cf3f0a1789ddc57328133b9332" - integrity sha512-BJ1s/kuUuOeo1bF/EM2E4yqW9te0Hpof3wgwBx40AWJE18zsD1Tqo0kr7ijnOc+lRsrlrqKPauJAHqaxOItoUA== + version "1.3.775" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082" + integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q== emittery@^0.7.1: version "0.7.2" @@ -3701,7 +3704,7 @@ err-code@^2.0.2: resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3763,10 +3766,10 @@ es6-error@^4.0.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild@^0.12.9: - version "0.12.9" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.9.tgz#bed4e7087c286cd81d975631f77d47feb1660070" - integrity sha512-MWRhAbMOJ9RJygCrt778rz/qNYgA4ZVj6aXnNPxFjs7PmIpb0fuB9Gmg5uWrr6n++XKwwm/RmSz6RR5JL2Ocsw== +esbuild@^0.12.15: + version "0.12.15" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.15.tgz#9d99cf39aeb2188265c5983e983e236829f08af0" + integrity sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw== escalade@^3.1.1: version "3.1.1" @@ -3939,13 +3942,14 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint@^7.29.0: - version "7.29.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0" - integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA== +eslint@^7.30.0: + version "7.30.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.30.0.tgz#6d34ab51aaa56112fd97166226c9a97f505474f8" + integrity sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg== dependencies: "@babel/code-frame" "7.12.11" "@eslint/eslintrc" "^0.4.2" + "@humanwhocodes/config-array" "^0.5.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -4201,16 +4205,15 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + version "3.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" fast-json-patch@^2.2.1: version "2.2.1" @@ -4235,9 +4238,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + version "1.11.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.1.tgz#5d8175aae17db61947f8b162cfc7f63264d22807" + integrity sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw== dependencies: reusify "^1.0.4" @@ -4315,14 +4318,6 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -4374,9 +4369,9 @@ flatted@^2.0.1: integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== flatted@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" - integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + version "3.2.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.1.tgz#bbef080d95fca6709362c73044a1634f7c6e7d05" + integrity sha512-OMQjaErSFHmHqZe+PSidH5n8j3O0F2DdnVh8JB4j4eUQ2k6KvB0qGfrKIhapvez5JerBbmWkaLYUYWISaESoXg== follow-redirects@^1.10.0, follow-redirects@^1.11.0: version "1.14.1" @@ -4574,15 +4569,14 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-pkg-repo@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" - integrity sha1-xztInAbYDMVTbCyFP54FIyBWly0= +get-pkg-repo@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.1.2.tgz#c4ffd60015cf091be666a0212753fc158f01a4c0" + integrity sha512-/FjamZL9cBYllEbReZkxF2IMh80d8TJoC4e3bmLNif8ibHw95aj0N/tzqK0kZz9eU/3w3dL6lF4fnnX/sDdW3A== dependencies: - hosted-git-info "^2.1.4" - meow "^3.3.0" - normalize-package-data "^2.3.0" - parse-github-repo-url "^1.3.0" + "@hutson/parse-repository-url" "^3.0.0" + hosted-git-info "^4.0.0" + meow "^7.0.0" through2 "^2.0.0" get-port@^5.1.1: @@ -4590,11 +4584,6 @@ get-port@^5.1.1: resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - get-stdin@~8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" @@ -4643,7 +4632,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -git-raw-commits@^2.0.8: +git-raw-commits@^2.0.10, git-raw-commits@^2.0.8: version "2.0.10" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== @@ -4671,17 +4660,17 @@ git-semver-tags@^4.0.0, git-semver-tags@^4.1.1: semver "^6.0.0" git-up@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.2.tgz#10c3d731051b366dc19d3df454bfca3f77913a7c" - integrity sha512-kbuvus1dWQB2sSW4cbfTeGpCMd8ge9jx9RKnhXhuJ7tnvT+NIrTVfYZxjtflZddQYcmdOTlkAcjmx7bor+15AQ== + version "4.0.5" + resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.5.tgz#e7bb70981a37ea2fb8fe049669800a1f9a01d759" + integrity sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA== dependencies: is-ssh "^1.3.0" - parse-url "^5.0.0" + parse-url "^6.0.0" git-url-parse@^11.4.4: - version "11.4.4" - resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" - integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== + version "11.5.0" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.5.0.tgz#acaaf65239cb1536185b19165a24bbc754b3f764" + integrity sha512-TZYSMDeM37r71Lqg1mbnMlOqlHd7BSij9qN7XwTkRqSAYFMihGLGhfHwgqQob3GUhEneKnV4nskN9rbQw2KGxA== dependencies: git-up "^4.0.0" @@ -4702,14 +4691,14 @@ github-api@^3.4.0: js-base64 "^2.1.9" utf8 "^2.1.1" -glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@^5.1.2: +glob-parent@^5.1.1, glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@^7.0.0, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@~7.1.6: +glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@~7.1.6: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== @@ -4727,9 +4716,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.9.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb" - integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA== + version "13.10.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676" + integrity sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g== dependencies: type-fest "^0.20.2" @@ -4873,7 +4862,7 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== -hosted-git-info@^4.0.1: +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== @@ -5018,13 +5007,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -5086,11 +5068,6 @@ inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -5152,9 +5129,9 @@ is-ci@^2.0.0: ci-info "^2.0.0" is-core-module@^2.2.0, is-core-module@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" - integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" + integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg== dependencies: has "^1.0.3" @@ -5217,11 +5194,6 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -5373,11 +5345,6 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - is-weakmap@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" @@ -6016,65 +5983,65 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.30.0.tgz#b905760ddf5e29c6c6ef31b8c670a2d1db7291c0" - integrity sha512-74GeV8ab8BrS3k8h8HKnI8f5PecsRahflElxJuc6bI9xA5AhRAzBF/Lrt5HibuYPuSsyLAmhTU1GTHdRvKq8aA== +jsii-diff@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.31.0.tgz#7f32b340cf340cc1929f4d534bdfa6495fc09bed" + integrity sha512-eEKFfZXGXxlWFg7E0F4h2UGOnpVCzHclM586SE4KnMwHzSlpRrdYrXa2KhFQSLs/gpZofDV4rPLZ9UDLvNu75Q== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" fs-extra "^9.1.0" - jsii-reflect "^1.30.0" + jsii-reflect "^1.31.0" log4js "^6.3.0" typescript "~3.9.9" yargs "^16.2.0" -jsii-pacmak@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.30.0.tgz#a6a7570da1388027ce4e5ca1603d4144f341d307" - integrity sha512-hYvISYBXZ5WL/+LtG3HpVrimguqAoWa3D8jaqsnoiIGrdmaxKCZ0VnioJYxEX7wVamYuCwXu5NFx/b31BspU6A== +jsii-pacmak@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.31.0.tgz#7e4fa67f1de582be04263904aa45966d84210996" + integrity sha512-fGiAoooRPMadwTWU0vfHJdcNzeYdESnkU/8LmlI4k6yF1iIlFMIbWPulBxP6fV7SqV3CZQKGpUbcPD/Uzf1glg== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" clone "^2.1.2" - codemaker "^1.30.0" + codemaker "^1.31.0" commonmark "^0.29.3" escape-string-regexp "^4.0.0" fs-extra "^9.1.0" - jsii-reflect "^1.30.0" - jsii-rosetta "^1.30.0" + jsii-reflect "^1.31.0" + jsii-rosetta "^1.31.0" semver "^7.3.5" spdx-license-list "^6.4.0" xmlbuilder "^15.1.1" yargs "^16.2.0" -jsii-reflect@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.30.0.tgz#b079d448ed35c9d9dfea8798a8ef39487ed0c86c" - integrity sha512-t/1Zr1gGqQSYt94Lfq860VLnCr8y8MLvlLorWYqmBeWKCaSPhtYSC1blGhZhDrAW+CBXiT0Oy64j4Q++AntRmw== +jsii-reflect@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.31.0.tgz#83acdae835071c734bb8847cf3cad7ccc4497540" + integrity sha512-jKc3tryVeEyEBZFv5bDB8rOaEgW+yBPh0DE4GQCKQQLdkp76Lm9ZSkrnJk5e0gEuAWsmuc1DUs35OcVNr8QRWg== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" colors "^1.4.0" fs-extra "^9.1.0" - oo-ascii-tree "^1.30.0" + oo-ascii-tree "^1.31.0" yargs "^16.2.0" -jsii-rosetta@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.30.0.tgz#5c974eefef9a8e5e1b8364e53e6856f07c7eaf68" - integrity sha512-ChFg5qhvxCaM2bspCqizs48yMtsm5YLHjBoNZLCkbXyc3yMM5l8pnn787B5ww5TI3+tKxKYWkbiKf356kQ1OgQ== +jsii-rosetta@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.31.0.tgz#f5174b532b4c3a79eadd9ed059aa33bee21e3225" + integrity sha512-Heu6D+yI5mmUklLQdX3PdDvHUQm14618Fj4PQM9seKa4cohxzJ7EHopfRObKYHMko9awopx4Qr7Gtu6u/QPqfw== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" commonmark "^0.29.3" fs-extra "^9.1.0" typescript "~3.9.9" xmldom "^0.6.0" yargs "^16.2.0" -jsii@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.30.0.tgz#fe20f60e33d0beaae24bc6537fb623333e913da4" - integrity sha512-TfVHhGjP0QiTEkyfnxrDIE8Da+itxnNUK2YoD69qIPAzmZ58goKVqK4sbXrXz2urHSToGLDmWI8+H69cLeVjJw== +jsii@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.31.0.tgz#513ff04581eae233accef2e2ce06a19d9bd4d972" + integrity sha512-q/p5a6OLO9V0pIcyzS5sygkU9lPskY57KM7KbmppLDPVi5nIqpsRyFfsbPnGWFfDBMk//nkcfj+dbKJIplVkgg== dependencies: - "@jsii/spec" "^1.30.0" + "@jsii/spec" "^1.31.0" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.5" @@ -6138,20 +6105,13 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@2.x, json5@^2.1.2: +json5@2.x, json5@^2.1.2, json5@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== dependencies: minimist "^1.2.5" -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - jsonc-parser@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" @@ -6364,17 +6324,6 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -6536,14 +6485,6 @@ log4js@^6.3.0: rfdc "^1.1.4" streamroller "^2.2.4" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -6654,7 +6595,7 @@ map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-obj@^1.0.0, map-obj@^1.0.1: +map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= @@ -6728,21 +6669,22 @@ mdurl@^1.0.1, mdurl@~1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" +meow@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" + integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.13.1" + yargs-parser "^18.1.3" meow@^8.0.0: version "8.1.2" @@ -6802,7 +6744,7 @@ micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -6853,7 +6795,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: +minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -7084,10 +7026,10 @@ nise@^5.1.0: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.1.0.tgz#41c8ce8b35ab7d618c4cbf40de1d5bce319979ba" - integrity sha512-3N3DUY8XYrxxzWazQ+nSBpiaJ3q6gcpNh4gXovC/QBxrsvNp4tq+wsLHF6mJ3nrn3lPLn7KCJqKxy/9aD+0fdw== +nock@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.1.1.tgz#3c830129d4560957f59b6f480a41ddbaf9cf57af" + integrity sha512-YKTR9MjfK3kS9/l4nuTxyYm30cgOExRHzkLNhL8nhEUyU4f8Za/dRxOqjhVT1vGs0svWo3dDnJTUX1qxYeWy5w== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -7189,7 +7131,7 @@ nopt@^5.0.0: dependencies: abbrev "1" -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -7221,10 +7163,10 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== +normalize-url@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== npm-bundled@^1.1.1, npm-bundled@^1.1.2: version "1.1.2" @@ -7417,7 +7359,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -7432,9 +7374,9 @@ object-copy@^0.1.0: kind-of "^3.0.3" object-inspect@^1.10.3, object-inspect@^1.9.0: - version "1.10.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" - integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== + version "1.11.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== object-is@^1.1.4: version "1.1.5" @@ -7510,10 +7452,10 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.30.0.tgz#5a20204d05370c0578b800836ed1e8c660d3c4e0" - integrity sha512-TzXuoCnha2QHFcAR+8+tBgD7Wnn6Uh+P3aZMoXKDJ3CVLXFnTnzHy4WMmmz01pTfv+f5haQMjhL9OIFJLEZ5kA== +oo-ascii-tree@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.31.0.tgz#36e10dcad35ba767db41c2d2050ff2174f3d5e6f" + integrity sha512-gNb2MyP1ZcF7cX0WgsAjYe4gZcx7BMLBWKE2TJZZbQ9/j4D8gbJh5Aq6RlXBgev74ODlgAVVcPr2wKU4Dufhqg== open@^7.4.2: version "7.4.2" @@ -7755,11 +7697,11 @@ package-hash@^4.0.0: release-zalgo "^1.0.0" pacote@^11.2.6: - version "11.3.4" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.4.tgz#c290b790a5cee3082bb8fa223f3f3e2fdf3d0bfc" - integrity sha512-RfahPCunM9GI7ryJV/zY0bWQiokZyLqaSNHXtbNSoLb7bwTvBbJBEyCJ01KWs4j1Gj7GmX8crYXQ1sNX6P2VKA== + version "11.3.5" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.5.tgz#73cf1fc3772b533f575e39efa96c50be8c3dc9d2" + integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== dependencies: - "@npmcli/git" "^2.0.1" + "@npmcli/git" "^2.1.0" "@npmcli/installed-package-contents" "^1.0.6" "@npmcli/promise-spawn" "^1.2.0" "@npmcli/run-script" "^1.8.2" @@ -7791,18 +7733,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-github-repo-url@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" - integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -7831,13 +7761,13 @@ parse-path@^4.0.0: qs "^6.9.4" query-string "^6.13.8" -parse-url@^5.0.0: - version "5.0.5" - resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-5.0.5.tgz#06b7f6978b65cac7851bb768bec4e0b950714e1a" - integrity sha512-AwfVhXaQrNNI6UPUJq/GJN2qoY0L9gPgxhh9VbDP0bfBAJWaC/Zh8hjQ58YKTi4AagOT70fpadkYSKPo+eFb1w== +parse-url@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-6.0.0.tgz#f5dd262a7de9ec00914939220410b66cff09107d" + integrity sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw== dependencies: is-ssh "^1.3.0" - normalize-url "4.5.0" + normalize-url "^6.1.0" parse-path "^4.0.0" protocols "^1.4.0" @@ -7870,13 +7800,6 @@ patch-package@^6.4.7: slash "^2.0.0" tmp "^0.0.33" -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -7914,15 +7837,6 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -7940,12 +7854,12 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -pify@^2.0.0, pify@^2.3.0: +pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -7965,18 +7879,6 @@ pify@^5.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - pirates@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" @@ -8279,14 +8181,6 @@ read-package-tree@^5.3.1: readdir-scoped-modules "^1.0.0" util-promisify "^2.1.0" -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -8312,15 +8206,6 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -8396,21 +8281,6 @@ readdir-scoped-modules@^1.0.0: graceful-fs "^4.1.2" once "^1.3.0" -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -8462,13 +8332,6 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -8532,7 +8395,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.20.0: +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -8740,15 +8603,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@^0.8.3: - version "0.8.4" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" - integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -9188,13 +9042,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -9215,13 +9062,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -9588,11 +9428,6 @@ traverse@^0.6.6: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -9658,12 +9493,11 @@ tsame@^2.0.1: integrity sha512-jxyxgKVKa4Bh5dPcO42TJL22lIvfd9LOVJwdovKOnJa4TLLrHxquK+DlGm4rkGmrcur+GRx+x4oW00O2pY/fFw== tsconfig-paths@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + version "3.10.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz#79ae67a68c15289fdf5c51cb74f397522d795ed7" + integrity sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q== dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" + json5 "^2.2.0" minimist "^1.2.0" strip-bom "^3.0.0" @@ -9720,6 +9554,11 @@ type-detect@4.0.8, type-detect@^4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -9796,9 +9635,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-js@^3.1.4: - version "3.13.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.9.tgz#4d8d21dcd497f29cfd8e9378b9df123ad025999b" - integrity sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g== + version "3.13.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.10.tgz#a6bd0d28d38f592c3adb6b180ea6e07e1e540a8d" + integrity sha512-57H3ACYFXeo1IaZ1w02sfA71wI60MGco/IQFjOqK+WtKoprh7Go2/yvd2HPtoJILO2Or84ncLccI4xoHMTSbGg== uid-number@0.0.6: version "0.0.6" @@ -10043,9 +9882,9 @@ whatwg-mimetype@^2.3.0: integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.6.0.tgz#27c0205a4902084b872aecb97cf0f2a7a3011f4c" - integrity sha512-os0KkeeqUOl7ccdDT1qqUcS4KH4tcBTSKK5Nl5WKb2lyxInIZ/CpjkqKa1Ss12mjfdcRX9mHmPPs7/SxG1Hbdw== + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== dependencies: lodash "^4.7.0" tr46 "^2.1.0" @@ -10213,9 +10052,9 @@ write-pkg@^4.0.0: write-json-file "^3.2.0" ws@^7.4.5: - version "7.5.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" - integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== + version "7.5.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" + integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== xml-js@^1.6.11: version "1.6.11" @@ -10330,7 +10169,7 @@ yargs-parser@^13.0.0, yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.2: +yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== From 594d7c664abed631163ec6b5cfede0a61acb0602 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 14 Jul 2021 19:43:01 +0200 Subject: [PATCH 050/105] fix(ecr-assets): There is already a Construct with name 'Staging' when using tarball image (#15540) Use `this` and not `scope` for the scope of `AssetStaging` in `TarballImageAsset`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts | 2 +- .../aws-ecr-assets/test/tarball-asset.test.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts index 48af505e1148e..bb7c40617b4ea 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/tarball-asset.ts @@ -60,7 +60,7 @@ export class TarballImageAsset extends CoreConstruct implements IAsset { throw new Error(`Cannot find file at ${props.tarballFile}`); } - const stagedTarball = new AssetStaging(scope, 'Staging', { sourcePath: props.tarballFile }); + const stagedTarball = new AssetStaging(this, 'Staging', { sourcePath: props.tarballFile }); this.sourceHash = stagedTarball.assetHash; this.assetHash = stagedTarball.assetHash; diff --git a/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts index c4654fed87044..20d7517e23915 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/tarball-asset.test.ts @@ -16,7 +16,7 @@ describe('image asset', () => { testFutureBehavior('test instantiating Asset Image', flags, App, (app) => { // GIVEN const stack = new Stack(app); - const assset = new TarballImageAsset(stack, 'Image', { + const asset = new TarballImageAsset(stack, 'Image', { tarballFile: __dirname + '/demo-tarball/empty.tar', }); @@ -30,23 +30,26 @@ describe('image asset', () => { expect(Object.keys(manifest.files ?? {}).length).toBe(1); expect(Object.keys(manifest.dockerImages ?? {}).length).toBe(1); - expect(manifest.dockerImages?.[assset.assetHash]?.destinations?.['current_account-current_region']).toStrictEqual( + expect(manifest.dockerImages?.[asset.assetHash]?.destinations?.['current_account-current_region']).toStrictEqual( { assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}', - imageTag: assset.assetHash, + imageTag: asset.assetHash, repositoryName: 'cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}', }, ); - expect(manifest.dockerImages?.[assset.assetHash]?.source).toStrictEqual( + expect(manifest.dockerImages?.[asset.assetHash]?.source).toStrictEqual( { executable: [ 'sh', '-c', - `docker load -i asset.${assset.assetHash}.tar | sed "s/Loaded image: //g"`, + `docker load -i asset.${asset.assetHash}.tar | sed "s/Loaded image: //g"`, ], }, ); + + // AssetStaging in TarballImageAsset uses `this` as scope' + expect(asset.node.tryFindChild('Staging')).toBeDefined(); }); testFutureBehavior('asset.repository.grantPull can be used to grant a principal permissions to use the image', flags, App, (app) => { From b06f7bf8ee59379a3478e4200b941635174c777e Mon Sep 17 00:00:00 2001 From: arcrank Date: Wed, 14 Jul 2021 18:56:58 -0400 Subject: [PATCH 051/105] feat(servicecatalog): Add portfolio-product association and tag update constraint (#15452) Adding ability to associate a product with a portfolio. Adding initial framework for constraints and the first actual one, starting here with TagUpdate Constraint. Since there is considerable amount of code in the `association-manager` it felt more manageable to split these PRs so that adding the other constraints only requires adding the APIs and then direct construction methods. BREAKING CHANGE: `AcceptLanguage` enum has been renamed to `MessageLanguage`, and fields that accepted this enum have been updated to reflect this change. * **servicecatalog:** property `acceptLanguage` in `PortfolioShareOptions` has been renamed to `messageLanguage`. * **servicecatalog:** property `acceptLanguage` in `PortfolioProps` has been renamed to `messageLanguage`. * **servicecatalog:** property `acceptLanguage` in `CloudFormationProductProps` has been renamed `messageLanguage`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* Co-authored-by: Dillon Ponzo --- .../@aws-cdk/aws-servicecatalog/README.md | 50 ++++- .../@aws-cdk/aws-servicecatalog/lib/common.ts | 6 +- .../aws-servicecatalog/lib/constraints.ts | 32 +++ .../@aws-cdk/aws-servicecatalog/lib/index.ts | 1 + .../aws-servicecatalog/lib/portfolio.ts | 48 ++++- .../lib/private/association-manager.ts | 54 +++++ .../aws-servicecatalog/lib/product.ts | 10 +- .../rosetta/portfolio-product.ts-fixture | 28 +++ .../test/integ.portfolio.expected.json | 191 +++++++++++------- .../test/integ.portfolio.ts | 17 ++ .../aws-servicecatalog/test/portfolio.test.ts | 84 +++++++- 11 files changed, 425 insertions(+), 96 deletions(-) create mode 100644 packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts create mode 100644 packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts create mode 100644 packages/@aws-cdk/aws-servicecatalog/rosetta/portfolio-product.ts-fixture diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index 74073c55b37ce..63b461e1460f3 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -30,6 +30,9 @@ enables organizations to create and manage catalogs of products for their end us - [Granting access to a portfolio](#granting-access-to-a-portfolio) - [Sharing a portfolio with another AWS account](#sharing-a-portfolio-with-another-aws-account) - [Product](#product) + - [Adding a product to a portfolio](#adding-a-product-to-a-portfolio) +- [Constraints](#constraints) + - [Tag update constraint](#tag-update-constraint) The `@aws-cdk/aws-servicecatalog` package contains resources that enable users to automate governance and management of their AWS resources at scale. @@ -57,7 +60,7 @@ new servicecatalog.Portfolio(this, 'MyFirstPortfolio', { displayName: 'MyFirstPortfolio', providerName: 'MyTeam', description: 'Portfolio for a project', - acceptLanguage: servicecatalog.AcceptLanguage.EN, + messageLanguage: servicecatalog.MessageLanguage.EN, }); ``` @@ -125,6 +128,8 @@ Assets are files that are uploaded to an S3 Bucket before deployment. `CloudFormationTemplate.fromAsset` can be utilized to create a Product by passing the path to a local template file on your disk: ```ts +import * as path from 'path'; + const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', { productName: "My Product", owner: "Product Owner", @@ -141,3 +146,46 @@ const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', ] }); ``` + +### Adding a product to a portfolio + +You add products to a portfolio to manage your resources at scale. After adding a product to a portfolio, +it creates a portfolio-product association, and will become visible from the portfolio side in both the console and service catalog CLI. +A product can be added to multiple portfolios depending on your resource and organizational needs. + +```ts fixture=portfolio-product +portfolio.addProduct(product); +``` + +## Constraints + +Constraints define governance mechanisms that allow you to manage permissions, notifications, and options related to actions end users can perform on products, +Constraints are applied on a portfolio-product association. +Using the CDK, if you do not explicitly associate a product to a portfolio and add a constraint, it will automatically add an association for you. + +There are rules around plurariliites of constraints for a portfolio and product. +For example, you can only have a single "tag update" constraint applied to a portfolio-product association. +If a misconfigured constraint is added, `synth` will fail with an error message. + +Read more at [Service Catalog Constraints](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/constraints.html). + +### Tag update constraint + +Tag update constraints allow or disallow end users to update tags on resources associated with an AWS Service Catalog product upon provisioning. +By default, tag updating is not permitted. +If tag updating is allowed, then new tags associated with the product or portfolio will be applied to provisioned resources during a provisioned product update. + +```ts fixture=portfolio-product +portfolio.addProduct(product); + +portfolio.constrainTagUpdates(product); +``` + +If you want to disable this feature later on, you can update it by setting the "allow" parameter to `false`: + +```ts fixture=portfolio-product +// to disable tag updates: +portfolio.constrainTagUpdates(product, { + allow: false, +}); +``` diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/common.ts b/packages/@aws-cdk/aws-servicecatalog/lib/common.ts index f1382342626af..4f207be273867 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/common.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/common.ts @@ -1,7 +1,9 @@ /** * The language code. + * Used for error and logging messages for end users. + * The default behavior if not specified is English. */ -export enum AcceptLanguage { +export enum MessageLanguage { /** * English */ @@ -16,4 +18,4 @@ export enum AcceptLanguage { * Chinese */ ZH = 'zh' -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts b/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts new file mode 100644 index 0000000000000..54a6e40973c4f --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts @@ -0,0 +1,32 @@ +import { MessageLanguage } from './common'; + +/** + * Properties for governance mechanisms and constraints. + */ +export interface CommonConstraintOptions { + /** + * The language code. + * Configures the language for error messages from service catalog. + * + * @default - English + */ + readonly messageLanguage?: MessageLanguage; + + /** + * The description of the constraint. + * + * @default - No description provided + */ + readonly description?: string; +} + +/** + * Properties for ResourceUpdateConstraint. + */ +export interface TagUpdateConstraintOptions extends CommonConstraintOptions { + /** + * Toggle for if users should be allowed to change/update tags on provisioned products. + * @default true + */ + readonly allow?: boolean; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts index 330513de79bf6..7c621b4438ea4 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts @@ -1,4 +1,5 @@ export * from './common'; +export * from './constraints'; export * from './cloudformation-template'; export * from './portfolio'; export * from './product'; diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts index c11c980bb4520..ce1ac8edee493 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts @@ -1,8 +1,11 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { AcceptLanguage } from './common'; +import { MessageLanguage } from './common'; +import { TagUpdateConstraintOptions } from './constraints'; +import { AssociationManager } from './private/association-manager'; import { hashValues } from './private/util'; import { InputValidator } from './private/validation'; +import { IProduct } from './product'; import { CfnPortfolio, CfnPortfolioPrincipalAssociation, CfnPortfolioShare } from './servicecatalog.generated'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -15,15 +18,18 @@ import { Construct } from 'constructs'; export interface PortfolioShareOptions { /** * Whether to share tagOptions as a part of the portfolio share + * * @default - share not specified */ readonly shareTagOptions?: boolean; /** - * The accept language of the share - * @default - accept language not specified + * The message language of the share. + * Controls status and error message language for share. + * + * @default - English */ - readonly acceptLanguage?: AcceptLanguage; + readonly messageLanguage?: MessageLanguage; } /** @@ -66,6 +72,17 @@ export interface IPortfolio extends cdk.IResource { * @param options Options for the initiate share */ shareWithAccount(accountId: string, options?: PortfolioShareOptions): void; + + /** + * Associate portfolio with the given product. + * @param product A service catalog produt. + */ + addProduct(product: IProduct): void; + + /** + * Add a Resource Update Constraint. + */ + constrainTagUpdates(product: IProduct, options?: TagUpdateConstraintOptions): void; } abstract class PortfolioBase extends cdk.Resource implements IPortfolio { @@ -85,16 +102,24 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio { this.associatePrincipal(group.groupArn, group.node.addr); } + public addProduct(product: IProduct): void { + AssociationManager.associateProductWithPortfolio(this, product); + } + public shareWithAccount(accountId: string, options: PortfolioShareOptions = {}): void { const hashId = this.generateUniqueHash(accountId); new CfnPortfolioShare(this, `PortfolioShare${hashId}`, { portfolioId: this.portfolioId, accountId: accountId, shareTagOptions: options.shareTagOptions, - acceptLanguage: options.acceptLanguage, + acceptLanguage: options.messageLanguage, }); } + public constrainTagUpdates(product: IProduct, options: TagUpdateConstraintOptions = {}): void { + AssociationManager.constrainTagUpdates(this, product, options); + } + /** * Associate a principal with the portfolio. * If the principal is already associated, it will skip. @@ -132,13 +157,16 @@ export interface PortfolioProps { readonly providerName: string; /** - * The accept language. - * @default - No accept language provided + * The message language. Controls language for + * status logging and errors. + * + * @default - English */ - readonly acceptLanguage?: AcceptLanguage; + readonly messageLanguage?: MessageLanguage; /** * Description for portfolio. + * * @default - No description provided */ readonly description?: string; @@ -156,7 +184,7 @@ export class Portfolio extends PortfolioBase { * @param portfolioArn the Amazon Resource Name of the existing portfolio. */ public static fromPortfolioArn(scope: Construct, id: string, portfolioArn: string): IPortfolio { - const arn = cdk.Stack.of(scope).parseArn(portfolioArn); + const arn = cdk.Stack.of(scope).splitArn(portfolioArn, cdk.ArnFormat.SLASH_RESOURCE_NAME); const portfolioId = arn.resourceName; if (!portfolioId) { @@ -190,7 +218,7 @@ export class Portfolio extends PortfolioBase { displayName: props.displayName, providerName: props.providerName, description: props.description, - acceptLanguage: props.acceptLanguage, + acceptLanguage: props.messageLanguage, }); this.portfolioId = this.portfolio.ref; this.portfolioArn = cdk.Stack.of(this).formatArn({ diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts new file mode 100644 index 0000000000000..5a163073b29a1 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts @@ -0,0 +1,54 @@ +import * as cdk from '@aws-cdk/core'; +import { TagUpdateConstraintOptions } from '../constraints'; +import { IPortfolio } from '../portfolio'; +import { IProduct } from '../product'; +import { CfnPortfolioProductAssociation, CfnResourceUpdateConstraint } from '../servicecatalog.generated'; +import { hashValues } from './util'; +import { InputValidator } from './validation'; + +export class AssociationManager { + public static associateProductWithPortfolio( + portfolio: IPortfolio, product: IProduct, + ): { associationKey: string, cfnPortfolioProductAssociation: CfnPortfolioProductAssociation } { + const associationKey = hashValues(portfolio.node.addr, product.node.addr, product.stack.node.addr); + const constructId = `PortfolioProductAssociation${associationKey}`; + const existingAssociation = portfolio.node.tryFindChild(constructId); + const cfnAssociation = existingAssociation + ? existingAssociation as CfnPortfolioProductAssociation + : new CfnPortfolioProductAssociation(portfolio as unknown as cdk.Resource, constructId, { + portfolioId: portfolio.portfolioId, + productId: product.productId, + }); + + return { + associationKey: associationKey, + cfnPortfolioProductAssociation: cfnAssociation, + }; + } + + + public static constrainTagUpdates(portfolio: IPortfolio, product: IProduct, options: TagUpdateConstraintOptions): void { + InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options.description); + const association = this.associateProductWithPortfolio(portfolio, product); + const constructId = `ResourceUpdateConstraint${association.associationKey}`; + + if (!portfolio.node.tryFindChild(constructId)) { + const constraint = new CfnResourceUpdateConstraint(portfolio as unknown as cdk.Resource, constructId, { + acceptLanguage: options.messageLanguage, + description: options.description, + portfolioId: portfolio.portfolioId, + productId: product.productId, + tagUpdateOnProvisionedProduct: options.allow === false ? 'NOT_ALLOWED' : 'ALLOWED', + }); + + // Add dependsOn to force proper order in deployment. + constraint.addDependsOn(association.cfnPortfolioProductAssociation); + } else { + throw new Error(`Cannot have multiple tag update constraints for association ${this.prettyPrintAssociation(portfolio, product)}`); + } + } + + private static prettyPrintAssociation(portfolio: IPortfolio, product: IProduct): string { + return `- Portfolio: ${portfolio.node.path} | Product: ${product.node.path}`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts index 3de0ad8387460..466e1fa726e55 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts @@ -1,7 +1,7 @@ import { ArnFormat, IResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CloudFormationTemplate } from './cloudformation-template'; -import { AcceptLanguage } from './common'; +import { MessageLanguage } from './common'; import { InputValidator } from './private/validation'; import { CfnCloudFormationProduct } from './servicecatalog.generated'; @@ -77,9 +77,11 @@ export interface CloudFormationProductProps { /** * The language code. - * @default - No accept language provided + * Controls language for logging and errors. + * + * @default - English */ - readonly acceptLanguage?: AcceptLanguage; + readonly messageLanguage?: MessageLanguage; /** * The description of the product. @@ -156,7 +158,7 @@ export class CloudFormationProduct extends Product { this.validateProductProps(props); const product = new CfnCloudFormationProduct(this, 'Resource', { - acceptLanguage: props.acceptLanguage, + acceptLanguage: props.messageLanguage, description: props.description, distributor: props.distributor, name: props.productName, diff --git a/packages/@aws-cdk/aws-servicecatalog/rosetta/portfolio-product.ts-fixture b/packages/@aws-cdk/aws-servicecatalog/rosetta/portfolio-product.ts-fixture new file mode 100644 index 0000000000000..20a1db30bf3ee --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/rosetta/portfolio-product.ts-fixture @@ -0,0 +1,28 @@ +// Fixture with packages imported, but nothing else +import { Construct, Stack } from '@aws-cdk/core'; +import * as servicecatalog from '@aws-cdk/aws-servicecatalog'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const portfolio = new servicecatalog.Portfolio(this, "MyFirstPortfolio", { + displayName: "MyFirstPortfolio", + providerName: "MyTeam", + }); + + const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', { + productName: "My Product", + owner: "Product Owner", + productVersions: [ + { + productVersionName: "v1", + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl( + 'https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ServiceCatalog/Product.yaml'), + }, + ] + }); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json index 0409382efba03..1fc70614de939 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json @@ -1,87 +1,128 @@ { - "Resources": { - "TestRole6C9272DF": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::", - { - "Ref": "AWS::AccountId" - }, - ":root" - ] + "Resources": { + "TestRole6C9272DF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" ] - } + ] } } - ], - "Version": "2012-10-17" - } - } - }, - "TestGroupAF88660E": { - "Type": "AWS::IAM::Group" - }, - "TestPortfolio4AC794EB": { - "Type": "AWS::ServiceCatalog::Portfolio", - "Properties": { - "DisplayName": "TestPortfolio", - "ProviderName": "TestProvider", - "Description": "This is our Service Catalog Portfolio" + } + ], + "Version": "2012-10-17" } - }, - "TestPortfolioPortolioPrincipalAssociation20e1afa20ac27E1A060D": { - "Type": "AWS::ServiceCatalog::PortfolioPrincipalAssociation", - "Properties": { - "PortfolioId": { - "Ref": "TestPortfolio4AC794EB" - }, - "PrincipalARN": { - "Fn::GetAtt": [ - "TestRole6C9272DF", - "Arn" - ] - }, - "PrincipalType": "IAM" + } + }, + "TestGroupAF88660E": { + "Type": "AWS::IAM::Group" + }, + "TestPortfolio4AC794EB": { + "Type": "AWS::ServiceCatalog::Portfolio", + "Properties": { + "DisplayName": "TestPortfolio", + "ProviderName": "TestProvider", + "AcceptLanguage": "en", + "Description": "This is our Service Catalog Portfolio" + } + }, + "TestPortfolioPortolioPrincipalAssociation20e1afa20ac27E1A060D": { + "Type": "AWS::ServiceCatalog::PortfolioPrincipalAssociation", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "PrincipalARN": { + "Fn::GetAtt": [ + "TestRole6C9272DF", + "Arn" + ] + }, + "PrincipalType": "IAM" + } + }, + "TestPortfolioPortolioPrincipalAssociation44a1ca1c23384D6E460B": { + "Type": "AWS::ServiceCatalog::PortfolioPrincipalAssociation", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "PrincipalARN": { + "Fn::GetAtt": [ + "TestGroupAF88660E", + "Arn" + ] + }, + "PrincipalType": "IAM" + } + }, + "TestPortfolioPortfolioSharebf5b82f042508F035880": { + "Type": "AWS::ServiceCatalog::PortfolioShare", + "Properties": { + "AccountId": "123456789012", + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" } - }, - "TestPortfolioPortolioPrincipalAssociation44a1ca1c23384D6E460B": { - "Type": "AWS::ServiceCatalog::PortfolioPrincipalAssociation", - "Properties": { - "PortfolioId": { - "Ref": "TestPortfolio4AC794EB" - }, - "PrincipalARN": { - "Fn::GetAtt": [ - "TestGroupAF88660E", - "Arn" - ] - }, - "PrincipalType": "IAM" + } + }, + "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7": { + "Type": "AWS::ServiceCatalog::PortfolioProductAssociation", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "ProductId": { + "Ref": "TestProduct7606930B" } + } + }, + "TestPortfolioResourceUpdateConstrainta0185761d231AB0EAAE0": { + "Type": "AWS::ServiceCatalog::ResourceUpdateConstraint", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "ProductId": { + "Ref": "TestProduct7606930B" + }, + "TagUpdateOnProvisionedProduct": "ALLOWED" }, - "TestPortfolioPortfolioSharebf5b82f042508F035880": { - "Type": "AWS::ServiceCatalog::PortfolioShare", - "Properties": { - "AccountId": "123456789012", - "PortfolioId": { - "Ref": "TestPortfolio4AC794EB" + "DependsOn": [ + "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" + ] + }, + "TestProduct7606930B": { + "Type": "AWS::ServiceCatalog::CloudFormationProduct", + "Properties": { + "Name": "testProduct", + "Owner": "testOwner", + "ProvisioningArtifactParameters": [ + { + "DisableTemplateValidation": true, + "Info": { + "LoadTemplateFromURL": "https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template" + } } - } + ] } } } - \ No newline at end of file +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts index b62523c59083f..d48bd9796286e 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts @@ -15,6 +15,7 @@ const portfolio = new servicecatalog.Portfolio(stack, 'TestPortfolio', { displayName: 'TestPortfolio', providerName: 'TestProvider', description: 'This is our Service Catalog Portfolio', + messageLanguage: servicecatalog.MessageLanguage.EN, }); portfolio.giveAccessToRole(role); @@ -22,4 +23,20 @@ portfolio.giveAccessToGroup(group); portfolio.shareWithAccount('123456789012'); +const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + validateTemplate: false, + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl( + 'https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template'), + }, + ], +}); + +portfolio.addProduct(product); + +portfolio.constrainTagUpdates(product); + app.synth(); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts index c85bf4b05955c..1df07c37ae356 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts @@ -30,17 +30,17 @@ describe('Portfolio', () => { }); }), - test('portfolio with explicit acceptLanguage and description', () => { + test('portfolio with explicit message language and description', () => { new servicecatalog.Portfolio(stack, 'MyPortfolio', { displayName: 'testPortfolio', providerName: 'testProvider', description: 'test portfolio description', - acceptLanguage: servicecatalog.AcceptLanguage.ZH, + messageLanguage: servicecatalog.MessageLanguage.ZH, }); expect(stack).toHaveResourceLike('AWS::ServiceCatalog::Portfolio', { Description: 'test portfolio description', - AcceptLanguage: servicecatalog.AcceptLanguage.ZH, + AcceptLanguage: servicecatalog.MessageLanguage.ZH, }); }), @@ -185,7 +185,7 @@ describe('Portfolio', () => { portfolio.shareWithAccount(shareAccountId, { shareTagOptions: true, - acceptLanguage: servicecatalog.AcceptLanguage.EN, + messageLanguage: servicecatalog.MessageLanguage.EN, }); expect(stack).toHaveResourceLike('AWS::ServiceCatalog::PortfolioShare', { @@ -259,3 +259,79 @@ describe('Portfolio', () => { }); }); }); + +describe('portfolio associations and product constraints', () => { + let stack: cdk.Stack; + let portfolio: servicecatalog.Portfolio; + let product: servicecatalog.CloudFormationProduct; + + beforeEach(() => { + stack = new cdk.Stack(); + + portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolio', { + displayName: 'testPortfolio', + providerName: 'testProvider', + }); + + product = new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl('https://awsdocs.s3.amazonaws.com/servicecatalog/development-environment.template'), + }, + ], + }); + }), + + test('basic portfolio product association', () => { + portfolio.addProduct(product); + + expect(stack).toHaveResource('AWS::ServiceCatalog::PortfolioProductAssociation'); + }); + + test('portfolio product associations are idempotent', () => { + portfolio.addProduct(product); + portfolio.addProduct(product); // If not idempotent these calls should fail + + expect(stack).toCountResources('AWS::ServiceCatalog::PortfolioProductAssociation', 1); //check anyway + }), + + test('add tag update constraint', () => { + portfolio.addProduct(product); + portfolio.constrainTagUpdates(product, { + allow: true, + }); + + expect(stack).toHaveResourceLike('AWS::ServiceCatalog::ResourceUpdateConstraint', { + TagUpdateOnProvisionedProduct: 'ALLOWED', + }); + }); + + test('tag update constraint still adds without explicit association', () => { + portfolio.constrainTagUpdates(product, { + messageLanguage: servicecatalog.MessageLanguage.EN, + description: 'test constraint description', + allow: false, + }); + + expect(stack).toHaveResourceLike('AWS::ServiceCatalog::ResourceUpdateConstraint', { + AcceptLanguage: servicecatalog.MessageLanguage.EN, + Description: 'test constraint description', + TagUpdateOnProvisionedProduct: 'NOT_ALLOWED', + }); + }), + + test('fails to add multiple tag update constraints', () => { + portfolio.constrainTagUpdates(product, { + description: 'test constraint description', + }); + + expect(() => { + portfolio.constrainTagUpdates(product, { + allow: false, + description: 'another test constraint description', + }); + }).toThrowError(/Cannot have multiple tag update constraints for association/); + }); +}); \ No newline at end of file From 1589ff859e3816e1326b25e4fc855be86f76ffc8 Mon Sep 17 00:00:00 2001 From: Seiya6329 Date: Wed, 14 Jul 2021 18:50:50 -0700 Subject: [PATCH 052/105] feat(appmesh): add support for Gateway Route request matching and path rewriting (#15527) closes #15305 - Adding new match properties for `GatewayRoute`. - For HTTP match, adding `path` and `headers`, `hostname`, `method`, and `queryParameters`. Remove `prefixPath`. - For gRPC match, adding `metadata` and `hostname` - Adding a feature to configure `rewrite` option on the request that is coming into `GatewayRoute`. BREAKING CHANGE: `prefixPath` property in `HttpGatewayRouteMatch` has been renamed to `path`, and its type changed from `string` to `HttpGatewayRoutePathMatch` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 70 +- .../aws-appmesh/lib/gateway-route-spec.ts | 206 +++- .../aws-appmesh/lib/http-route-path-match.ts | 156 ++- .../@aws-cdk/aws-appmesh/lib/private/utils.ts | 16 +- .../@aws-cdk/aws-appmesh/lib/route-spec.ts | 4 +- .../aws-appmesh/test/integ.mesh.expected.json | 340 +++++++ .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 67 ++ .../aws-appmesh/test/test.gateway-route.ts | 960 +++++++++++++++++- .../@aws-cdk/aws-appmesh/test/test.route.ts | 2 +- 9 files changed, 1769 insertions(+), 52 deletions(-) diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index af8d876f1fea3..02a63b5c0b844 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -631,29 +631,87 @@ The `backendDefaults` property is added to the node while creating the virtual g A _gateway route_ is attached to a virtual gateway and routes traffic to an existing virtual service. If a route matches a request, it can distribute traffic to a target virtual service. -For HTTP based routes, the match field can be used to match on a route prefix. -By default, an HTTP based route will match on `/`. All matches must start with a leading `/`. +For HTTP based gateway routes, the match field can be used to match on +path (prefix, exact, or regex), HTTP method, host name, HTTP headers, and query parameters. +By default, an HTTP based route will match all requests. ```ts gateway.addGatewayRoute('gateway-route-http', { routeSpec: appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { - prefixMatch: '/', + path: appmesh.HttpGatewayRoutePathMatch.regex('regex'), }, }), }); ``` -For GRPC based routes, the match field can be used to match on service names. -You cannot omit the field, and must specify a match for these routes. +For gRPC based gateway routes, the match field can be used to match on service name, host name, and metadata. ```ts gateway.addGatewayRoute('gateway-route-grpc', { routeSpec: appmesh.GatewayRouteSpec.grpc({ routeTarget: virtualService, match: { - serviceName: 'my-service.default.svc.cluster.local', + hostname: appmesh.GatewayRouteHostnameMatch.endsWith('.example.com'), + }, + }), +}); +``` + +For HTTP based gateway routes, App Mesh automatically rewrites the matched prefix path in Gateway Route to “/”. +This automatic rewrite configuration can be overwritten in following ways: + +```ts +gateway.addGatewayRoute('gateway-route-http', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + // This disables the default rewrite to '/', and retains original path. + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/path-to-app/', ''), + }, + }), +}); + +gateway.addGatewayRoute('gateway-route-http-1', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + // If the request full path is '/path-to-app/xxxxx', this rewrites the path to '/rewrittenUri/xxxxx'. + // Please note both `prefixPathMatch` and `rewriteTo` must start and end with the `/` character. + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/path-to-app/', '/rewrittenUri/'), + }, + }), +}); +``` + +If matching other path (exact or regex), only specific rewrite path can be specified. +Unlike `startsWith()` method above, no default rewrite is performed. + +```ts +gateway.addGatewayRoute('gateway-route-http-2', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + // This rewrites the path from '/test' to '/rewrittenPath'. + path: appmesh.HttpGatewayRoutePathMatch.exactly('/test', '/rewrittenPath'), + }, + }), +}); +``` + +For HTTP/gRPC based routes, App Mesh automatically rewrites +the original request received at the Virtual Gateway to the destination Virtual Service name. +This default host name rewrite can be configured by specifying the rewrite rule as one of the `match` property: + +```ts +gateway.addGatewayRoute('gateway-route-grpc', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), + // This disables the default rewrite to virtual service name and retain original request. + rewriteRequestHostname: false, }, }), }); diff --git a/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts index 0a90fb1632ff9..9e8f6315a2356 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts @@ -1,4 +1,9 @@ import { CfnGatewayRoute } from './appmesh.generated'; +import { HeaderMatch } from './header-match'; +import { HttpRouteMethod } from './http-route-method'; +import { HttpGatewayRoutePathMatch } from './http-route-path-match'; +import { validateGrpcMatchArrayLength, validateGrpcGatewayRouteMatch } from './private/utils'; +import { QueryParameterMatch } from './query-parameter-match'; import { Protocol } from './shared-interfaces'; import { IVirtualService } from './virtual-service'; @@ -7,16 +12,105 @@ import { IVirtualService } from './virtual-service'; import { Construct } from '@aws-cdk/core'; /** - * The criterion for determining a request match for this GatewayRoute + * Configuration for gateway route host name match. + */ +export interface GatewayRouteHostnameMatchConfig { + /** + * GatewayRoute CFN configuration for host name match. + */ + readonly hostnameMatch: CfnGatewayRoute.GatewayRouteHostnameMatchProperty; +} + +/** + * Used to generate host name matching methods. + */ +export abstract class GatewayRouteHostnameMatch { + /** + * The value of the host name must match the specified value exactly. + * + * @param name The exact host name to match on + */ + public static exactly(name: string): GatewayRouteHostnameMatch { + return new GatewayRouteHostnameMatchImpl({ exact: name }); + } + + /** + * The value of the host name with the given name must end with the specified characters. + * + * @param suffix The specified ending characters of the host name to match on + */ + public static endsWith(suffix: string): GatewayRouteHostnameMatch { + return new GatewayRouteHostnameMatchImpl({ suffix }); + } + + /** + * Returns the gateway route host name match configuration. + */ + public abstract bind(scope: Construct): GatewayRouteHostnameMatchConfig; +} + +class GatewayRouteHostnameMatchImpl extends GatewayRouteHostnameMatch { + constructor( + private readonly matchProperty: CfnGatewayRoute.GatewayRouteHostnameMatchProperty, + ) { + super(); + } + + bind(_scope: Construct): GatewayRouteHostnameMatchConfig { + return { + hostnameMatch: this.matchProperty, + }; + } +} + +/** + * The criterion for determining a request match for this GatewayRoute. */ export interface HttpGatewayRouteMatch { /** - * Specifies the path to match requests with. - * This parameter must always start with /, which by itself matches all requests to the virtual service name. - * You can also match for path-based routing of requests. For example, if your virtual service name is my-service.local - * and you want the route to match requests to my-service.local/metrics, your prefix should be /metrics. + * Specify how to match requests based on the 'path' part of their URL. + * + * @default - matches requests with any path + */ + readonly path?: HttpGatewayRoutePathMatch; + + /** + * Specifies the client request headers to match on. All specified headers + * must match for the gateway route to match. + * + * @default - do not match on headers + */ + readonly headers?: HeaderMatch[]; + + /** + * The gateway route host name to be matched on. + * + * @default - do not match on host name + */ + readonly hostname?: GatewayRouteHostnameMatch; + + /** + * The method to match on. + * + * @default - do not match on method + */ + readonly method?: HttpRouteMethod; + + /** + * The query parameters to match on. + * All specified query parameters must match for the route to match. + * + * @default - do not match on query parameters */ - readonly prefixPath: string; + readonly queryParameters?: QueryParameterMatch[]; + + /** + * When `true`, rewrites the original request received at the Virtual Gateway to the destination Virtual Service name. + * When `false`, retains the original hostname from the request. + * + * @default true + */ + readonly rewriteRequestHostname?: boolean; } /** @@ -24,9 +118,34 @@ export interface HttpGatewayRouteMatch { */ export interface GrpcGatewayRouteMatch { /** - * The fully qualified domain name for the service to match from the request + * Create service name based gRPC gateway route match. + * + * @default - no matching on service name + */ + readonly serviceName?: string; + + /** + * Create host name based gRPC gateway route match. + * + * @default - no matching on host name + */ + readonly hostname?: GatewayRouteHostnameMatch; + + /** + * Create metadata based gRPC gateway route match. + * All specified metadata must match for the route to match. + * + * @default - no matching on metadata + */ + readonly metadata?: HeaderMatch[]; + + /** + * When `true`, rewrites the original request received at the Virtual Gateway to the destination Virtual Service name. + * When `false`, retains the original hostname from the request. + * + * @default true */ - readonly serviceName: string; + readonly rewriteRequestHostname?: boolean; } /** @@ -34,9 +153,10 @@ export interface GrpcGatewayRouteMatch { */ export interface HttpGatewayRouteSpecOptions { /** - * The criterion for determining a request match for this GatewayRoute + * The criterion for determining a request match for this GatewayRoute. + * When path match is defined, this may optionally determine the path rewrite configuration. * - * @default - matches on '/' + * @default - matches any path and automatically rewrites the path to '/' */ readonly match?: HttpGatewayRouteMatch; @@ -47,7 +167,7 @@ export interface HttpGatewayRouteSpecOptions { } /** - * Properties specific for a GRPC GatewayRoute + * Properties specific for a gRPC GatewayRoute */ export interface GrpcGatewayRouteSpecOptions { /** @@ -110,7 +230,7 @@ export abstract class GatewayRouteSpec { } /** - * Creates an GRPC Based GatewayRoute + * Creates an gRPC Based GatewayRoute * * @param options - no grpc gateway route */ @@ -126,11 +246,6 @@ export abstract class GatewayRouteSpec { } class HttpGatewayRouteSpec extends GatewayRouteSpec { - /** - * The criterion for determining a request match for this GatewayRoute. - * - * @default - matches on '/' - */ readonly match?: HttpGatewayRouteMatch; /** @@ -150,14 +265,21 @@ class HttpGatewayRouteSpec extends GatewayRouteSpec { this.match = options.match; } - public bind(_scope: Construct): GatewayRouteSpecConfig { - const prefixPath = this.match ? this.match.prefixPath : '/'; - if (prefixPath[0] != '/') { - throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); - } + public bind(scope: Construct): GatewayRouteSpecConfig { + const pathMatchConfig = (this.match?.path ?? HttpGatewayRoutePathMatch.startsWith('/')).bind(scope); + const rewriteRequestHostname = this.match?.rewriteRequestHostname; + + const prefixPathRewrite = pathMatchConfig.prefixPathRewrite; + const wholePathRewrite = pathMatchConfig.wholePathRewrite; + const httpConfig: CfnGatewayRoute.HttpGatewayRouteProperty = { match: { - prefix: prefixPath, + prefix: pathMatchConfig.prefixPathMatch, + path: pathMatchConfig.wholePathMatch, + hostname: this.match?.hostname?.bind(scope).hostnameMatch, + method: this.match?.method, + headers: this.match?.headers?.map(header => header.bind(scope).headerMatch), + queryParameters: this.match?.queryParameters?.map(queryParameter => queryParameter.bind(scope).queryParameterMatch), }, action: { target: { @@ -165,6 +287,17 @@ class HttpGatewayRouteSpec extends GatewayRouteSpec { virtualServiceName: this.routeTarget.virtualServiceName, }, }, + rewrite: rewriteRequestHostname !== undefined || prefixPathRewrite || wholePathRewrite + ? { + hostname: rewriteRequestHostname === undefined + ? undefined + : { + defaultTargetHostname: rewriteRequestHostname? 'ENABLED' : 'DISABLED', + }, + prefix: prefixPathRewrite, + path: wholePathRewrite, + } + : undefined, }, }; return { @@ -175,11 +308,6 @@ class HttpGatewayRouteSpec extends GatewayRouteSpec { } class GrpcGatewayRouteSpec extends GatewayRouteSpec { - /** - * The criterion for determining a request match for this GatewayRoute. - * - * @default - no default - */ readonly match: GrpcGatewayRouteMatch; /** @@ -193,18 +321,32 @@ class GrpcGatewayRouteSpec extends GatewayRouteSpec { this.routeTarget = options.routeTarget; } - public bind(_scope: Construct): GatewayRouteSpecConfig { + public bind(scope: Construct): GatewayRouteSpecConfig { + const metadataMatch = this.match.metadata; + + validateGrpcGatewayRouteMatch(this.match); + validateGrpcMatchArrayLength(metadataMatch); + return { grpcSpecConfig: { + match: { + serviceName: this.match.serviceName, + hostname: this.match.hostname?.bind(scope).hostnameMatch, + metadata: metadataMatch?.map(metadata => metadata.bind(scope).headerMatch), + }, action: { target: { virtualService: { virtualServiceName: this.routeTarget.virtualServiceName, }, }, - }, - match: { - serviceName: this.match.serviceName, + rewrite: this.match.rewriteRequestHostname === undefined + ? undefined + : { + hostname: { + defaultTargetHostname: this.match.rewriteRequestHostname ? 'ENABLED' : 'DISABLED', + }, + }, }, }, }; diff --git a/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts b/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts index 819cc3821b429..7131063ac9615 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts @@ -1,4 +1,4 @@ -import { CfnRoute } from './appmesh.generated'; +import { CfnGatewayRoute, CfnRoute } from './appmesh.generated'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -68,8 +68,8 @@ class HttpRoutePrefixPathMatch extends HttpRoutePathMatch { constructor(private readonly prefix: string) { super(); - if (this.prefix && this.prefix[0] !== '/') { - throw new Error(`Prefix Path for the match must start with \'/\', got: ${this.prefix}`); + if (prefix && prefix[0] !== '/') { + throw new Error(`Prefix Path for the match must start with \'/\', got: ${prefix}`); } } @@ -84,8 +84,8 @@ class HttpRouteWholePathMatch extends HttpRoutePathMatch { constructor(private readonly match: CfnRoute.HttpPathMatchProperty) { super(); - if (this.match.exact && this.match.exact[0] !== '/') { - throw new Error(`Exact Path for the match must start with \'/\', got: ${this.match.exact}`); + if (match.exact && match.exact[0] !== '/') { + throw new Error(`Exact Path for the match must start with \'/\', got: ${match.exact}`); } } @@ -95,3 +95,149 @@ class HttpRouteWholePathMatch extends HttpRoutePathMatch { }; } } + +/** + * The type returned from the `bind()` method in {@link HttpGatewayRoutePathMatch}. + */ +export interface HttpGatewayRoutePathMatchConfig { + /** + * Gateway route configuration for matching on the complete URL path of the request. + * + * @default - no matching will be performed on the complete URL path + */ + readonly wholePathMatch?: CfnGatewayRoute.HttpPathMatchProperty; + + /** + * Gateway route configuration for matching on the prefix of the URL path of the request. + * + * @default - no matching will be performed on the prefix of the URL path + */ + readonly prefixPathMatch?: string; + + /** + * Gateway route configuration for rewriting the complete URL path of the request.. + * + * @default - no rewrite will be performed on the request's complete URL path + */ + readonly wholePathRewrite?: CfnGatewayRoute.HttpGatewayRoutePathRewriteProperty; + + /** + * Gateway route configuration for rewriting the prefix of the URL path of the request. + * + * @default - rewrites the request's URL path to '/' + */ + readonly prefixPathRewrite?: CfnGatewayRoute.HttpGatewayRoutePrefixRewriteProperty; +} + +/** + * Defines HTTP gateway route matching based on the URL path of the request. + */ +export abstract class HttpGatewayRoutePathMatch { + /** + * The value of the path must match the specified prefix. + * + * @param prefix the value to use to match the beginning of the path part of the URL of the request. + * It must start with the '/' character. + * When `rewriteTo` is provided, it must also end with the '/' character. + * If provided as "/", matches all requests. + * For example, if your virtual service name is "my-service.local" + * and you want the route to match requests to "my-service.local/metrics", your prefix should be "/metrics". + * @param rewriteTo Specify either disabling automatic rewrite or rewriting to specified prefix path. + * To disable automatic rewrite, provide `''`. + * As a default, request's URL path is automatically rewritten to '/'. + */ + public static startsWith(prefix: string, rewriteTo?: string): HttpGatewayRoutePathMatch { + return new HttpGatewayRoutePrefixPathMatch(prefix, rewriteTo); + } + + /** + * The value of the path must match the specified value exactly. + * The provided `path` must start with the '/' character. + * + * @param path the exact path to match on + * @param rewriteTo the value to substitute for the matched part of the path of the gateway request URL + * As a default, retains original request's URL path. + */ + public static exactly(path: string, rewriteTo?: string): HttpGatewayRoutePathMatch { + return new HttpGatewayRouteWholePathMatch({ exact: path }, rewriteTo); + } + + /** + * The value of the path must match the specified regex. + * + * @param regex the regex used to match the path + * @param rewriteTo the value to substitute for the matched part of the path of the gateway request URL + * As a default, retains original request's URL path. + */ + public static regex(regex: string, rewriteTo?: string): HttpGatewayRoutePathMatch { + return new HttpGatewayRouteWholePathMatch({ regex }, rewriteTo); + } + + /** + * Returns the gateway route path match configuration. + */ + public abstract bind(scope: Construct): HttpGatewayRoutePathMatchConfig; +} + +class HttpGatewayRoutePrefixPathMatch extends HttpGatewayRoutePathMatch { + constructor( + private readonly prefixPathMatch: string, + private readonly rewriteTo?: string, + ) { + super(); + + if (prefixPathMatch[0] !== '/') { + throw new Error('Prefix path for the match must start with \'/\', ' + + `got: ${prefixPathMatch}`); + } + + if (rewriteTo) { + if (prefixPathMatch[prefixPathMatch.length - 1] !== '/') { + throw new Error('When prefix path for the rewrite is specified, prefix path for the match must end with \'/\', ' + + `got: ${prefixPathMatch}`); + } + if (rewriteTo[0] !== '/' || rewriteTo[rewriteTo.length - 1] !== '/') { + throw new Error('Prefix path for the rewrite must start and end with \'/\', ' + + `got: ${rewriteTo}`); + } + } + } + + bind(_scope: Construct): HttpGatewayRoutePathMatchConfig { + return { + prefixPathMatch: this.prefixPathMatch, + prefixPathRewrite: this.rewriteTo === undefined + ? undefined + : { + defaultPrefix: this.rewriteTo === '' ? 'DISABLED' : undefined, + value: this.rewriteTo === '' ? undefined : this.rewriteTo, + }, + }; + } +} + +class HttpGatewayRouteWholePathMatch extends HttpGatewayRoutePathMatch { + constructor( + private readonly wholePathMatch: CfnGatewayRoute.HttpPathMatchProperty, + private readonly exactPathRewrite?: string | undefined, + ) { + super(); + + if (wholePathMatch.exact && wholePathMatch.exact[0] !== '/') { + throw new Error(`Exact Path for the match must start with \'/\', got: ${ wholePathMatch.exact }`); + } + if (exactPathRewrite === '') { + throw new Error('Exact Path for the rewrite cannot be empty. Unlike startsWith() method, no automatic rewrite on whole path match'); + } + if (exactPathRewrite && exactPathRewrite[0] !== '/') { + throw new Error(`Exact Path for the rewrite must start with \'/\', got: ${ exactPathRewrite }`); + } + } + + bind(_scope: Construct): HttpGatewayRoutePathMatchConfig { + return { + wholePathMatch: this.wholePathMatch, + wholePathRewrite: this.exactPathRewrite === undefined ? undefined : { exact: this.exactPathRewrite }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts index 6f07827aa78ff..0820ae42c41aa 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -1,5 +1,6 @@ import { Token, TokenComparison } from '@aws-cdk/core'; import { CfnVirtualNode } from '../appmesh.generated'; +import { GrpcGatewayRouteMatch } from '../gateway-route-spec'; import { HeaderMatch } from '../header-match'; import { ListenerTlsOptions } from '../listener-tls-options'; import { QueryParameterMatch } from '../query-parameter-match'; @@ -125,10 +126,19 @@ export function validateGrpcMatchArrayLength(metadata?: HeaderMatch[]): void { } /** - * This is the helper method to validate at least one of gRPC match type is defined. + * This is the helper method to validate at least one of gRPC route match type is defined. */ -export function validateGrpcMatch(match: GrpcRouteMatch): void { +export function validateGrpcRouteMatch(match: GrpcRouteMatch): void { if (match.serviceName === undefined && match.metadata === undefined && match.methodName === undefined) { - throw new Error('At least one gRPC match property must be provided'); + throw new Error('At least one gRPC route match property must be provided'); + } +} + +/** + * This is the helper method to validate at least one of gRPC gateway route match type is defined. + */ +export function validateGrpcGatewayRouteMatch(match: GrpcGatewayRouteMatch): void { + if (match.serviceName === undefined && match.metadata === undefined && match.hostname === undefined) { + throw new Error('At least one gRPC gateway route match property beside rewriteRequestHostname must be provided'); } } diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index 0ff6bb8d88af1..a27d589a61ded 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -3,7 +3,7 @@ import { CfnRoute } from './appmesh.generated'; import { HeaderMatch } from './header-match'; import { HttpRouteMethod } from './http-route-method'; import { HttpRoutePathMatch } from './http-route-path-match'; -import { validateGrpcMatch, validateGrpcMatchArrayLength, validateHttpMatchArrayLength } from './private/utils'; +import { validateGrpcRouteMatch, validateGrpcMatchArrayLength, validateHttpMatchArrayLength } from './private/utils'; import { QueryParameterMatch } from './query-parameter-match'; import { GrpcTimeout, HttpTimeout, Protocol, TcpTimeout } from './shared-interfaces'; import { IVirtualNode } from './virtual-node'; @@ -555,7 +555,7 @@ class GrpcRouteSpec extends RouteSpec { const methodName = this.match.methodName; const metadata = this.match.metadata; - validateGrpcMatch(this.match); + validateGrpcRouteMatch(this.match); validateGrpcMatchArrayLength(metadata); if (methodName && !serviceName) { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index faafaa4f96ea6..4b6c3e54f543e 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -1372,6 +1372,141 @@ "GatewayRouteName": "meshstackmeshgateway1gateway1routehttpBA921D42" } }, + "meshgateway1gateway1routehttp2B672D43F": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "HttpRoute": { + "Action": { + "Rewrite": { + "Hostname": { + "DefaultTargetHostname": "ENABLED" + }, + "Prefix": { + "DefaultPrefix": "DISABLED" + } + }, + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "service6D174F83", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Headers": [ + { + "Invert": false, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Prefix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Suffix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Regex": "application/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Prefix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Suffix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Regex": "application/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Content-Type" + } + ], + "Hostname": { + "Exact": "example.com" + }, + "Method": "POST", + "Prefix": "/", + "QueryParameters": [ + { + "Match": { + "Exact": "value" + }, + "Name": "query-field" + } + ] + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp27F17263B" + } + }, "meshgateway1gateway1routehttp2FD69C306": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { @@ -1409,6 +1544,89 @@ "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp255781963" } }, + "meshgateway1gateway1routehttp2225001508": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "Rewrite": { + "Path": { + "Exact": "/rewrittenpath" + } + }, + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "service6D174F83", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Path": { + "Exact": "/exact" + } + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp22BD49AE9D" + } + }, + "meshgateway1gateway1routehttp2376EB99D6": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "service6D174F83", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Path": { + "Regex": "regex" + } + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp23E44F5774" + } + }, "meshgateway1gateway1routegrpc76486062": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { @@ -1451,6 +1669,128 @@ "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpcCD4D891D" } }, + "meshgateway1gateway1routegrpc2FAC1FF36": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "GrpcRoute": { + "Action": { + "Rewrite": { + "Hostname": { + "DefaultTargetHostname": "DISABLED" + } + }, + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "service6D174F83", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Hostname": { + "Exact": "example.com" + }, + "Metadata": [ + { + "Invert": false, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Exact": "text/html" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Prefix": "application/" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Prefix": "text/" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Suffix": "/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Suffix": "/json+foobar" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Regex": "application/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Regex": "text/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Max-Forward" + }, + { + "Invert": true, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Max-Forward" + } + ] + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpc2AE8379FD" + } + }, "service6D174F83": { "Type": "AWS::AppMesh::VirtualService", "Properties": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 41a047415f5dc..b01bb32cde119 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -339,12 +339,57 @@ gateway.addGatewayRoute('gateway1-route-http', { }), }); +gateway.addGatewayRoute('gateway1-route-http-2', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/', ''), + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), + method: appmesh.HttpRouteMethod.POST, + headers: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valuesIsInRange('Content-Type', 1, 5), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valuesIsNotInRange('Content-Type', 1, 5), + ], + queryParameters: [ + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + ], + rewriteRequestHostname: true, + }, + }), +}); + gateway.addGatewayRoute('gateway1-route-http2', { routeSpec: appmesh.GatewayRouteSpec.http2({ routeTarget: virtualService, }), }); +gateway.addGatewayRoute('gateway1-route-http2-2', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.exactly('/exact', '/rewrittenpath'), + }, + }), +}); + +gateway.addGatewayRoute('gateway1-route-http2-3', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.regex('regex'), + }, + }), +}); + gateway.addGatewayRoute('gateway1-route-grpc', { routeSpec: appmesh.GatewayRouteSpec.grpc({ routeTarget: virtualService, @@ -353,3 +398,25 @@ gateway.addGatewayRoute('gateway1-route-grpc', { }, }), }); + +gateway.addGatewayRoute('gateway1-route-grpc-2', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + rewriteRequestHostname: false, + }, + }), +}); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts index 76f325e99dd10..eb5bf6145282d 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts @@ -1,7 +1,6 @@ import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; - import * as appmesh from '../lib'; export = { @@ -128,10 +127,10 @@ export = { test.throws(() => appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { - prefixPath: 'wrong', + path: appmesh.HttpRoutePathMatch.startsWith('wrong'), }, }).bind(stack), - /Prefix Path must start with \'\/\', got: wrong/); + /Prefix Path for the match must start with \'\/\', got: wrong/); test.done(); }, @@ -172,6 +171,961 @@ export = { test.done(); }, }, + + 'with host name rewrite': { + 'should set default target host name'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + rewriteRequestHostname: true, + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + serviceName: virtualService.virtualServiceName, + rewriteRequestHostname: false, + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Action: { + Rewrite: { + Hostname: { + DefaultTargetHostname: 'ENABLED', + }, + }, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-grpc-route', + Spec: { + GrpcRoute: { + Action: { + Rewrite: { + Hostname: { + DefaultTargetHostname: 'DISABLED', + }, + }, + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with wholePath rewrite': { + 'should set exact path'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP2 Route + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + method: appmesh.HttpRouteMethod.GET, + path: appmesh.HttpGatewayRoutePathMatch.exactly('/test', '/rewrittenPath'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route', + Spec: { + Http2Route: { + Action: { + Rewrite: { + Path: { + Exact: '/rewrittenPath', + }, + }, + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with prefix rewrite': { + 'should set default prefix or value'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/test/', ''), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // Add an HTTP2 Route + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/test/', '/rewrittenUri/'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + + // Add an HTTP2 Route + virtualGateway.addGatewayRoute('gateway-http2-route-1', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/test/'), + }, + }), + gatewayRouteName: 'gateway-http2-route-1', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Action: { + Rewrite: { + Prefix: { + DefaultPrefix: 'DISABLED', + }, + }, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route', + Spec: { + Http2Route: { + Action: { + Rewrite: { + Prefix: { + Value: '/rewrittenUri/', + }, + }, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route-1', + Spec: { + Http2Route: { + Action: { + Rewrite: ABSENT, + }, + }, + }, + })); + + test.done(); + }, + + "should throw an error if the prefix match does not start and end with '/'"(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // WHEN + THEN + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('test/', '/rewrittenUri/'), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + }, /Prefix path for the match must start with \'\/\', got: test\//); + + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/test', '/rewrittenUri/'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + }, /When prefix path for the rewrite is specified, prefix path for the match must end with \'\/\', got: \/test/); + + test.done(); + }, + + "should throw an error if the custom prefix does not start and end with '/'"(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // WHEN + THEN + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/', 'rewrittenUri/'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + }, /Prefix path for the rewrite must start and end with \'\/\', got: rewrittenUri\//); + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http2-route-1', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.startsWith('/', '/rewrittenUri'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + }, /Prefix path for the rewrite must start and end with \'\/\', got: \/rewrittenUri/); + + test.done(); + }, + }, + + 'with host name match': { + 'should match based on host name'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + hostname: appmesh.GatewayRouteHostnameMatch.exactly('example.com'), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + hostname: appmesh.GatewayRouteHostnameMatch.endsWith('.example.com'), + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Hostname: { + Exact: 'example.com', + }, + }, + Action: { + Rewrite: ABSENT, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-grpc-route', + Spec: { + GrpcRoute: { + Match: { + Hostname: { + Suffix: '.example.com', + }, + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with metadata match': { + 'should match based on metadata'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-grpc-route', + Spec: { + GrpcRoute: { + Match: { + Metadata: [ + { + Invert: false, + Match: { Exact: 'application/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Exact: 'text/html' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Prefix: 'application/' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Prefix: 'text/' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Suffix: '/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Suffix: '/json+foobar' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Regex: 'application/.*' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Regex: 'text/.*' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + { + Invert: true, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + ], + }, + }, + }, + })); + + test.done(); + }, + + 'should throw an error if the array length is invalid'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + // size 0 array + metadata: [ + ], + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + }, /Number of metadata provided for matching must be between 1 and 10/); + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-grpc-route-1', { + routeSpec: appmesh.GatewayRouteSpec.grpc({ + routeTarget: virtualService, + match: { + // size 11 array + metadata: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + }, /Number of metadata provided for matching must be between 1 and 10/); + + test.done(); + }, + }, + + 'with header match': { + 'should match based on header'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + headers: [ + appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Headers: [ + { + Invert: false, + Match: { Exact: 'application/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Exact: 'text/html' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Prefix: 'application/' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Prefix: 'text/' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Suffix: '/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Suffix: '/json+foobar' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Regex: 'application/.*' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Regex: 'text/.*' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + { + Invert: true, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + ], + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with method match': { + 'should match based on method'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + method: appmesh.HttpRouteMethod.DELETE, + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Method: 'DELETE', + }, + }, + }, + })); + + test.done(); + }, + }, + + 'with path match': { + 'should match based on path'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.exactly('/exact', undefined), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // Add an HTTP2 Route + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.regex('regex'), + }, + }), + gatewayRouteName: 'gateway-http2-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Path: { + Exact: '/exact', + }, + }, + Action: { + Rewrite: ABSENT, + }, + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route', + Spec: { + Http2Route: { + Match: { + Path: { + Regex: 'regex', + }, + }, + }, + }, + })); + + test.done(); + }, + + 'should throw an error if empty string is passed'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // WHEN + THEN + + test.throws(() => { + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + path: appmesh.HttpGatewayRoutePathMatch.exactly('/exact', ''), + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + }, /Exact Path for the rewrite cannot be empty. Unlike startsWith\(\) method, no automatic rewrite on whole path match/); + + test.done(); + }, + }, + + 'with query paramater match': { + 'should match based on query parameter'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + queryParameters: [ + appmesh.QueryParameterMatch.valueIs('query-field', 'value'), + ], + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + QueryParameters: [ + { + Name: 'query-field', + Match: { + Exact: 'value', + }, + }, + ], + }, + }, + }, + })); + + test.done(); + }, + }, + }, + + 'with empty HTTP/HTTP2match': { + 'should match based on prefix'(test:Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.http()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.http({ + routeTarget: virtualService, + match: { + }, + }), + gatewayRouteName: 'gateway-http-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Match: { + Prefix: '/', + }, + }, + }, + })); + + test.done(); + }, }, 'Can import Gateway Routes using an ARN'(test: Test) { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index f28d443e35317..f6e2a86d6d651 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -1291,7 +1291,7 @@ export = { match: {}, }), }); - }, /At least one gRPC match property must be provided/); + }, /At least one gRPC route match property must be provided/); test.done(); }, From 718d143a376893fb168121b0ff9b57f8a057281e Mon Sep 17 00:00:00 2001 From: Seiya6329 Date: Wed, 14 Jul 2021 19:30:23 -0700 Subject: [PATCH 053/105] feat(appmesh): the App Mesh Construct Library is now Generally Available (stable) (#15560) Shout out to all contributors! Special thanks to @skinny85 , @dfezzie , @alexbrjo , @sshver, and @rishijatia Closes #9489 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 12 +----------- packages/@aws-cdk/aws-appmesh/package.json | 4 ++-- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 02a63b5c0b844..6354289442d16 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -5,17 +5,7 @@ ![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) -> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. -> -> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib - -![cdk-constructs: Developer Preview](https://img.shields.io/badge/cdk--constructs-developer--preview-informational.svg?style=for-the-badge) - -> The APIs of higher level constructs in this module are in **developer preview** before they -> become stable. We will only make breaking changes to address unforeseen API issues. Therefore, -> these APIs are not subject to [Semantic Versioning](https://semver.org/), and breaking changes -> will be announced in release notes. This means that while you may use them, you may need to -> update your source code when upgrading to a newer version of this package. +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) --- diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index 6ce89ff68a1e6..d4d9a0bdfe84c 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -189,8 +189,8 @@ "no-unused-type:@aws-cdk/aws-appmesh.Protocol" ] }, - "stability": "experimental", - "maturity": "developer-preview", + "stability": "stable", + "maturity": "stable", "awscdkio": { "announce": false }, From 165ee3aa89bda7c18fcb4820c0bf2f6905adc4ed Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 15 Jul 2021 12:07:13 +0200 Subject: [PATCH 054/105] feat(pipelines): revised version of the API (#12326) Add a new, modernized API to the `pipelines` library. Advantages of the new API are: - Removes the need to interact with the underlying AWS CodePipeline library for `Artifacts` and `Sources` - A streamlined API for sources (more sensible defaults allowing you to specify less) - `Synth` classes hide less from you, allowing you more control and remove the need to decide whether or not to "eject" from the convenience classes of the original API - Supports parallel deployments (speeding up large pipelines) - Supports stages of >25 stacks - Supports multiple sources powering the build - Gives more control over the CodeBuild projects that get generated In addition, by clearly separating out generic parts of the library from CodePipeline/CodeBuild-specific parts, allows easier development of construct libraries that target alternative deployment systems while reusing large parts of the logic of this library. This does not remove or deprecate the old API, though starting today its use is discouraged in favor of the new API, which will see more development in the future. Closes #10872. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 6 +- .../aws-codepipeline/lib/private/stage.ts | 11 +- packages/@aws-cdk/pipelines/ORIGINAL_API.md | 498 ++++ packages/@aws-cdk/pipelines/README.md | 917 ++++--- .../pipelines/lib/blueprint/asset-type.ts | 15 + .../pipelines/lib/blueprint/file-set.ts | 66 + .../@aws-cdk/pipelines/lib/blueprint/index.ts | 8 + .../lib/blueprint/manual-approval.ts | 37 + .../pipelines/lib/blueprint/script-step.ts | 275 ++ .../lib/blueprint/stack-deployment.ts | 311 +++ .../lib/blueprint/stage-deployment.ts | 118 + .../@aws-cdk/pipelines/lib/blueprint/step.ts | 72 + .../@aws-cdk/pipelines/lib/blueprint/wave.ts | 113 + .../lib/codepipeline/_codebuild-factory.ts | 502 ++++ .../lib/codepipeline/artifact-map.ts | 71 + .../lib/codepipeline/codebuild-step.ts | 189 ++ .../codepipeline-action-factory.ts | 100 + .../lib/codepipeline/codepipeline-source.ts | 354 +++ .../lib/codepipeline/codepipeline.ts | 961 +++++++ .../pipelines/lib/codepipeline/index.ts | 5 + .../pipelines/lib/docker-credentials.ts | 32 +- .../pipelines/lib/helpers-internal/graph.ts | 385 +++ .../pipelines/lib/helpers-internal/index.ts | 2 + .../lib/helpers-internal/pipeline-graph.ts | 319 +++ .../lib/helpers-internal/pipeline-queries.ts | 67 + .../lib/helpers-internal/toposort.ts | 67 + packages/@aws-cdk/pipelines/lib/index.ts | 11 +- .../actions/deploy-cdk-stack-action.ts | 4 +- .../lib/{ => legacy}/actions/index.ts | 0 .../actions/publish-assets-action.ts | 18 +- .../actions/update-pipeline-action.ts | 4 +- .../@aws-cdk/pipelines/lib/legacy/index.ts | 5 + .../pipelines/lib/{ => legacy}/pipeline.ts | 7 +- .../pipelines/lib/{ => legacy}/stage.ts | 7 +- .../lib/{ => legacy}/synths/_util.ts | 0 .../lib/{ => legacy}/synths/index.ts | 0 .../synths/simple-synth-action.ts | 4 +- .../lib/{ => legacy}/validation/_files.ts | 0 .../lib/{ => legacy}/validation/index.ts | 0 .../validation/shell-script-action.ts | 0 packages/@aws-cdk/pipelines/lib/main/index.ts | 1 + .../pipelines/lib/main/pipeline-base.ts | 132 + .../lib/private/cloud-assembly-internals.ts | 13 + .../lib/private/construct-internals.ts | 13 +- .../pipelines/lib/private/javascript.ts | 90 + .../lib/private/template-configuration.ts | 21 + packages/@aws-cdk/pipelines/package.json | 6 + .../actions/update-pipeline-action.test.ts | 52 - .../test/blueprint/fixtures/file-asset1.txt | 1 + .../helpers-internal/dependencies.test.ts | 52 + .../blueprint/helpers-internal/graph.test.ts | 40 + .../helpers-internal/pipeline-graph.test.ts | 264 ++ .../test/blueprint/helpers-internal/util.ts | 38 + .../blueprint/logicalid-stability.test.ts | 122 + .../test/blueprint/stack-deployment.test.ts | 66 + .../test/build-role-policy-statements.test.ts | 57 - .../@aws-cdk/pipelines/test/builds.test.ts | 621 ----- .../assets.test.ts} | 764 +++--- .../test/compliance/basic-behavior.test.ts | 228 ++ .../compliance/docker-credentials.test.ts | 292 +++ .../test/compliance/environments.test.ts | 391 +++ .../test/compliance/escape-hatching.test.ts | 274 ++ .../test/compliance/self-mutation.test.ts | 241 ++ .../{ => compliance}/stack-ordering.test.ts | 97 +- .../pipelines/test/compliance/synths.test.ts | 981 +++++++ .../test/compliance/validations.test.ts | 799 ++++++ .../test/cross-environment-infra.test.ts | 161 -- .../pipelines/test/existing-pipeline.test.ts | 129 - .../test/integ.newpipeline.expected.json | 2336 +++++++++++++++++ .../pipelines/test/integ.newpipeline.ts | 62 + ...nteg.pipeline-with-assets-single-upload.ts | 4 +- .../test/integ.pipeline-with-assets.ts | 4 +- .../@aws-cdk/pipelines/test/integ.pipeline.ts | 2 +- .../pipelines/test/{ => legacy}/fs.test.ts | 2 +- .../@aws-cdk/pipelines/test/pipeline.test.ts | 563 ---- .../assets}/test-docker-asset/Dockerfile | 0 .../assets}/test-file-asset-two.txt | 0 .../assets}/test-file-asset.txt | 0 .../{helpers => testhelpers}/compliance.ts | 43 +- .../pipelines/test/testhelpers/index.ts | 5 + .../test/testhelpers/legacy-pipeline.ts | 48 + .../test/testhelpers/modern-pipeline.ts | 26 + .../pipelines/test/testhelpers/test-app.ts | 214 ++ .../test/{ => testhelpers}/testmatchers.ts | 17 + packages/@aws-cdk/pipelines/test/testutil.ts | 136 - .../pipelines/test/validation.test.ts | 497 ---- scripts/best | 4 + scripts/jest-fail-fast-setup.js | 4 + scripts/print-construct-tree.py | 45 + 89 files changed, 12363 insertions(+), 3156 deletions(-) create mode 100644 packages/@aws-cdk/pipelines/ORIGINAL_API.md create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/asset-type.ts create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/file-set.ts create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/index.ts create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/stack-deployment.ts create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/step.ts create mode 100644 packages/@aws-cdk/pipelines/lib/blueprint/wave.ts create mode 100644 packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts create mode 100644 packages/@aws-cdk/pipelines/lib/codepipeline/artifact-map.ts create mode 100644 packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts create mode 100644 packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts create mode 100644 packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts create mode 100644 packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts create mode 100644 packages/@aws-cdk/pipelines/lib/codepipeline/index.ts create mode 100644 packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts create mode 100644 packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts create mode 100644 packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts create mode 100644 packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-queries.ts create mode 100644 packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts rename packages/@aws-cdk/pipelines/lib/{ => legacy}/actions/deploy-cdk-stack-action.ts (98%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/actions/index.ts (100%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/actions/publish-assets-action.ts (96%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/actions/update-pipeline-action.ts (97%) create mode 100644 packages/@aws-cdk/pipelines/lib/legacy/index.ts rename packages/@aws-cdk/pipelines/lib/{ => legacy}/pipeline.ts (98%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/stage.ts (98%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/synths/_util.ts (100%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/synths/index.ts (100%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/synths/simple-synth-action.ts (99%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/validation/_files.ts (100%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/validation/index.ts (100%) rename packages/@aws-cdk/pipelines/lib/{ => legacy}/validation/shell-script-action.ts (100%) create mode 100644 packages/@aws-cdk/pipelines/lib/main/index.ts create mode 100644 packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts create mode 100644 packages/@aws-cdk/pipelines/lib/private/cloud-assembly-internals.ts create mode 100644 packages/@aws-cdk/pipelines/lib/private/javascript.ts create mode 100644 packages/@aws-cdk/pipelines/lib/private/template-configuration.ts delete mode 100644 packages/@aws-cdk/pipelines/test/actions/update-pipeline-action.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/blueprint/fixtures/file-asset1.txt create mode 100644 packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/graph.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/util.ts create mode 100644 packages/@aws-cdk/pipelines/test/blueprint/logicalid-stability.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/blueprint/stack-deployment.test.ts delete mode 100644 packages/@aws-cdk/pipelines/test/build-role-policy-statements.test.ts delete mode 100644 packages/@aws-cdk/pipelines/test/builds.test.ts rename packages/@aws-cdk/pipelines/test/{pipeline-assets.test.ts => compliance/assets.test.ts} (56%) create mode 100644 packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/compliance/environments.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts rename packages/@aws-cdk/pipelines/test/{ => compliance}/stack-ordering.test.ts (67%) create mode 100644 packages/@aws-cdk/pipelines/test/compliance/synths.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/compliance/validations.test.ts delete mode 100644 packages/@aws-cdk/pipelines/test/cross-environment-infra.test.ts delete mode 100644 packages/@aws-cdk/pipelines/test/existing-pipeline.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json create mode 100644 packages/@aws-cdk/pipelines/test/integ.newpipeline.ts rename packages/@aws-cdk/pipelines/test/{ => legacy}/fs.test.ts (85%) delete mode 100644 packages/@aws-cdk/pipelines/test/pipeline.test.ts rename packages/@aws-cdk/pipelines/test/{ => testhelpers/assets}/test-docker-asset/Dockerfile (100%) rename packages/@aws-cdk/pipelines/test/{ => testhelpers/assets}/test-file-asset-two.txt (100%) rename packages/@aws-cdk/pipelines/test/{ => testhelpers/assets}/test-file-asset.txt (100%) rename packages/@aws-cdk/pipelines/test/{helpers => testhelpers}/compliance.ts (62%) create mode 100644 packages/@aws-cdk/pipelines/test/testhelpers/index.ts create mode 100644 packages/@aws-cdk/pipelines/test/testhelpers/legacy-pipeline.ts create mode 100644 packages/@aws-cdk/pipelines/test/testhelpers/modern-pipeline.ts create mode 100644 packages/@aws-cdk/pipelines/test/testhelpers/test-app.ts rename packages/@aws-cdk/pipelines/test/{ => testhelpers}/testmatchers.ts (61%) delete mode 100644 packages/@aws-cdk/pipelines/test/testutil.ts delete mode 100644 packages/@aws-cdk/pipelines/test/validation.test.ts create mode 100755 scripts/best create mode 100644 scripts/jest-fail-fast-setup.js create mode 100755 scripts/print-construct-tree.py diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index c1e4df100aa23..65b7e84abbf61 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -479,7 +479,7 @@ export class Pipeline extends PipelineBase { } /** @internal */ - public _attachActionToPipeline(stage: Stage, action: IAction, actionScope: CoreConstruct): FullActionDescriptor { + public _attachActionToPipeline(stage: Stage, action: IAction, actionScope: Construct): FullActionDescriptor { const richAction = new RichAction(action, this); // handle cross-region actions here @@ -491,8 +491,8 @@ export class Pipeline extends PipelineBase { // // CodePipeline Variables validateNamespaceName(richAction.actionProperties.variablesNamespace); - // bind the Action - const actionConfig = richAction.bind(actionScope, stage, { + // bind the Action (type h4x) + const actionConfig = richAction.bind(actionScope as CoreConstruct, stage, { role: actionRole ? actionRole : this.role, bucket: crossRegionInfo.artifactBucket, }); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts index e9ed5a6995f02..b5f5aa86dc1c4 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts @@ -1,5 +1,6 @@ import * as events from '@aws-cdk/aws-events'; import * as cdk from '@aws-cdk/core'; +import { Construct, Node } from 'constructs'; import { IAction, IPipeline, IStage } from '../action'; import { Artifact } from '../artifact'; import { CfnPipeline } from '../codepipeline.generated'; @@ -137,7 +138,15 @@ export class Stage implements IStage { private attachActionToPipeline(action: IAction): FullActionDescriptor { // notify the Pipeline of the new Action - const actionScope = new cdk.Construct(this.scope, action.actionProperties.actionName); + // + // It may be that a construct already exists with the given action name (CDK Pipelines + // may do this to maintain construct tree compatibility between versions). + // + // If so, we simply reuse it. + let actionScope = Node.of(this.scope).tryFindChild(action.actionProperties.actionName) as Construct | undefined; + if (!actionScope) { + actionScope = new cdk.Construct(this.scope, action.actionProperties.actionName); + } return this._pipeline._attachActionToPipeline(this, action, actionScope); } diff --git a/packages/@aws-cdk/pipelines/ORIGINAL_API.md b/packages/@aws-cdk/pipelines/ORIGINAL_API.md new file mode 100644 index 0000000000000..3f1bd5920bcd2 --- /dev/null +++ b/packages/@aws-cdk/pipelines/ORIGINAL_API.md @@ -0,0 +1,498 @@ +# CDK Pipelines, original API + +This document describes the API the CDK Pipelines library originally went into +Developer Preview with. The API has since been reworked, but the original one +left in place because of popular uptake. The original API still works and is +still supported, but the revised one is preferred for future projects. + +## Definining the pipeline + +In the original API, you have to import the `aws-codepipeline` construct +library and create `Artifact` objects for the source and Cloud Assembly +artifacts: + +```ts +import { Construct, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; +import { CdkPipeline } from '@aws-cdk/pipelines'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; + +/** + * Stack to hold the pipeline + */ +class MyPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact(); + + const pipeline = new CdkPipeline(this, 'Pipeline', { + cloudAssemblyArtifact, + + sourceAction: new codepipeline_actions.GitHubSourceAction({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), + // Replace these with your actual GitHub project name + owner: 'OWNER', + repo: 'REPO', + branch: 'main', // default: 'master' + }), + + synthAction: SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + + // Use this if you need a build step (if you're not using ts-node + // or if you have TypeScript Lambdas that need to be compiled). + buildCommand: 'npm run build', + }), + }); + + // Do this as many times as necessary with any account and region + // Account and region may different from the pipeline's. + pipeline.addApplicationStage(new MyApplication(this, 'Prod', { + env: { + account: '123456789012', + region: 'eu-west-1', + } + })); + } +} +``` + +### A note on cost + +By default, the `CdkPipeline` construct creates an AWS Key Management Service +(AWS KMS) Customer Master Key (CMK) for you to encrypt the artifacts in the +artifact bucket, which incurs a cost of +**$1/month**. This default configuration is necessary to allow cross-account +deployments. + +If you do not intend to perform cross-account deployments, you can disable +the creation of the Customer Master Keys by passing `crossAccountKeys: false` +when defining the Pipeline: + +```ts +const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { + crossAccountKeys: false, + + // ... +}); +``` + +### Defining the Pipeline (Source and Synth) + +The pipeline is defined by instantiating `CdkPipeline` in a Stack. This defines the +source location for the pipeline as well as the build commands. For example, the following +defines a pipeline whose source is stored in a GitHub repository, and uses NPM +to build. The Pipeline will be provisioned in account `111111111111` and region +`eu-west-1`: + +```ts +class MyPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact(); + + const pipeline = new CdkPipeline(this, 'Pipeline', { + pipelineName: 'MyAppPipeline', + cloudAssemblyArtifact, + + sourceAction: new codepipeline_actions.GitHubSourceAction({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), + // Replace these with your actual GitHub project name + owner: 'OWNER', + repo: 'REPO', + branch: 'main', // default: 'master' + }), + + synthAction: SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + + // Optionally specify a VPC in which the action runs + vpc: new ec2.Vpc(this, 'NpmSynthVpc'), + + // Use this if you need a build step (if you're not using ts-node + // or if you have TypeScript Lambdas that need to be compiled). + buildCommand: 'npm run build', + }), + }); + } +} + +const app = new App(); +new MyPipelineStack(app, 'PipelineStack', { + env: { + account: '111111111111', + region: 'eu-west-1', + } +}); +``` + +If you prefer more control over the underlying CodePipeline object, you can +create one yourself, including custom Source and Build stages: + +```ts +const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [...], + }, + { + stageName: 'CustomBuild', + actions: [...], + }, + ], +}); + +const app = new App(); +const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { + codePipeline, + cloudAssemblyArtifact, +}); +``` + +If you use assets for files or Docker images, every asset will get its own upload action during the asset stage. +By setting the value `singlePublisherPerType` to `true`, only one action for files and one action for +Docker images is created that handles all assets of the respective type. + +If you need to run commands to setup proxies, mirrors, etc you can supply them using the `assetPreInstallCommands`. + +#### Sources + +Any of the regular sources from the [`@aws-cdk/aws-codepipeline-actions`](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-codepipeline-actions-readme.html#github) module can be used. + +#### Synths + +You define how to build and synth the project by specifying a `synthAction`. +This can be any CodePipeline action that produces an artifact with a CDK +Cloud Assembly in it (the contents of the `cdk.out` directory created when +`cdk synth` is called). Pass the output artifact of the synth in the +Pipeline's `cloudAssemblyArtifact` property. + +`SimpleSynthAction` is available for synths that can be performed by running a couple +of simple shell commands (install, build, and synth) using AWS CodeBuild. When +using these, the source repository does not need to have a `buildspec.yml`. An example +of using `SimpleSynthAction` to run a Maven build followed by a CDK synth: + +```ts +const pipeline = new CdkPipeline(this, 'Pipeline', { + // ... + synthAction: new SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + installCommands: ['npm install -g aws-cdk'], + buildCommands: ['mvn package'], + synthCommand: 'cdk synth', + }) +}); +``` + +Available as factory functions on `SimpleSynthAction` are some common +convention-based synth: + +* `SimpleSynthAction.standardNpmSynth()`: build using NPM conventions. Expects a `package-lock.json`, + a `cdk.json`, and expects the CLI to be a versioned dependency in `package.json`. Does + not perform a build step by default. +* `CdkSynth.standardYarnSynth()`: build using Yarn conventions. Expects a `yarn.lock` + a `cdk.json`, and expects the CLI to be a versioned dependency in `package.json`. Does + not perform a build step by default. + +If you need a custom build/synth step that is not covered by `SimpleSynthAction`, you can +always add a custom CodeBuild project and pass a corresponding `CodeBuildAction` to the +pipeline. + +#### Add Additional permissions to the CodeBuild Project Role for building and synthesizing + +You can customize the role permissions used by the CodeBuild project so it has access to +the needed resources. eg: Adding CodeArtifact repo permissions so we pull npm packages +from the CA repo instead of NPM. + +```ts +class MyPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + ... + const pipeline = new CdkPipeline(this, 'Pipeline', { + ... + synthAction: SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + + // Use this to customize and a permissions required for the build + // and synth + rolePolicyStatements: [ + new PolicyStatement({ + actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], + resources: ['arn:codeartifact:repo:arn'], + }), + ], + + // Then you can login to codeartifact repository + // and npm will now pull packages from your repository + // Note the codeartifact login command requires more params to work. + buildCommands: [ + 'aws codeartifact login --tool npm', + 'npm run build', + ], + }), + }); + } +} +``` + +### Adding Application Stages + +To define an application that can be added to the pipeline integrally, define a subclass +of `Stage`. The `Stage` can contain one or more stack which make up your application. If +there are dependencies between the stacks, the stacks will automatically be added to the +pipeline in the right order. Stacks that don't depend on each other will be deployed in +parallel. You can add a dependency relationship between stacks by calling +`stack1.addDependency(stack2)`. + +Stages take a default `env` argument which the Stacks inside the Stage will fall back to +if no `env` is defined for them. + +An application is added to the pipeline by calling `addApplicationStage()` with instances +of the Stage. The same class can be instantiated and added to the pipeline multiple times +to define different stages of your DTAP or multi-region application pipeline: + +```ts +// Testing stage +pipeline.addApplicationStage(new MyApplication(this, 'Testing', { + env: { account: '111111111111', region: 'eu-west-1' } +})); + +// Acceptance stage +pipeline.addApplicationStage(new MyApplication(this, 'Acceptance', { + env: { account: '222222222222', region: 'eu-west-1' } +})); + +// Production stage +pipeline.addApplicationStage(new MyApplication(this, 'Production', { + env: { account: '333333333333', region: 'eu-west-1' } +})); +``` + +> Be aware that adding new stages via `addApplicationStage()` will +> automatically add them to the pipeline and deploy the new stacks, but +> *removing* them from the pipeline or deleting the pipeline stack will not +> automatically delete deployed application stacks. You must delete those +> stacks by hand using the AWS CloudFormation console or the AWS CLI. + +### More Control + +Every *Application Stage* added by `addApplicationStage()` will lead to the addition of +an individual *Pipeline Stage*, which is subsequently returned. You can add more +actions to the stage by calling `addAction()` on it. For example: + +```ts +const testingStage = pipeline.addApplicationStage(new MyApplication(this, 'Testing', { + env: { account: '111111111111', region: 'eu-west-1' } +})); + +// Add a action -- in this case, a Manual Approval action +// (for illustration purposes: testingStage.addManualApprovalAction() is a +// convenience shorthand that does the same) +testingStage.addAction(new ManualApprovalAction({ + actionName: 'ManualApproval', + runOrder: testingStage.nextSequentialRunOrder(), +})); +``` + +You can also add more than one *Application Stage* to one *Pipeline Stage*. For example: + +```ts +// Create an empty pipeline stage +const testingStage = pipeline.addStage('Testing'); + +// Add two application stages to the same pipeline stage +testingStage.addApplication(new MyApplication1(this, 'MyApp1', { + env: { account: '111111111111', region: 'eu-west-1' } +})); +testingStage.addApplication(new MyApplication2(this, 'MyApp2', { + env: { account: '111111111111', region: 'eu-west-1' } +})); +``` + +Even more, adding a manual approval action or reserving space for some extra sequential actions +between 'Prepare' and 'Execute' ChangeSet actions is possible. + +```ts + pipeline.addApplicationStage(new MyApplication(this, 'Production'), { + manualApprovals: true, + extraRunOrderSpace: 1, + }); +``` + +### Adding validations to the pipeline + +You can add any type of CodePipeline Action to the pipeline in order to validate +the deployments you are performing. + +The CDK Pipelines construct library comes with a `ShellScriptAction` which uses AWS CodeBuild +to run a set of shell commands (potentially running a test set that comes with your application, +using stack outputs of the deployed stacks). + +In its simplest form, adding validation actions looks like this: + +```ts +const stage = pipeline.addApplicationStage(new MyApplication(/* ... */)); + +stage.addActions(new ShellScriptAction({ + actionName: 'MyValidation', + commands: ['curl -Ssf https://my.webservice.com/'], + // Optionally specify a VPC if, for example, the service is deployed with a private load balancer + vpc, + // Optionally specify SecurityGroups + securityGroups, + // Optionally specify a BuildEnvironment + environment, +})); +``` + +#### Using CloudFormation Stack Outputs in ShellScriptAction + +Because many CloudFormation deployments result in the generation of resources with unpredictable +names, validations have support for reading back CloudFormation Outputs after a deployment. This +makes it possible to pass (for example) the generated URL of a load balancer to the test set. + +To use Stack Outputs, expose the `CfnOutput` object you're interested in, and +call `pipeline.stackOutput()` on it: + +```ts +class MyLbApplication extends Stage { + public readonly loadBalancerAddress: CfnOutput; + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const lbStack = new LoadBalancerStack(this, 'Stack'); + + // Or create this in `LoadBalancerStack` directly + this.loadBalancerAddress = new CfnOutput(lbStack, 'LbAddress', { + value: `https://${lbStack.loadBalancer.loadBalancerDnsName}/` + }); + } +} + +const lbApp = new MyLbApplication(this, 'MyApp', { + env: { /* ... */ } +}); +const stage = pipeline.addApplicationStage(lbApp); +stage.addActions(new ShellScriptAction({ + // ... + useOutputs: { + // When the test is executed, this will make $URL contain the + // load balancer address. + URL: pipeline.stackOutput(lbApp.loadBalancerAddress), + } +}); +``` + +#### Using additional files in Shell Script Actions + +As part of a validation, you probably want to run a test suite that's more +elaborate than what can be expressed in a couple of lines of shell script. +You can bring additional files into the shell script validation by supplying +the `additionalArtifacts` property. + +Here are some typical examples for how you might want to bring in additional +files from several sources: + +* Directory from the source repository +* Additional compiled artifacts from the synth step + +#### Controlling IAM permissions + +IAM permissions can be added to the execution role of a `ShellScriptAction` in +two ways. + +Either pass additional policy statements in the `rolePolicyStatements` property: + +```ts +new ShellScriptAction({ + // ... + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['s3:GetObject'], + resources: ['*'], + }), + ], +})); +``` + +The Action can also be used as a Grantable after having been added to a Pipeline: + +```ts +const action = new ShellScriptAction({ /* ... */ }); +pipeline.addStage('Test').addActions(action); + +bucket.grantRead(action); +``` + +#### Additional files from the source repository + +Bringing in additional files from the source repository is appropriate if the +files in the source repository are directly usable in the test (for example, +if they are executable shell scripts themselves). Pass the `sourceArtifact`: + +```ts +const sourceArtifact = new codepipeline.Artifact(); + +const pipeline = new CdkPipeline(this, 'Pipeline', { + // ... +}); + +const validationAction = new ShellScriptAction({ + actionName: 'TestUsingSourceArtifact', + additionalArtifacts: [sourceArtifact], + + // 'test.sh' comes from the source repository + commands: ['./test.sh'], +}); +``` + +#### Additional files from the synth step + +Getting the additional files from the synth step is appropriate if your +tests need the compilation step that is done as part of synthesis. + +On the synthesis step, specify `additionalArtifacts` to package +additional subdirectories into artifacts, and use the same artifact +in the `ShellScriptAction`'s `additionalArtifacts`: + +```ts +// If you are using additional output artifacts from the synth step, +// they must be named. +const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); +const integTestsArtifact = new codepipeline.Artifact('IntegTests'); + +const pipeline = new CdkPipeline(this, 'Pipeline', { + synthAction: SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + buildCommands: ['npm run build'], + additionalArtifacts: [ + { + directory: 'test', + artifact: integTestsArtifact, + } + ], + }), + // ... +}); + +const validationAction = new ShellScriptAction({ + actionName: 'TestUsingBuildArtifact', + additionalArtifacts: [integTestsArtifact], + // 'test.js' was produced from 'test/test.ts' during the synth step + commands: ['node ./test.js'], +}); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 6e4f73c895b1f..76a8c3a84f8fb 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -17,19 +17,36 @@ A construct library for painless Continuous Delivery of CDK applications. -![Developer Preview](https://img.shields.io/badge/developer--preview-informational.svg?style=for-the-badge) - -> This module is in **developer preview**. We may make breaking changes to address unforeseen API issues. Therefore, these APIs are not subject to [Semantic Versioning](https://semver.org/), and breaking changes will be announced in release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. +> This module contains two sets of APIs: an **original** and a **modern** version of +CDK Pipelines. The *modern* API has been updated to be easier to work with and +customize, and will be the preferred API going forward. The *original* version +of the API is still available for backwards compatibility, but we recommend migrating +to the new version if possible. +> +> Compared to the original API, the modern API: has more sensible defaults; is +> more flexible; supports parallel deployments; supports multiple synth inputs; +> allows more control of CodeBuild project generation; supports deployment +> engines other than CodePipeline. +> +> The README for the original API can be found in [our GitHub repository](https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/pipelines/ORIGINAL_API.md). ## At a glance -Defining a pipeline for your application is as simple as defining a subclass -of `Stage`, and calling `pipeline.addApplicationStage()` with instances of -that class. Deploying to a different account or region looks exactly the -same, the *CDK Pipelines* library takes care of the details. +Deploying your application continuously starts by defining a +`MyApplicationStage`, a subclass of `Stage` that contains the stacks that make +up a single copy of your application. -(Note that have to *bootstrap* all environments before the following code -will work, see the section **CDK Environment Bootstrapping** below). +You then define a `Pipeline`, instantiate as many instances of +`MyApplicationStage` as you want for your test and production environments, with +different parameters for each, and calling `pipeline.addStage()` for each of +them. You can deploy to the same account and Region, or to a different one, +with the same amount of code. The *CDK Pipelines* library takes care of the +details. + +CDK Pipelines supports multiple *deployment engines* (see below), and comes with +a deployment engine that deployes CDK apps using AWS CodePipeline. To use the +CodePipeline engine, define a `CodePipeline` construct. The following +example creates a CodePipeline that deploys an application from GitHub: ```ts /** The stacks for our app are defined in my-stacks.ts. The internals of these @@ -38,10 +55,42 @@ will work, see the section **CDK Environment Bootstrapping** below). * to this table in its properties. */ import { DatabaseStack, ComputeStack } from '../lib/my-stacks'; - import { Construct, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; -import { CdkPipeline } from '@aws-cdk/pipelines'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import { CodePipeline, CodePipelineSource, ShellStep } from '@aws-cdk/pipelines'; + +/** + * Stack to hold the pipeline + */ +class MyPipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new CodePipeline(this, 'Pipeline', { + synth: new ShellStep('Synth', { + // Use a connection created using the AWS console to authenticate to GitHub + // Other sources are available. + input: CodePipelineSource.connection('my-org/my-app', 'main', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console * });', + }), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), + }); + + // 'MyApplication' is defined below. Call `addStage` as many times as + // necessary with any account and region (may be different from the + // pipeline's). + pipeline.addStage(new MyApplication(this, 'Prod', { + env: { + account: '123456789012', + region: 'eu-west-1', + } + })); + } +} /** * Your application @@ -62,30 +111,13 @@ class MyApplication extends Stage { } } -/** - * Stack to hold the pipeline - */ -class MyPipelineStack extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); - - const sourceArtifact = new codepipeline.Artifact(); - const cloudAssemblyArtifact = new codepipeline.Artifact(); - - const pipeline = new CdkPipeline(this, 'Pipeline', { - // ...source and build information here (see below) - }); - - // Do this as many times as necessary with any account and region - // Account and region may different from the pipeline's. - pipeline.addApplicationStage(new MyApplication(this, 'Prod', { - env: { - account: '123456789012', - region: 'eu-west-1', - } - })); +// In your main file +new MyPipelineStack(app, 'PipelineStack', { + env: { + account: '123456789012', + region: 'eu-west-1', } -} +}); ``` The pipeline is **self-mutating**, which means that if you add new @@ -93,10 +125,13 @@ application stages in the source code, or new stacks to `MyApplication`, the pipeline will automatically reconfigure itself to deploy those new stages and stacks. +(Note that have to *bootstrap* all environments before the above code +will work, see the section **CDK Environment Bootstrapping** below). + ## CDK Versioning -This library uses prerelease features of the CDK framework, which can be enabled by adding the -following to `cdk.json`: +This library uses prerelease features of the CDK framework, which can be enabled +by adding the following to `cdk.json`: ```js { @@ -107,484 +142,538 @@ following to `cdk.json`: } ``` -## A note on cost +## Provisioning the pipeline -By default, the `CdkPipeline` construct creates an AWS Key Management Service -(AWS KMS) Customer Master Key (CMK) for you to encrypt the artifacts in the -artifact bucket, which incurs a cost of -**$1/month**. This default configuration is necessary to allow cross-account -deployments. +To provision the pipeline you have defined, making sure the target environment +has been bootstrapped (see below), and then executing deploying the +`PipelineStack` *once*. Afterwards, the pipeline will keep itself up-to-date. -If you do not intend to perform cross-account deployments, you can disable -the creation of the Customer Master Keys by passing `crossAccountKeys: false` -when defining the Pipeline: +> **Important**: be sure to `git commit` and `git push` before deploying the +> Pipeline stack using `cdk deploy`! +> +> The reason is that the pipeline will start deploying and self-mutating +> right away based on the sources in the repository, so the sources it finds +> in there should be the ones you want it to find. -```ts -const pipeline = new pipelines.CdkPipeline(this, 'Pipeline', { - crossAccountKeys: false, +Run the following commands to get the pipeline going: - // ... -}); +```console +$ git commit -a +$ git push +$ cdk deploy PipelineStack ``` -## Defining the Pipeline (Source and Synth) +Administrative permissions to the account are only necessary up until +this point. We recommend you shed access to these credentials after doing this. + +### Working on the pipeline + +The self-mutation feature of the Pipeline might at times get in the way +of the pipeline development workflow. Each change to the pipeline must be pushed +to git, otherwise, after the pipeline was updated using `cdk deploy`, it will +automatically revert to the state found in git. -The pipeline is defined by instantiating `CdkPipeline` in a Stack. This defines the -source location for the pipeline as well as the build commands. For example, the following -defines a pipeline whose source is stored in a GitHub repository, and uses NPM -to build. The Pipeline will be provisioned in account `111111111111` and region -`eu-west-1`: +To make the development more convenient, the self-mutation feature can be turned +off temporarily, by passing `selfMutation: false` property, example: ```ts -class MyPipelineStack extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); +// Modern API +const pipeline = new CodePipeline(this, 'Pipeline', { + selfMutation: false, + ... +}); - const sourceArtifact = new codepipeline.Artifact(); - const cloudAssemblyArtifact = new codepipeline.Artifact(); - - const pipeline = new CdkPipeline(this, 'Pipeline', { - pipelineName: 'MyAppPipeline', - cloudAssemblyArtifact, - - sourceAction: new codepipeline_actions.GitHubSourceAction({ - actionName: 'GitHub', - output: sourceArtifact, - oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), - // Replace these with your actual GitHub project name - owner: 'OWNER', - repo: 'REPO', - branch: 'main', // default: 'master' - }), +// Original API +const pipeline = new CdkPipeline(this, 'Pipeline', { + selfMutating: false, + ... +}); +``` - synthAction: SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, +## Definining the pipeline - // Optionally specify a VPC in which the action runs - vpc: new ec2.Vpc(this, 'NpmSynthVpc'), +This section of the documentation describes the AWS CodePipeline engine, which +comes with this library. If you want to use a different deployment engine, read +the section *Using a different deployment engine* below. - // Use this if you need a build step (if you're not using ts-node - // or if you have TypeScript Lambdas that need to be compiled). - buildCommand: 'npm run build', - }), - }); - } -} +### Synth and sources -const app = new App(); -new MyPipelineStack(app, 'PipelineStack', { - env: { - account: '111111111111', - region: 'eu-west-1', - } -}); -``` +To define a pipeline, instantiate a `CodePipeline` construct from the +`@aws-cdk/pipelines` module. It takes one argument, a `synth` step, which is +expected to produce the CDK Cloud Assembly as its single output (the contents of +the `cdk.out` directory after running `cdk synth`). "Steps" are arbitrary +actions in the pipeline, typically used to run scripts or commands. -If you prefer more control over the underlying CodePipeline object, you can -create one yourself, including custom Source and Build stages: +For the synth, use a `ShellStep` and specify the commands necessary to build +your project and run `cdk synth`; the specific commands required will depend on +the programming language you are using. For a typical NPM-based project, the synth +will look like this: ```ts -const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { - stages: [ - { - stageName: 'CustomSource', - actions: [...], - }, - { - stageName: 'CustomBuild', - actions: [...], - }, - ], +const source = /* the repository source */; + +const pipeline = new CodePipeline(this, 'Pipeline', { + synth: new ShellStep('Synth', { + input: source, + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), }); +``` -const app = new App(); -const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { - codePipeline, - cloudAssemblyArtifact, +The pipeline assumes that your `ShellStep` will produce a `cdk.out` +directory in the root, containing the CDK cloud assembly. If your +CDK project lives in a subdirectory, be sure to adjust the +`primaryOutputDirectory` to match: + +```ts +const pipeline = new CodePipeline(this, 'Pipeline', { + synth: new ShellStep('Synth', { + input: source, + commands: [ + 'cd mysubdir', + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + primaryOutputDirectory: 'mysubdir/cdk.out', + }), }); ``` -If you use assets for files or Docker images, every asset will get its own upload action during the asset stage. -By setting the value `singlePublisherPerType` to `true`, only one action for files and one action for -Docker images is created that handles all assets of the respective type. +The underlying `@aws-cdk/aws-codepipeline.Pipeline` construct will be produced +when `app.synth()` is called. You can also force it to be produced +earlier by calling `pipeline.buildPipeline()`. After you've called +that method, you can inspect the constructs that were produced by +accessing the properties of the `pipeline` object. -If you need to run commands to setup proxies, mirrors, etc you can supply them using the `assetPreInstallCommands`. +#### CodePipeline Sources -## Initial pipeline deployment +In CodePipeline, *Sources* define where the source of your application lives. +When a change to the source is detected, the pipeline will start executing. +Source objects can be created by factory methods on the `CodePipelineSource` class: -You provision this pipeline by making sure the target environment has been -bootstrapped (see below), and then executing deploying the `PipelineStack` -*once*. Afterwards, the pipeline will keep itself up-to-date. - -> **Important**: be sure to `git commit` and `git push` before deploying the -> Pipeline stack using `cdk deploy`! -> -> The reason is that the pipeline will start deploying and self-mutating -> right away based on the sources in the repository, so the sources it finds -> in there should be the ones you want it to find. +##### GitHub, GitHub Enterprise, BitBucket using a connection -Run the following commands to get the pipeline going: +The recommended way of connecting to GitHub or BitBucket is by using a *connection*. +You will first use the AWS Console to authenticate to the source control +provider, and then use the connection ARN in your pipeline definition: -```console -$ git commit -a -$ git push -$ cdk deploy PipelineStack +```ts +CodePipelineSource.connection('org/repo', 'branch', { + connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', +}); ``` -Administrative permissions to the account are only necessary up until -this point. We recommend you shed access to these credentials after doing this. +##### GitHub using OAuth -### Sources +You can also authenticate to GitHub using a personal access token. This expects +that you've created a personal access token and stored it in Secrets Manager. +By default, the source object will look for a secret named **github-token**, but +you can change the name. The token should have the **repo** and **admin:repo_hook** +scopes. -Any of the regular sources from the [`@aws-cdk/aws-codepipeline-actions`](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-codepipeline-actions-readme.html#github) module can be used. - -### Synths +```ts +CodePipelineSource.gitHub('org/repo', 'branch', { + // This is optional + authentication: SecretValue.secretsManager('my-token'), +}); +``` -You define how to build and synth the project by specifying a `synthAction`. -This can be any CodePipeline action that produces an artifact with a CDK -Cloud Assembly in it (the contents of the `cdk.out` directory created when -`cdk synth` is called). Pass the output artifact of the synth in the -Pipeline's `cloudAssemblyArtifact` property. +##### CodeCommit -`SimpleSynthAction` is available for synths that can be performed by running a couple -of simple shell commands (install, build, and synth) using AWS CodeBuild. When -using these, the source repository does not need to have a `buildspec.yml`. An example -of using `SimpleSynthAction` to run a Maven build followed by a CDK synth: +You can use a CodeCommit repository as the source. Either create or import +that the CodeCommit repository and then use `CodePipelineSource.codeCommit` +to reference it: ```ts -const pipeline = new CdkPipeline(this, 'Pipeline', { - // ... - synthAction: new SimpleSynthAction({ - sourceArtifact, - cloudAssemblyArtifact, - installCommands: ['npm install -g aws-cdk'], - buildCommands: ['mvn package'], - synthCommand: 'cdk synth', - }) -}); +const repository = codecommit.fromRepositoryName(this, 'Repository', 'my-repository'); +CodePipelineSource.codeCommit(repository); ``` -Available as factory functions on `SimpleSynthAction` are some common -convention-based synth: +##### S3 -* `SimpleSynthAction.standardNpmSynth()`: build using NPM conventions. Expects a `package-lock.json`, - a `cdk.json`, and expects the CLI to be a versioned dependency in `package.json`. Does - not perform a build step by default. -* `CdkSynth.standardYarnSynth()`: build using Yarn conventions. Expects a `yarn.lock` - a `cdk.json`, and expects the CLI to be a versioned dependency in `package.json`. Does - not perform a build step by default. +You can use a zip file in S3 as the source of the pipeline. The pipeline will be +triggered every time the file in S3 is changed: -If you need a custom build/synth step that is not covered by `SimpleSynthAction`, you can -always add a custom CodeBuild project and pass a corresponding `CodeBuildAction` to the -pipeline. +```ts +const bucket = s3.Bucket.fromBucketName(this, 'Bucket', 'my-bucket'); +CodePipelineSource.s3(bucket, 'my/source.zip'); +``` -## Adding Application Stages +#### Additional inputs -To define an application that can be added to the pipeline integrally, define a subclass -of `Stage`. The `Stage` can contain one or more stack which make up your application. If -there are dependencies between the stacks, the stacks will automatically be added to the -pipeline in the right order. Stacks that don't depend on each other will be deployed in -parallel. You can add a dependency relationship between stacks by calling -`stack1.addDependency(stack2)`. +`ShellStep` allows passing in more than one input: additional +inputs will be placed in the directories you specify. Any step that produces an +output file set can be used as an input, such as a `CodePipelineSource`, but +also other `ShellStep`: -Stages take a default `env` argument which the Stacks inside the Stage will fall back to -if no `env` is defined for them. +```ts +const prebuild = new ShellStep('Prebuild', { + input: CodePipelineSource.gitHub('myorg/repo1'), + primaryOutputDirectory: './build', + commands: ['./build.sh'], +}); -An application is added to the pipeline by calling `addApplicationStage()` with instances -of the Stage. The same class can be instantiated and added to the pipeline multiple times -to define different stages of your DTAP or multi-region application pipeline: +const pipeline = new CodePipeline(this, 'Pipeline', { + synth: new ShellStep('Synth', { + input: CodePipelineSource.gitHub('myorg/repo2'), + additionalInputs: { + 'subdir': CodePipelineSource.gitHub('myorg/repo3'), + '../siblingdir': prebuild, + }, -```ts -// Testing stage -pipeline.addApplicationStage(new MyApplication(this, 'Testing', { - env: { account: '111111111111', region: 'eu-west-1' } -})); + commands: ['./build.sh'], + }) +}); +``` -// Acceptance stage -pipeline.addApplicationStage(new MyApplication(this, 'Acceptance', { - env: { account: '222222222222', region: 'eu-west-1' } -})); +### CDK application deployments -// Production stage -pipeline.addApplicationStage(new MyApplication(this, 'Production', { - env: { account: '333333333333', region: 'eu-west-1' } +After you have defined the pipeline and the `synth` step, you can add one or +more CDK `Stages` which will be deployed to their target environments. To do +so, call `pipeline.addStage()` on the Stage object: + +```ts +// Do this as many times as necessary with any account and region +// Account and region may different from the pipeline's. +pipeline.addStage(new MyApplicationStage(this, 'Prod', { + env: { + account: '123456789012', + region: 'eu-west-1', + } })); ``` -> Be aware that adding new stages via `addApplicationStage()` will -> automatically add them to the pipeline and deploy the new stacks, but -> *removing* them from the pipeline or deleting the pipeline stack will not -> automatically delete deployed application stacks. You must delete those -> stacks by hand using the AWS CloudFormation console or the AWS CLI. +CDK Pipelines will automatically discover all `Stacks` in the given `Stage` +object, determine their dependency order, and add appropriate actions to the +pipeline to publish the assets referenced in those stacks and deploy the stacks +in the right order. + +If the `Stacks` are targeted at an environment in a different AWS account or +Region and that environment has been +[bootstrapped](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html) +, CDK Pipelines will transparently make sure the IAM roles are set up +correctly and any requisite replication Buckets are created. -### More Control +#### Deploying in parallel -Every *Application Stage* added by `addApplicationStage()` will lead to the addition of -an individual *Pipeline Stage*, which is subsequently returned. You can add more -actions to the stage by calling `addAction()` on it. For example: +By default, all applications added to CDK Pipelines by calling `addStage()` will +be deployed in sequence, one after the other. If you have a lot of stages, you can +speed up the pipeline by choosing to deploy some stages in parallel. You do this +by calling `addWave()` instead of `addStage()`: a *wave* is a set of stages that +are all deployed in parallel instead of sequentially. Waves themselves are still +deployed in sequence. For example, the following will deploy two copies of your +application to `eu-west-1` and `eu-central-1` in parallel: ```ts -const testingStage = pipeline.addApplicationStage(new MyApplication(this, 'Testing', { - env: { account: '111111111111', region: 'eu-west-1' } +const europeWave = pipeline.addWave('Europe'); +europeWave.addStage(new MyApplicationStage(this, 'Ireland', { + env: { region: 'eu-west-1' } })); - -// Add a action -- in this case, a Manual Approval action -// (for illustration purposes: testingStage.addManualApprovalAction() is a -// convenience shorthand that does the same) -testingStage.addAction(new ManualApprovalAction({ - actionName: 'ManualApproval', - runOrder: testingStage.nextSequentialRunOrder(), +europeWave.addStage(new MyApplicationStage(this, 'Germany', { + env: { region: 'eu-central-1' } })); ``` -You can also add more than one *Application Stage* to one *Pipeline Stage*. For example: +#### Deploying to other accounts / encrypting the Artifact Bucket -```ts -// Create an empty pipeline stage -const testingStage = pipeline.addStage('Testing'); +CDK Pipelines can transparently deploy to other Regions and other accounts +(provided those target environments have been +[*bootstrapped*](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html)). +However, deploying to another account requires one additional piece of +configuration: you need to enable `crossAccountKeys: true` when creating the +pipeline. -// Add two application stages to the same pipeline stage -testingStage.addApplication(new MyApplication1(this, 'MyApp1', { - env: { account: '111111111111', region: 'eu-west-1' } -})); -testingStage.addApplication(new MyApplication2(this, 'MyApp2', { - env: { account: '111111111111', region: 'eu-west-1' } -})); -``` +This will encrypt the artifact bucket(s), but incurs a cost for maintaining the +KMS key. -Even more, adding a manual approval action or reserving space for some extra sequential actions -between 'Prepare' and 'Execute' ChangeSet actions is possible. +Example: ```ts - pipeline.addApplicationStage(new MyApplication(this, 'Production'), { - manualApprovals: true, - extraRunOrderSpace: 1, - }); +const pipeline = new CodePipeline(this, 'Pipeline', { + // Encrypt artifacts, required for cross-account deployments + crossAccountKeys: true, +}); ``` -## Adding validations to the pipeline - -You can add any type of CodePipeline Action to the pipeline in order to validate -the deployments you are performing. +### Validation -The CDK Pipelines construct library comes with a `ShellScriptAction` which uses AWS CodeBuild -to run a set of shell commands (potentially running a test set that comes with your application, -using stack outputs of the deployed stacks). +Every `addStage()` and `addWave()` command takes additional options. As part of these options, +you can specify `pre` and `post` steps, which are arbitrary steps that run before or after +the contents of the stage or wave, respectively. You can use these to add validations like +manual or automated gates to your pipeline. -In its simplest form, adding validation actions looks like this: +The following example shows both an automated approval in the form of a `ShellStep`, and +a manual approvel in the form of a `ManualApprovalStep` added to the pipeline. Both must +pass in order to promote from the `PreProd` to the `Prod` environment: ```ts -const stage = pipeline.addApplicationStage(new MyApplication(/* ... */)); - -stage.addActions(new ShellScriptAction({ - actionName: 'MyValidation', - commands: ['curl -Ssf https://my.webservice.com/'], - // Optionally specify a VPC if, for example, the service is deployed with a private load balancer - vpc, - // Optionally specify SecurityGroups - securityGroups, - // Optionally specify a BuildEnvironment - environment, -})); +const preprod = new MyApplicationStage(this, 'PreProd', { ... }); +const prod = new MyApplicationStage(this, 'Prod', { ... }); + +pipeline.addStage(preprod, { + post: [ + new ShellStep('Validate Endpoint', { + commands: ['curl -Ssf https://my.webservice.com/'], + }), + ], +}); +pipeline.addStage(prod, { + pre: [ + new ManualApprovalStep('PromoteToProd'), + ], +}); ``` -### Using CloudFormation Stack Outputs in ShellScriptAction +#### Using CloudFormation Stack Outputs in approvals Because many CloudFormation deployments result in the generation of resources with unpredictable names, validations have support for reading back CloudFormation Outputs after a deployment. This makes it possible to pass (for example) the generated URL of a load balancer to the test set. To use Stack Outputs, expose the `CfnOutput` object you're interested in, and -call `pipeline.stackOutput()` on it: +pass it to `envFromCfnOutputs` of the `ShellStep`: ```ts -class MyLbApplication extends Stage { +class MyApplicationStage extends Stage { public readonly loadBalancerAddress: CfnOutput; - - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - - const lbStack = new LoadBalancerStack(this, 'Stack'); - - // Or create this in `LoadBalancerStack` directly - this.loadBalancerAddress = new CfnOutput(lbStack, 'LbAddress', { - value: `https://${lbStack.loadBalancer.loadBalancerDnsName}/` - }); - } + // ... } -const lbApp = new MyLbApplication(this, 'MyApp', { - env: { /* ... */ } -}); -const stage = pipeline.addApplicationStage(lbApp); -stage.addActions(new ShellScriptAction({ - // ... - useOutputs: { - // When the test is executed, this will make $URL contain the - // load balancer address. - URL: pipeline.stackOutput(lbApp.loadBalancerAddress), - } +const lbApp = new MyApplicationStage(this, 'MyApp', { /* ... */ }); +pipeline.addStage(lbApp, { + post: [ + new ShellStep('HitEndpoint', { + envFromCfnOutputs: { + // Make the load balancer address available as $URL inside the commands + URL: lbApp.loadBalancerAddress, + }, + commands: ['curl -Ssf $URL'], + }); + ], }); ``` -### Using additional files in Shell Script Actions +#### Running scripts compiled during the synth step As part of a validation, you probably want to run a test suite that's more elaborate than what can be expressed in a couple of lines of shell script. You can bring additional files into the shell script validation by supplying -the `additionalArtifacts` property. +the `input` or `additionalInputs` property of `ShellStep`. The input can +be produced by the `Synth` step, or come from a source or any other build +step. -Here are some typical examples for how you might want to bring in additional -files from several sources: +Here's an example that captures an additional output directory in the synth +step and runs tests from there: -* Directory from the source repository -* Additional compiled artifacts from the synth step +```ts +const synth = new ShellStep('Synth', { /* ... */ }); +const pipeline = new CodePipeline(this, 'Pipeline', { synth }); -### Controlling IAM permissions +new ShellStep('Approve', { + // Use the contents of the 'integ' directory from the synth step as the input + input: synth.addOutputDirectory('integ'), + commands: ['cd integ && ./run.sh'], +}); +``` -IAM permissions can be added to the execution role of a `ShellScriptAction` in -two ways. +### Customizing CodeBuild Projects -Either pass additional policy statements in the `rolePolicyStatements` property: +CDK pipelines will generate CodeBuild projects for each `ShellStep` you use, and it +will also generate CodeBuild projects to publish assets and perform the self-mutation +of the pipeline. To control the various aspects of the CodeBuild projects that get +generated, use a `CodeBuildStep` instead of a `ShellStep`. This class has a number +of properties that allow you to customize various aspects of the projects: ```ts -new ShellScriptAction({ - // ... - rolePolicyStatements: [ - new iam.PolicyStatement({ - actions: ['s3:GetObject'], - resources: ['*'], - }), - ], -})); -``` +new CodeBuildStep('Synth', { + // ...standard RunScript props... + commands: [/* ... */], + env: { /* ... */ }, + + // If you are using a CodeBuildStep explicitly, set the 'cdk.out' directory + // to be the synth step's output. + primaryOutputDirectory: 'cdk.out', + + // Control the name of the project + projectName: 'MyProject', + + // Control parts of the BuildSpec other than the regular 'build' and 'install' commands + partialBuildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + // ... + }), -The Action can also be used as a Grantable after having been added to a Pipeline: + // Control the build environment + buildEnvironment: { + computeType: codebuild.ComputeType.LARGE, + }, -```ts -const action = new ShellScriptAction({ /* ... */ }); -pipeline.addStage('Test').addActions(action); + // Control Elastic Network Interface creation + vpc: vpc, + subnetSelection: { subnetType: ec2.SubnetType.PRIVATE }, + securityGroups: [mySecurityGroup], -bucket.grantRead(action); + // Additional policy statements for the execution role + rolePolicy: [ + new iam.PolicyStatement({ /* ... */ }), + ], +}); ``` -#### Additional files from the source repository - -Bringing in additional files from the source repository is appropriate if the -files in the source repository are directly usable in the test (for example, -if they are executable shell scripts themselves). Pass the `sourceArtifact`: +You can also configure defaults for *all* CodeBuild projects by passing `codeBuildDefaults`, +or just for the asset publishing and self-mutation projects by passing `assetPublishingCodeBuildDefaults` +or `selfMutationCodeBuildDefaults`: ```ts -const sourceArtifact = new codepipeline.Artifact(); - -const pipeline = new CdkPipeline(this, 'Pipeline', { +new CodePipeline(this, 'Pipeline', { // ... -}); -const validationAction = new ShellScriptAction({ - actionName: 'TestUsingSourceArtifact', - additionalArtifacts: [sourceArtifact], + // Defaults for all CodeBuild projects + codeBuildDefaults: { + // Prepend commands and configuration to all projects + partialBuildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + // ... + }), + + // Control the build environment + buildEnvironment: { + computeType: codebuild.ComputeType.LARGE, + }, - // 'test.sh' comes from the source repository - commands: ['./test.sh'], + // Control Elastic Network Interface creation + vpc: vpc, + subnetSelection: { subnetType: ec2.SubnetType.PRIVATE }, + securityGroups: [mySecurityGroup], + + // Additional policy statements for the execution role + rolePolicy: [ + new iam.PolicyStatement({ /* ... */ }), + ], + }, + + assetPublishingCodeBuildDefaults: { /* ... */ }, + selfMutationCodeBuildDefaults: { /* ... */ }, }); ``` -#### Additional files from the synth step +### Arbitrary CodePipeline actions -Getting the additional files from the synth step is appropriate if your -tests need the compilation step that is done as part of synthesis. +If you want to add a type of CodePipeline action to the CDK Pipeline that +doesn't have a matching class yet, you can define your own step class that extends +`Step` and implements `ICodePipelineActionFactory`. -On the synthesis step, specify `additionalArtifacts` to package -additional subdirectories into artifacts, and use the same artifact -in the `ShellScriptAction`'s `additionalArtifacts`: +Here's a simple example that adds a Jenkins step: ```ts -// If you are using additional output artifacts from the synth step, -// they must be named. -const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); -const integTestsArtifact = new codepipeline.Artifact('IntegTests'); +class MyJenkinsStep extends Step implements ICodePipelineActionFactory { + constructor(private readonly provider: codepipeline_actions.JenkinsProvider, private readonly input: FileSet) { + } -const pipeline = new CdkPipeline(this, 'Pipeline', { - synthAction: SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - buildCommands: ['npm run build'], - additionalArtifacts: [ - { - directory: 'test', - artifact: integTestsArtifact, - } - ], - }), - // ... -}); + public produceAction(stage: codepipeline.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { -const validationAction = new ShellScriptAction({ - actionName: 'TestUsingBuildArtifact', - additionalArtifacts: [integTestsArtifact], - // 'test.js' was produced from 'test/test.ts' during the synth step - commands: ['node ./test.js'], -}); + // This is where you control what type of Action gets added to the + // CodePipeline + stage.addAction(new codepipeline_actions.JenkinsAction({ + // Copy 'actionName' and 'runOrder' from the options + actionName: options.actionName, + runOrder: options.runOrder, + + // Jenkins-specific configuration + type: cpactions.JenkinsActionType.TEST, + jenkinsProvider: this.provider, + projectName: 'MyJenkinsProject', + + // Translate the FileSet into a codepipeline.Artifact + inputs: [options.artifacts.toCodePipeline(this.input)], + })); + + return { runOrdersConsumed: 1 }; + } +} ``` -#### Add Additional permissions to the CodeBuild Project Role for building and synthesizing +## Using Docker in the pipeline + +Docker can be used in 3 different places in the pipeline: + +* If you are using Docker image assets in your application stages: Docker will + run in the asset publishing projects. +* If you are using Docker image assets in your stack (for example as + images for your CodeBuild projects): Docker will run in the self-mutate project. +* If you are using Docker to bundle file assets anywhere in your project (for + example, if you are using such construct libraries as + `@aws-cdk/aws-lambda-nodejs`): Docker will run in the + *synth* project. -You can customize the role permissions used by the CodeBuild project so it has access to -the needed resources. eg: Adding CodeArtifact repo permissions so we pull npm packages -from the CA repo instead of NPM. +For the first case, you don't need to do anything special. For the other two cases, +you need to make sure that **privileged mode** is enabled on the correct CodeBuild +projects, so that Docker can run correctly. The follow sections describe how to do +that. + +You may also need to authenticate to Docker registries to avoid being throttled. +See the section **Authenticating to Docker registries** below for information on how to do +that. + +### Using Docker image assets in the pipeline + +If your `PipelineStack` is using Docker image assets (as opposed to the application +stacks the pipeline is deploying), for example by the use of `LinuxBuildImage.fromAsset()`, +you need to pass `dockerEnabledForSelfMutation: true` to the pipeline. For example: ```ts -class MyPipelineStack extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - ... - const pipeline = new CdkPipeline(this, 'Pipeline', { - ... - synthAction: SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - - // Use this to customize and a permissions required for the build - // and synth - rolePolicyStatements: [ - new PolicyStatement({ - actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], - resources: ['arn:codeartifact:repo:arn'], - }), - ], +const pipeline = new CodePipeline(this, 'Pipeline', { + // ... - // Then you can login to codeartifact repository - // and npm will now pull packages from your repository - // Note the codeartifact login command requires more params to work. - buildCommands: [ - 'aws codeartifact login --tool npm', - 'npm run build', - ], - }), - }); - } -} + // Turn this on because the pipeline uses Docker image assets + dockerEnabledForSelfMutation: true, +}); + +pipeline.addWave('MyWave', { + post: [ + new CodeBuildStep('RunApproval', { + commands: ['command-from-image'], + buildEnvironment: { + // The user of a Docker image asset in the pipeline requires turning on + // 'dockerEnabledForSelfMutation'. + buildImage: LinuxBuildImage.fromAsset(this, 'Image', { + directory: './docker-image', + }) + }, + }) + ], +}); ``` -### Developing the pipeline +> **Important**: You must turn on the `dockerEnabledForSelfMutation` flag, +> commit and allow the pipeline to self-update *before* adding the actual +> Docker asset. -The self-mutation feature of the `CdkPipeline` might at times get in the way -of the pipeline development workflow. Each change to the pipeline must be pushed -to git, otherwise, after the pipeline was updated using `cdk deploy`, it will -automatically revert to the state found in git. +### Using bundled file assets -To make the development more convenient, the self-mutation feature can be turned -off temporarily, by passing `selfMutating: false` property, example: +If you are using asset bundling anywhere (such as automatically done for you +if you add a construct like `@aws-cdk/aws-lambda-nodejs`), you need to pass +`dockerEnabledForSynth: true` to the pipeline. For example: ```ts -const pipeline = new CdkPipeline(this, 'Pipeline', { - selfMutating: false, - ... +const pipeline = new CodePipeline(this, 'Pipeline', { + // ... + + // Turn this on because the application uses bundled file assets + dockerEnabledForSynth: true, }); ``` -## Docker Registry Credentials +> **Important**: You must turn on the `dockerEnabledForSynth` flag, +> commit and allow the pipeline to self-update *before* adding the actual +> Docker asset. + +### Authenticating to Docker registries You can specify credentials to use for authenticating to Docker registries as part of the pipeline definition. This can be useful if any Docker image assets — in the pipeline or @@ -597,26 +686,27 @@ const customRegSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'CRSec const repo1 = ecr.Repository.fromRepositoryArn(stack, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo1'); const repo2 = ecr.Repository.fromRepositoryArn(stack, 'Repo', 'arn:aws:ecr:eu-west-1:0123456789012:repository/Repo2'); -const pipeline = new CdkPipeline(this, 'Pipeline', { +const pipeline = new CodePipeline(this, 'Pipeline', { dockerCredentials: [ DockerCredential.dockerHub(dockerHubSecret), DockerCredential.customRegistry('dockerregistry.example.com', customRegSecret), DockerCredential.ecr([repo1, repo2]); ], - ... + // ... }); ``` -You can authenticate to DockerHub, or any other Docker registry, by specifying a secret -with the username and secret/password to pass to `docker login`. The names of the fields -within the secret to use for the username and password can be customized. Authentication -to ECR repostories is done using the execution role of the relevant CodeBuild job. Both -types of credentials can be provided with an optional role to assume before requesting -the credentials. +For authenticating to Docker registries that require a username and password combination +(like DockerHub), create a Secrets Manager Secret with fields named `username` +and `secret`, and import it (the field names change be customized). -By default, the Docker credentials provided to the pipeline will be available to the -Synth/Build, Self-Update, and Asset Publishing actions within the pipeline. The scope of -the credentials can be limited via the `DockerCredentialUsage` option. +Authentication to ECR repostories is done using the execution role of the +relevant CodeBuild job. Both types of credentials can be provided with an +optional role to assume before requesting the credentials. + +By default, the Docker credentials provided to the pipeline will be available to +the **Synth**, **Self-Update**, and **Asset Publishing** actions within the +*pipeline. The scope of the credentials can be limited via the `DockerCredentialUsage` option. ```ts const dockerHubSecret = secretsmanager.Secret.fromSecretCompleteArn(this, 'DHSecret', 'arn:aws:...'); @@ -640,6 +730,11 @@ Before you can provision the pipeline, you have to *bootstrap* the environment y to create it in. If you are deploying your application to different environments, you also have to bootstrap those and be sure to add a *trust* relationship. +After you have bootstrapped an environment and created a pipeline that deploys +to it, it's important that you don't delete the stack or change its *Qualifier*, +or future deployments to this environment will fail. If you want to upgrade +the bootstrap stack to a newer version, do that by updating it in-place. + > This library requires a newer version of the bootstrapping stack which has > been updated specifically to support cross-account continuous delivery. In the future, > this new bootstrapping stack will become the default, but for now it is still @@ -821,6 +916,16 @@ leading NPM 6 reading that same file to not install all required packages anymor Make sure you are using the same NPM version everywhere, either downgrade your workstation's version or upgrade the CodeBuild version. +### Cannot find module '.../check-node-version.js' (MODULE_NOT_FOUND) + +The above error may be produced by `npx` when executing the CDK CLI, or any +project that uses the AWS SDK for JavaScript, without the target application +having been installed yet. For example, it can be triggered by `npx cdk synth` +if `aws-cdk` is not in your `package.json`. + +Work around this by either installing the target application using NPM *before* +running `npx`, or set the environment variable `NPM_CONFIG_UNSAFE_PERM=true`. + ### Cannot connect to the Docker daemon at unix:///var/run/docker.sock If, in the 'Synth' action (inside the 'Build' stage) of your pipeline, you get an error like this: @@ -858,6 +963,13 @@ update to the right state). ### S3 error: Access Denied +An "S3 Access Denied" error can have two causes: + +* Asset hashes have changed, but self-mutation has been disabled in the pipeline. +* You have deleted and recreated the bootstrap stack, or changed its qualifier. + +#### Self-mutation step has been removed + Some constructs, such as EKS clusters, generate nested stacks. When CloudFormation tries to deploy those stacks, it may fail with this error: @@ -876,7 +988,7 @@ const pipeline = new CdkPipeline(this, 'MyPipeline', { }); ``` -### Action Execution Denied +#### Bootstrap roles have been renamed or recreated While attempting to deploy an application stage, the "Prepare" or "Deploy" stage may fail with a cryptic error like: @@ -922,7 +1034,7 @@ new MyStack(this, 'MyStack', { * Re-deploy the pipeline to use the original qualifier. * Delete the temporary bootstrap stack(s) -#### Manual Alternative +##### Manual Alternative Alternatively, the errors can be resolved by finding each impacted resource and policy, and correcting the policies by replacing the canonical IDs (e.g., `AROAYBRETNYCYV6ZF2R93`) with the appropriate ARNs. As an example, the KMS @@ -942,13 +1054,6 @@ encryption key policy for the artifacts bucket may have a statement that looks l Any resource or policy that references the qualifier (`hnb659fds` by default) will need to be updated. -## Current Limitations - -Limitations that we are aware of and will address: - -* **No context queries**: context queries are not supported. That means that - Vpc.fromLookup() and other functions like it will not work [#8905](https://github.com/aws/aws-cdk/issues/8905). - ## Known Issues There are some usability issues that are caused by underlying technology, and diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/asset-type.ts b/packages/@aws-cdk/pipelines/lib/blueprint/asset-type.ts new file mode 100644 index 0000000000000..3fe015586a7c9 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/asset-type.ts @@ -0,0 +1,15 @@ +/** + * Type of the asset that is being published + */ +export enum AssetType { + /** + * A file + */ + FILE = 'file', + + /** + * A Docker image + */ + DOCKER_IMAGE = 'docker-image', +} + diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/file-set.ts b/packages/@aws-cdk/pipelines/lib/blueprint/file-set.ts new file mode 100644 index 0000000000000..8070bf30de2be --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/file-set.ts @@ -0,0 +1,66 @@ +import { Step } from './step'; + +/** + * A set of files traveling through the deployment pipeline + * + * Individual steps in the pipeline produce or consume + * `FileSet`s. + */ +export class FileSet implements IFileSetProducer { + /** + * The primary output of a file set producer + * + * The primary output of a FileSet is itself. + */ + public readonly primaryOutput?: FileSet = this; + private _producer?: Step; + + constructor( + /** Human-readable descriptor for this file set (does not need to be unique) */ + public readonly id: string, producer?: Step) { + this._producer = producer; + } + + /** + * The Step that produces this FileSet + */ + public get producer() { + if (!this._producer) { + throw new Error(`FileSet '${this.id}' doesn\'t have a producer; call 'fileSet.producedBy()'`); + } + return this._producer; + } + + /** + * Mark the given Step as the producer for this FileSet + * + * This method can only be called once. + */ + public producedBy(producer?: Step) { + if (this._producer) { + throw new Error(`FileSet '${this.id}' already has a producer (${this._producer}) while setting producer: ${producer}`); + } + this._producer = producer; + } + + /** + * Return a string representation of this FileSet + */ + public toString() { + return `FileSet(${this.id})`; + } +} + +/** + * Any class that produces, or is itself, a `FileSet` + * + * Steps implicitly produce a primary FileSet as an output. + */ +export interface IFileSetProducer { + /** + * The `FileSet` produced by this file set producer + * + * @default - This producer doesn't produce any file set + */ + readonly primaryOutput?: FileSet; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/index.ts b/packages/@aws-cdk/pipelines/lib/blueprint/index.ts new file mode 100644 index 0000000000000..d842ca1c7cd67 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/index.ts @@ -0,0 +1,8 @@ +export * from './asset-type'; +export * from './file-set'; +export * from './script-step'; +export * from './stack-deployment'; +export * from './stage-deployment'; +export * from './step'; +export * from './wave'; +export * from './manual-approval'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts b/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts new file mode 100644 index 0000000000000..859c279533fa3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts @@ -0,0 +1,37 @@ +import { Step } from './step'; + +/** + * Construction properties for a `ManualApprovalStep` + */ +export interface ManualApprovalStepProps { + /** + * The comment to display with this manual approval + * + * @default - No comment + */ + readonly comment?: string; +} + +/** + * A manual approval step + * + * If this step is added to a Pipeline, the Pipeline will + * be paused waiting for a human to resume it + * + * Only engines that support pausing the deployment will + * support this step type. + */ +export class ManualApprovalStep extends Step { + /** + * The comment associated with this manual approval + * + * @default - No comment + */ + public readonly comment?: string; + + constructor(id: string, props: ManualApprovalStepProps = {}) { + super(id); + + this.comment = props.comment; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts new file mode 100644 index 0000000000000..75c1883d92419 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts @@ -0,0 +1,275 @@ +import { CfnOutput, Stack } from '@aws-cdk/core'; +import { mapValues } from '../private/javascript'; +import { FileSet, IFileSetProducer } from './file-set'; +import { StackDeployment } from './stack-deployment'; +import { Step } from './step'; + +/** + * Construction properties for a `ShellStep`. + */ +export interface ShellStepProps { + /** + * Commands to run + */ + readonly commands: string[]; + + /** + * Installation commands to run before the regular commands + * + * For deployment engines that support it, install commands will be classified + * differently in the job history from the regular `commands`. + * + * @default - No installation commands + */ + readonly installCommands?: string[]; + + /** + * Environment variables to set + * + * @default - No environment variables + */ + readonly env?: Record; + + /** + * Set environment variables based on Stack Outputs + * + * `ShellStep`s following stack or stage deployments may + * access the `CfnOutput`s of those stacks to get access to + * --for example--automatically generated resource names or + * endpoint URLs. + * + * @default - No environment variables created from stack outputs + */ + readonly envFromCfnOutputs?: Record; + + /** + * FileSet to run these scripts on + * + * The files in the FileSet will be placed in the working directory when + * the script is executed. Use `additionalInputs` to download file sets + * to other directories as well. + * + * @default - No input specified + */ + readonly input?: IFileSetProducer; + + /** + * Additional FileSets to put in other directories + * + * Specifies a mapping from directory name to FileSets. During the + * script execution, the FileSets will be available in the directories + * indicated. + * + * The directory names may be relative. For example, you can put + * the main input and an additional input side-by-side with the + * following configuration: + * + * ```ts + * const script = new ShellStep('MainScript', { + * // ... + * input: MyEngineSource.gitHub('org/source1'), + * additionalInputs: { + * '../siblingdir': MyEngineSource.gitHub('org/source2'), + * } + * }); + * ``` + * + * @default - No additional inputs + */ + readonly additionalInputs?: Record; + + /** + * The directory that will contain the primary output fileset + * + * After running the script, the contents of the given directory + * will be treated as the primary output of this Step. + * + * @default - No primary output + */ + readonly primaryOutputDirectory?: string; + +} + +/** + * Run shell script commands in the pipeline + */ +export class ShellStep extends Step { + /** + * Commands to run + */ + public readonly commands: string[]; + + /** + * Installation commands to run before the regular commands + * + * For deployment engines that support it, install commands will be classified + * differently in the job history from the regular `commands`. + * + * @default - No installation commands + */ + public readonly installCommands: string[]; + + /** + * Environment variables to set + * + * @default - No environment variables + */ + public readonly env: Record; + + /** + * Set environment variables based on Stack Outputs + * + * @default - No environment variables created from stack outputs + */ + public readonly envFromCfnOutputs: Record; + + /** + * Input FileSets + * + * A list of `(FileSet, directory)` pairs, which are a copy of the + * input properties. This list should not be modified directly. + */ + public readonly inputs: FileSetLocation[] = []; + + /** + * Output FileSets + * + * A list of `(FileSet, directory)` pairs, which are a copy of the + * input properties. This list should not be modified directly. + */ + public readonly outputs: FileSetLocation[] = []; + + private readonly _additionalOutputs: Record = {}; + + private _primaryOutputDirectory?: string; + + constructor(id: string, props: ShellStepProps) { + super(id); + + this.commands = props.commands; + this.installCommands = props.installCommands ?? []; + this.env = props.env ?? {}; + this.envFromCfnOutputs = mapValues(props.envFromCfnOutputs ?? {}, StackOutputReference.fromCfnOutput); + + // Inputs + if (props.input) { + const fileSet = props.input.primaryOutput; + if (!fileSet) { + throw new Error(`'${id}': primary input should be a step that produces a file set, got ${props.input}`); + } + this.addDependencyFileSet(fileSet); + this.inputs.push({ directory: '.', fileSet }); + } + + for (const [directory, step] of Object.entries(props.additionalInputs ?? {})) { + if (directory === '.') { + throw new Error(`'${id}': input for directory '.' should be passed via 'input' property`); + } + + const fileSet = step.primaryOutput; + if (!fileSet) { + throw new Error(`'${id}': additionalInput for directory '${directory}' should be a step that produces a file set, got ${step}`); + } + this.addDependencyFileSet(fileSet); + this.inputs.push({ directory, fileSet }); + } + + // Outputs + + if (props.primaryOutputDirectory) { + this._primaryOutputDirectory = props.primaryOutputDirectory; + const fileSet = new FileSet('Output', this); + this.configurePrimaryOutput(fileSet); + this.outputs.push({ directory: props.primaryOutputDirectory, fileSet }); + } + } + + /** + * Configure the given output directory as primary output + * + * If no primary output has been configured yet, this directory + * will become the primary output of this ShellStep, otherwise this + * method will throw if the given directory is different than the + * currently configured primary output directory. + */ + public primaryOutputDirectory(directory: string): FileSet { + if (this._primaryOutputDirectory !== undefined) { + if (this._primaryOutputDirectory !== directory) { + throw new Error(`${this}: primaryOutputDirectory is '${this._primaryOutputDirectory}', cannot be changed to '${directory}'`); + } + + return this.primaryOutput!; + } + + this._primaryOutputDirectory = directory; + const fileSet = new FileSet('Output', this); + this.configurePrimaryOutput(fileSet); + this.outputs.push({ directory: directory, fileSet }); + return fileSet; + } + + /** + * Add an additional output FileSet based on a directory. + * + * + * After running the script, the contents of the given directory + * will be exported as a `FileSet`. Use the `FileSet` as the + * input to another step. + * + * Multiple calls with the exact same directory name string (not normalized) + * will return the same FileSet. + */ + public addOutputDirectory(directory: string): FileSet { + let fileSet = this._additionalOutputs[directory]; + if (!fileSet) { + fileSet = new FileSet(directory, this); + this._additionalOutputs[directory] = fileSet; + this.outputs.push({ directory, fileSet }); + } + return fileSet; + } +} + +/** + * Location of a FileSet consumed or produced by a ShellStep + */ +export interface FileSetLocation { + /** + * The (relative) directory where the FileSet is found + */ + readonly directory: string; + + /** + * The FileSet object + */ + readonly fileSet: FileSet; +} + +/** + * A Reference to a Stack Output + */ +export class StackOutputReference { + /** + * Create a StackOutputReference that references the given CfnOutput + */ + public static fromCfnOutput(output: CfnOutput) { + const stack = Stack.of(output); + return new StackOutputReference(stack.node.path, stack.artifactId, stack.resolve(output.logicalId)); + } + + private constructor( + /** A human-readable description of the producing stack */ + public readonly stackDescription: string, + /** Artifact id of the producing stack */ + private readonly stackArtifactId: string, + /** Output name of the producing stack */ + public readonly outputName: string) { + } + + /** + * Whether or not this stack output is being produced by the given Stack deployment + */ + public isProducedBy(stack: StackDeployment) { + return stack.stackArtifactId === this.stackArtifactId; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/stack-deployment.ts b/packages/@aws-cdk/pipelines/lib/blueprint/stack-deployment.ts new file mode 100644 index 0000000000000..2fe74ef15ccd3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/stack-deployment.ts @@ -0,0 +1,311 @@ +import * as path from 'path'; +import { parse as parseUrl } from 'url'; +import * as cxapi from '@aws-cdk/cx-api'; +import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; +import { isAssetManifest } from '../private/cloud-assembly-internals'; +import { AssetType } from './asset-type'; + +/** + * Properties for a `StackDeployment` + */ +export interface StackDeploymentProps { + /** + * Artifact ID for this stack + */ + readonly stackArtifactId: string; + + /** + * Construct path for this stack + */ + readonly constructPath: string; + + /** + * Name for this stack + */ + readonly stackName: string; + + /** + * Region where the stack should be deployed + * + * @default - Pipeline region + */ + readonly region?: string; + + /** + * Account where the stack should be deployed + * + * @default - Pipeline account + */ + readonly account?: string; + + /** + * Role to assume before deploying this stack + * + * @default - Don't assume any role + */ + readonly assumeRoleArn?: string; + + /** + * Execution role to pass to CloudFormation + * + * @default - No execution role + */ + readonly executionRoleArn?: string; + + /** + * Tags to apply to the stack + * + * @default - No tags + */ + readonly tags?: Record; + + /** + * Template path on disk to cloud assembly (cdk.out) + */ + readonly absoluteTemplatePath: string; + + /** + * Assets referenced by this stack + * + * @default - No assets + */ + readonly assets?: StackAsset[]; + + /** + * The S3 URL which points to the template asset location in the publishing + * bucket. + * + * @default - Stack template is not published + */ + readonly templateS3Uri?: string; +} + +/** + * Deployment of a single Stack + * + * You don't need to instantiate this class -- it will + * be automatically instantiated as necessary when you + * add a `Stage` to a pipeline. + */ +export class StackDeployment { + /** + * Build a `StackDeployment` from a Stack Artifact in a Cloud Assembly. + */ + public static fromArtifact(stackArtifact: cxapi.CloudFormationStackArtifact): StackDeployment { + const artRegion = stackArtifact.environment.region; + const region = artRegion === cxapi.UNKNOWN_REGION ? undefined : artRegion; + const artAccount = stackArtifact.environment.account; + const account = artAccount === cxapi.UNKNOWN_ACCOUNT ? undefined : artAccount; + + return new StackDeployment({ + account, + region, + tags: stackArtifact.tags, + stackArtifactId: stackArtifact.id, + constructPath: stackArtifact.hierarchicalId, + stackName: stackArtifact.stackName, + absoluteTemplatePath: path.join(stackArtifact.assembly.directory, stackArtifact.templateFile), + assumeRoleArn: stackArtifact.assumeRoleArn, + executionRoleArn: stackArtifact.cloudFormationExecutionRoleArn, + assets: extractStackAssets(stackArtifact), + templateS3Uri: stackArtifact.stackTemplateAssetObjectUrl, + }); + } + + /** + * Artifact ID for this stack + */ + public readonly stackArtifactId: string; + + /** + * Construct path for this stack + */ + public readonly constructPath: string; + + /** + * Name for this stack + */ + public readonly stackName: string; + + /** + * Region where the stack should be deployed + * + * @default - Pipeline region + */ + public readonly region?: string; + + /** + * Account where the stack should be deployed + * + * @default - Pipeline account + */ + public readonly account?: string; + + /** + * Role to assume before deploying this stack + * + * @default - Don't assume any role + */ + public readonly assumeRoleArn?: string; + + /** + * Execution role to pass to CloudFormation + * + * @default - No execution role + */ + public readonly executionRoleArn?: string; + + /** + * Tags to apply to the stack + */ + public readonly tags: Record; + + /** + * Assets referenced by this stack + */ + public readonly assets: StackAsset[]; + + /** + * Other stacks this stack depends on + */ + public readonly stackDependencies: StackDeployment[] = []; + + /** + * The asset that represents the CloudFormation template for this stack. + */ + public readonly templateAsset?: StackAsset; + + /** + * The S3 URL which points to the template asset location in the publishing + * bucket. + * + * This is `undefined` if the stack template is not published. Use the + * `DefaultStackSynthesizer` to ensure it is. + * + * @example https://bucket.s3.amazonaws.com/object/key + */ + public readonly templateUrl?: string; + + /** + * Template path on disk to CloudAssembly + */ + public readonly absoluteTemplatePath: string; + + private constructor(props: StackDeploymentProps) { + this.stackArtifactId = props.stackArtifactId; + this.constructPath = props.constructPath; + this.account = props.account; + this.region = props.region; + this.tags = props.tags ?? {}; + this.assumeRoleArn = props.assumeRoleArn; + this.executionRoleArn = props.executionRoleArn; + this.stackName = props.stackName; + this.absoluteTemplatePath = props.absoluteTemplatePath; + this.templateUrl = props.templateS3Uri ? s3UrlFromUri(props.templateS3Uri, props.region) : undefined; + + this.assets = new Array(); + + for (const asset of props.assets ?? []) { + if (asset.isTemplate) { + this.templateAsset = asset; + } else { + this.assets.push(asset); + } + } + } + + /** + * Add a dependency on another stack + */ + public addStackDependency(stackDeployment: StackDeployment) { + this.stackDependencies.push(stackDeployment); + } +} + +/** + * An asset used by a Stack + */ +export interface StackAsset { + /** + * Absolute asset manifest path + * + * This needs to be made relative at a later point in time, but when this + * information is parsed we don't know about the root cloud assembly yet. + */ + readonly assetManifestPath: string; + + /** + * Asset identifier + */ + readonly assetId: string; + + /** + * Asset selector to pass to `cdk-assets`. + */ + readonly assetSelector: string; + + /** + * Type of asset to publish + */ + readonly assetType: AssetType; + + /** + * Role ARN to assume to publish + * + * @default - No need to assume any role + */ + readonly assetPublishingRoleArn?: string; + + /** + * Does this asset represent the CloudFormation template for the stack + * + * @default false + */ + readonly isTemplate: boolean; +} + +function extractStackAssets(stackArtifact: cxapi.CloudFormationStackArtifact): StackAsset[] { + const ret = new Array(); + + const assetManifests = stackArtifact.dependencies.filter(isAssetManifest); + for (const manifestArtifact of assetManifests) { + const manifest = AssetManifestReader.fromFile(manifestArtifact.file); + + for (const entry of manifest.entries) { + let assetType: AssetType; + let isTemplate = false; + + if (entry instanceof DockerImageManifestEntry) { + assetType = AssetType.DOCKER_IMAGE; + } else if (entry instanceof FileManifestEntry) { + isTemplate = entry.source.packaging === 'file' && entry.source.path === stackArtifact.templateFile; + assetType = AssetType.FILE; + } else { + throw new Error(`Unrecognized asset type: ${entry.type}`); + } + + ret.push({ + assetManifestPath: manifestArtifact.file, + assetId: entry.id.assetId, + assetSelector: entry.id.toString(), + assetType, + assetPublishingRoleArn: entry.destination.assumeRoleArn, + isTemplate, + }); + } + } + + return ret; +} + +/** + * Takes an s3://bucket/object-key uri and returns a region-aware https:// url for it + * + * @param uri The s3 URI + * @param region The region (if undefined, we will return the global endpoint) + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access + */ +function s3UrlFromUri(uri: string, region: string | undefined) { + const url = parseUrl(uri); + return `https://${url.hostname}.s3.${region ? `${region}.` : ''}amazonaws.com${url.path}`; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts b/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts new file mode 100644 index 0000000000000..499d324dfb25f --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts @@ -0,0 +1,118 @@ +import * as cdk from '@aws-cdk/core'; +import { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; +import { isStackArtifact } from '../private/cloud-assembly-internals'; +import { StackDeployment } from './stack-deployment'; +import { Step } from './step'; + +/** + * Properties for a `StageDeployment` + */ +export interface StageDeploymentProps { + /** + * Stage name to use in the pipeline + * + * @default - Use Stage's construct ID + */ + readonly stageName?: string; + + /** + * Additional steps to run before any of the stacks in the stage + * + * @default - No additional steps + */ + readonly pre?: Step[]; + + /** + * Additional steps to run after all of the stacks in the stage + * + * @default - No additional steps + */ + readonly post?: Step[]; +} + +/** + * Deployment of a single `Stage` + * + * A `Stage` consists of one or more `Stacks`, which will be + * deployed in dependency order. + */ +export class StageDeployment { + /** + * Create a new `StageDeployment` from a `Stage` + * + * Synthesizes the target stage, and deployes the stacks found inside + * in dependency order. + */ + public static fromStage(stage: cdk.Stage, props: StageDeploymentProps = {}) { + const assembly = stage.synth(); + if (assembly.stacks.length === 0) { + // If we don't check here, a more puzzling "stage contains no actions" + // error will be thrown come deployment time. + throw new Error(`The given Stage construct ('${stage.node.path}') should contain at least one Stack`); + } + + const stepFromArtifact = new Map(); + for (const artifact of assembly.stacks) { + const step = StackDeployment.fromArtifact(artifact); + stepFromArtifact.set(artifact, step); + } + + for (const artifact of assembly.stacks) { + const thisStep = stepFromArtifact.get(artifact); + if (!thisStep) { + throw new Error('Logic error: we just added a step for this artifact but it disappeared.'); + } + + const stackDependencies = artifact.dependencies.filter(isStackArtifact); + for (const dep of stackDependencies) { + const depStep = stepFromArtifact.get(dep); + if (!depStep) { + throw new Error(`Stack '${artifact.id}' depends on stack not found in same Stage: '${dep.id}'`); + } + thisStep.addStackDependency(depStep); + } + } + + return new StageDeployment(Array.from(stepFromArtifact.values()), { + stageName: stage.stageName, + ...props, + }); + } + + /** + * The display name of this stage + */ + public readonly stageName: string; + + /** + * Additional steps that are run before any of the stacks in the stage + */ + public readonly pre: Step[]; + + /** + * Additional steps that are run after all of the stacks in the stage + */ + public readonly post: Step[]; + + private constructor( + /** The stacks deployed in this stage */ + public readonly stacks: StackDeployment[], props: StageDeploymentProps = {}) { + this.stageName = props.stageName ?? ''; + this.pre = props.pre ?? []; + this.post = props.post ?? []; + } + + /** + * Add an additional step to run before any of the stacks in this stage + */ + public addPre(...steps: Step[]) { + this.pre.push(...steps); + } + + /** + * Add an additional step to run after all of the stacks in this stage + */ + public addPost(...steps: Step[]) { + this.post.push(...steps); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts new file mode 100644 index 0000000000000..e252765efd04e --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts @@ -0,0 +1,72 @@ +import { FileSet, IFileSetProducer } from './file-set'; + +/** + * A generic Step which can be added to a Pipeline + * + * Steps can be used to add Sources, Build Actions and Validations + * to your pipeline. + * + * This class is abstract. See specific subclasses of Step for + * useful steps to add to your Pipeline + */ +export abstract class Step implements IFileSetProducer { + /** + * The list of FileSets consumed by this Step + */ + public readonly dependencyFileSets: FileSet[] = []; + + /** + * Whether or not this is a Source step + * + * What it means to be a Source step depends on the engine. + */ + public readonly isSource: boolean = false; + + private _primaryOutput?: FileSet; + + constructor( + /** Identifier for this step */ + public readonly id: string) { + } + + /** + * Return the steps this step depends on, based on the FileSets it requires + */ + public get dependencies(): Step[] { + return this.dependencyFileSets.map(f => f.producer); + } + + /** + * Return a string representation of this Step + */ + public toString() { + return `${this.constructor.name}(${this.id})`; + } + + /** + * The primary FileSet produced by this Step + * + * Not all steps produce an output FileSet--if they do + * you can substitute the `Step` object for the `FileSet` object. + */ + public get primaryOutput(): FileSet | undefined { + // Accessor so it can be mutable in children + return this._primaryOutput; + } + + /** + * Add an additional FileSet to the set of file sets required by this step + * + * This will lead to a dependency on the producer of that file set. + */ + protected addDependencyFileSet(fs: FileSet) { + this.dependencyFileSets.push(fs); + } + + /** + * Configure the given FileSet as the primary output of this step + */ + protected configurePrimaryOutput(fs: FileSet) { + this._primaryOutput = fs; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/wave.ts b/packages/@aws-cdk/pipelines/lib/blueprint/wave.ts new file mode 100644 index 0000000000000..709d43a1ed8bd --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/blueprint/wave.ts @@ -0,0 +1,113 @@ +import * as cdk from '@aws-cdk/core'; +import { StageDeployment } from './stage-deployment'; +import { Step } from './step'; + +/** + * Construction properties for a `Wave` + */ +export interface WaveProps { + /** + * Additional steps to run before any of the stages in the wave + * + * @default - No additional steps + */ + readonly pre?: Step[]; + + /** + * Additional steps to run after all of the stages in the wave + * + * @default - No additional steps + */ + readonly post?: Step[]; +} + +/** + * Multiple stages that are deployed in parallel + */ +export class Wave { + /** + * Additional steps that are run before any of the stages in the wave + */ + public readonly pre: Step[]; + + /** + * Additional steps that are run after all of the stages in the wave + */ + public readonly post: Step[]; + + /** + * The stages that are deployed in this wave + */ + public readonly stages: StageDeployment[] = []; + + constructor( + /** Identifier for this Wave */ + public readonly id: string, props: WaveProps = {}) { + this.pre = props.pre ?? []; + this.post = props.post ?? []; + } + + /** + * Add a Stage to this wave + * + * It will be deployed in parallel with all other stages in this + * wave. + */ + public addStage(stage: cdk.Stage, options: AddStageOpts = {}) { + const ret = StageDeployment.fromStage(stage, options); + this.stages.push(ret); + return ret; + } + + /** + * Add an additional step to run before any of the stages in this wave + */ + public addPre(...steps: Step[]) { + this.pre.push(...steps); + } + + /** + * Add an additional step to run after all of the stages in this wave + */ + public addPost(...steps: Step[]) { + this.post.push(...steps); + } +} + +/** + * Options to pass to `addStage` + */ +export interface AddStageOpts { + /** + * Additional steps to run before any of the stacks in the stage + * + * @default - No additional steps + */ + readonly pre?: Step[]; + + /** + * Additional steps to run after all of the stacks in the stage + * + * @default - No additional steps + */ + readonly post?: Step[]; +} + +/** + * Options to pass to `addWave` + */ +export interface WaveOptions { + /** + * Additional steps to run before any of the stages in the wave + * + * @default - No additional steps + */ + readonly pre?: Step[]; + + /** + * Additional steps to run after all of the stages in the wave + * + * @default - No additional steps + */ + readonly post?: Step[]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts new file mode 100644 index 0000000000000..f814e8b8fe272 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts @@ -0,0 +1,502 @@ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { IDependable, Stack } from '@aws-cdk/core'; +import { Construct, Node } from 'constructs'; +import { FileSetLocation, ShellStep, StackDeployment, StackOutputReference } from '../blueprint'; +import { PipelineQueries } from '../helpers-internal/pipeline-queries'; +import { cloudAssemblyBuildSpecDir, obtainScope } from '../private/construct-internals'; +import { mapValues, mkdict, noEmptyObject, noUndefined, partition } from '../private/javascript'; +import { ArtifactMap } from './artifact-map'; +import { CodeBuildStep } from './codebuild-step'; +import { CodeBuildOptions } from './codepipeline'; +import { ICodePipelineActionFactory, ProduceActionOptions, CodePipelineActionFactoryResult } from './codepipeline-action-factory'; + +export interface CodeBuildFactoryProps { + /** + * Name for the generated CodeBuild project + * + * @default - Automatically generated + */ + readonly projectName?: string; + + /** + * Customization options for the project + * + * Will at CodeBuild production time be combined with the option + * defaults configured on the pipeline. + * + * @default - No special values + */ + readonly projectOptions?: CodeBuildOptions; + + /** + * Custom execution role to be used for the CodeBuild project + * + * @default - A role is automatically created + */ + readonly role?: iam.IRole; + + /** + * If true, the build spec will be passed via the Cloud Assembly instead of rendered onto the Project + * + * Doing this has two advantages: + * + * - Bypass size restrictions: the buildspec on the project is restricted + * in size, while buildspecs coming from an input artifact are not restricted + * in such a way. + * - Bypass pipeline update: if the SelfUpdate step has to change the buildspec, + * that just takes time. On the other hand, if the buildspec comes from the + * pipeline artifact, no such update has to take place. + * + * @default false + */ + readonly passBuildSpecViaCloudAssembly?: boolean; + + /** + * Override the construct tree where the CodeBuild project is created. + * + * Normally, the construct tree will look like this: + * + * ── Pipeline + * └── 'MyStage' <- options.scope + * └── 'MyAction' <- this is the CodeBuild project + * + * If this flag is set, the construct tree will look like this: + * + * ── Pipeline + * └── 'MyStage' <- options.scope + * └── 'MyAction' <- just a scope + * └── 'BackwardsCompatName' <- CodeBuild project + * + * This is to maintain logicalID compatibility with the previous iteration + * of pipelines (where the Action was a construct that would create the Project). + * + * @default true + */ + readonly additionalConstructLevel?: boolean; + + /** + * Additional dependency that the CodeBuild project should take + * + * @default - + */ + readonly additionalDependable?: IDependable; + + readonly inputs?: FileSetLocation[]; + readonly outputs?: FileSetLocation[]; + + readonly stepId?: string; + + readonly commands: string[]; + readonly installCommands?: string[]; + + readonly env?: Record; + readonly envFromCfnOutputs?: Record; + + /** + * If given, override the scope from the produce call with this scope. + */ + readonly scope?: Construct; + + /** + * Whether or not the given CodeBuild project is going to be the synth step + * + * @default false + */ + readonly isSynth?: boolean; +} + +/** + * Produce a CodeBuild project from a RunScript step and some CodeBuild-specific customizations + * + * The functionality here is shared between the `CodePipeline` translating a `ShellStep` into + * a CodeBuild project, as well as the `CodeBuildStep` straight up. + */ +export class CodeBuildFactory implements ICodePipelineActionFactory { + // eslint-disable-next-line max-len + public static fromShellStep(constructId: string, scriptStep: ShellStep, additional?: Partial): ICodePipelineActionFactory { + return new CodeBuildFactory(constructId, { + commands: scriptStep.commands, + env: scriptStep.env, + envFromCfnOutputs: scriptStep.envFromCfnOutputs, + inputs: scriptStep.inputs, + outputs: scriptStep.outputs, + stepId: scriptStep.id, + installCommands: scriptStep.installCommands, + ...additional, + }); + } + + public static fromCodeBuildStep(constructId: string, step: CodeBuildStep, additional?: Partial): ICodePipelineActionFactory { + const factory = CodeBuildFactory.fromShellStep(constructId, step, { + projectName: step.projectName, + role: step.role, + projectOptions: { + buildEnvironment: step.buildEnvironment, + rolePolicy: step.rolePolicyStatements, + securityGroups: step.securityGroups, + partialBuildSpec: step.partialBuildSpec, + vpc: step.vpc, + subnetSelection: step.subnetSelection, + ...additional?.projectOptions, + }, + ...additional, + }); + + return { + produceAction: (stage, options) => { + const result = factory.produceAction(stage, options); + if (result.project) { + step._setProject(result.project); + } + return result; + }, + }; + } + + private _project?: codebuild.IProject; + private stepId: string; + + private constructor( + private readonly constructId: string, + private readonly props: CodeBuildFactoryProps) { + + this.stepId = props.stepId ?? constructId; + } + + public get project(): codebuild.IProject { + if (!this._project) { + throw new Error('Project becomes available after produce() has been called'); + } + return this._project; + } + + public produceAction(stage: codepipeline.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { + const projectOptions = mergeCodeBuildOptions(options.codeBuildDefaults, this.props.projectOptions); + + const inputs = this.props.inputs ?? []; + const outputs = this.props.outputs ?? []; + + const mainInput = inputs.find(x => x.directory === '.'); + const extraInputs = inputs.filter(x => x.directory !== '.'); + + const inputArtifact = mainInput + ? options.artifacts.toCodePipeline(mainInput.fileSet) + : options.fallbackArtifact; + const extraInputArtifacts = extraInputs.map(x => options.artifacts.toCodePipeline(x.fileSet)); + const outputArtifacts = outputs.map(x => options.artifacts.toCodePipeline(x.fileSet)); + + if (!inputArtifact) { + // This should actually never happen because CodeBuild projects shouldn't be added before the + // Source, which always produces at least an artifact. + throw new Error(`CodeBuild action '${this.stepId}' requires an input (and the pipeline doesn't have a Source to fall back to). Add an input or a pipeline source.`); + } + + const installCommands = [ + ...generateInputArtifactLinkCommands(options.artifacts, extraInputs), + ...this.props.installCommands ?? [], + ]; + + const buildSpecHere = codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + install: (installCommands.length ?? 0) > 0 ? { commands: installCommands } : undefined, + build: this.props.commands.length > 0 ? { commands: this.props.commands } : undefined, + }, + artifacts: noEmptyObject(renderArtifactsBuildSpec(options.artifacts, this.props.outputs ?? [])), + }); + + // Partition environment variables into environment variables that can go on the project + // and environment variables that MUST go in the pipeline (those that reference CodePipeline variables) + const env = noUndefined(this.props.env ?? {}); + + const [actionEnvs, projectEnvs] = partition(Object.entries(env ?? {}), ([, v]) => containsPipelineVariable(v)); + + const environment = mergeBuildEnvironments( + projectOptions?.buildEnvironment ?? {}, + { + environmentVariables: noEmptyObject(mapValues(mkdict(projectEnvs), value => ({ value }))), + }); + + const fullBuildSpec = options.codeBuildDefaults?.partialBuildSpec + ? codebuild.mergeBuildSpecs(options.codeBuildDefaults?.partialBuildSpec, buildSpecHere) + : buildSpecHere; + + const osFromEnvironment = environment.buildImage && environment.buildImage instanceof codebuild.WindowsBuildImage + ? ec2.OperatingSystemType.WINDOWS + : ec2.OperatingSystemType.LINUX; + + const actualBuildSpec = filterBuildSpecCommands(fullBuildSpec, osFromEnvironment); + + const scope = this.props.scope ?? options.scope; + + let projectBuildSpec; + if (this.props.passBuildSpecViaCloudAssembly) { + // Write to disk and replace with a reference + const relativeSpecFile = `buildspec-${Node.of(scope).addr}-${this.constructId}.yaml`; + const absSpecFile = path.join(cloudAssemblyBuildSpecDir(scope), relativeSpecFile); + fs.writeFileSync(absSpecFile, Stack.of(scope).resolve(actualBuildSpec.toBuildSpec()), { encoding: 'utf-8' }); + projectBuildSpec = codebuild.BuildSpec.fromSourceFilename(relativeSpecFile); + } else { + projectBuildSpec = actualBuildSpec; + } + + // A hash over the values that make the CodeBuild Project unique (and necessary + // to restart the pipeline if one of them changes). projectName is not necessary to include + // here because the pipeline will definitely restart if projectName changes. + // (Resolve tokens) + const projectConfigHash = hash(Stack.of(scope).resolve({ + environment: serializeBuildEnvironment(environment), + buildSpecString: actualBuildSpec.toBuildSpec(), + })); + + const actionName = options.actionName ?? this.stepId; + + let projectScope = scope; + if (this.props.additionalConstructLevel ?? true) { + projectScope = obtainScope(scope, actionName); + } + + const project = new codebuild.PipelineProject(projectScope, this.constructId, { + projectName: this.props.projectName, + environment, + vpc: projectOptions.vpc, + subnetSelection: projectOptions.subnetSelection, + securityGroups: projectOptions.securityGroups, + buildSpec: projectBuildSpec, + role: this.props.role, + }); + + if (this.props.additionalDependable) { + project.node.addDependency(this.props.additionalDependable); + } + + if (projectOptions.rolePolicy !== undefined) { + projectOptions.rolePolicy.forEach(policyStatement => { + project.addToRolePolicy(policyStatement); + }); + } + + const queries = new PipelineQueries(options.pipeline); + + const stackOutputEnv = mapValues(this.props.envFromCfnOutputs ?? {}, outputRef => + `#{${stackVariableNamespace(queries.producingStack(outputRef))}.${outputRef.outputName}}`, + ); + + const configHashEnv = options.beforeSelfMutation + ? { _PROJECT_CONFIG_HASH: projectConfigHash } + : {}; + + stage.addAction(new codepipeline_actions.CodeBuildAction({ + actionName: actionName, + input: inputArtifact, + extraInputs: extraInputArtifacts, + outputs: outputArtifacts, + project, + runOrder: options.runOrder, + + // Inclusion of the hash here will lead to the pipeline structure for any changes + // made the config of the underlying CodeBuild Project. + // Hence, the pipeline will be restarted. This is necessary if the users + // adds (for example) build or test commands to the buildspec. + environmentVariables: noEmptyObject(cbEnv({ + ...mkdict(actionEnvs), + ...configHashEnv, + ...stackOutputEnv, + })), + })); + + this._project = project; + + return { runOrdersConsumed: 1, project }; + } +} + +/** + * Generate commands to move additional input artifacts into the right place + */ +function generateInputArtifactLinkCommands(artifacts: ArtifactMap, inputs: FileSetLocation[]): string[] { + return inputs.map(input => { + const fragments = []; + + if (!['.', '..'].includes(path.dirname(input.directory))) { + fragments.push(`mkdir -p "${input.directory}"`); + } + + const artifact = artifacts.toCodePipeline(input.fileSet); + + fragments.push(`ln -s "$CODEBUILD_SRC_DIR_${artifact.artifactName}" "${input.directory}"`); + + return fragments.join(' && '); + }); +} + +function renderArtifactsBuildSpec(artifactMap: ArtifactMap, outputs: FileSetLocation[]) { + // save the generated files in the output artifact + // This part of the buildspec has to look completely different depending on whether we're + // using secondary artifacts or not. + if (outputs.length === 0) { return {}; } + + if (outputs.length === 1) { + return { + 'base-directory': outputs[0].directory, + 'files': '**/*', + }; + } + + const secondary: Record = {}; + for (const output of outputs) { + const art = artifactMap.toCodePipeline(output.fileSet); + + if (!art.artifactName) { + throw new Error('You must give the output artifact a name'); + } + secondary[art.artifactName] = { + 'base-directory': output.directory, + 'files': '**/*', + }; + } + + return { 'secondary-artifacts': secondary }; +} + +export function mergeCodeBuildOptions(...opts: Array) { + const xs = [{}, ...opts.filter(isDefined)]; + while (xs.length > 1) { + const [a, b] = xs.splice(xs.length - 2, 2); + xs.push(merge2(a, b)); + } + return xs[0]; + + function merge2(a: CodeBuildOptions, b: CodeBuildOptions): CodeBuildOptions { + return { + buildEnvironment: mergeBuildEnvironments(a.buildEnvironment, b.buildEnvironment), + rolePolicy: definedArray([...a.rolePolicy ?? [], ...b.rolePolicy ?? []]), + securityGroups: definedArray([...a.securityGroups ?? [], ...b.securityGroups ?? []]), + partialBuildSpec: mergeBuildSpecs(a.partialBuildSpec, b.partialBuildSpec), + vpc: b.vpc ?? a.vpc, + subnetSelection: b.subnetSelection ?? a.subnetSelection, + }; + } +} + +function mergeBuildEnvironments(a: codebuild.BuildEnvironment, b?: codebuild.BuildEnvironment): codebuild.BuildEnvironment; +function mergeBuildEnvironments(a: codebuild.BuildEnvironment | undefined, b: codebuild.BuildEnvironment): codebuild.BuildEnvironment; +function mergeBuildEnvironments(a?: codebuild.BuildEnvironment, b?: codebuild.BuildEnvironment): codebuild.BuildEnvironment | undefined; +function mergeBuildEnvironments(a?: codebuild.BuildEnvironment, b?: codebuild.BuildEnvironment) { + if (!a || !b) { return a ?? b; } + + return { + buildImage: b.buildImage ?? a.buildImage, + computeType: b.computeType ?? a.computeType, + environmentVariables: { + ...a.environmentVariables, + ...b.environmentVariables, + }, + privileged: b.privileged ?? a.privileged, + }; +} + +export function mergeBuildSpecs(a: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec; +export function mergeBuildSpecs(a: codebuild.BuildSpec | undefined, b: codebuild.BuildSpec): codebuild.BuildSpec; +export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec | undefined; +export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec) { + if (!a || !b) { return a ?? b; } + return codebuild.mergeBuildSpecs(a, b); +} + +function isDefined(x: A | undefined): x is NonNullable { + return x !== undefined; +} + +function hash(obj: A) { + const d = crypto.createHash('sha256'); + d.update(JSON.stringify(obj)); + return d.digest('hex'); +} + +/** + * Serialize a build environment to data (get rid of constructs & objects), so we can JSON.stringify it + */ +function serializeBuildEnvironment(env: codebuild.BuildEnvironment) { + return { + privileged: env.privileged, + environmentVariables: env.environmentVariables, + type: env.buildImage?.type, + imageId: env.buildImage?.imageId, + computeType: env.computeType, + imagePullPrincipalType: env.buildImage?.imagePullPrincipalType, + secretsManagerArn: env.buildImage?.secretsManagerCredentials?.secretArn, + }; +} + +export function stackVariableNamespace(stack: StackDeployment) { + return stack.stackArtifactId; +} + +/** + * Whether the given string contains a reference to a CodePipeline variable + */ +function containsPipelineVariable(s: string) { + return !!s.match(/#\{[^}]+\}/); +} + +/** + * Turn a collection into a collection of CodePipeline environment variables + */ +function cbEnv(xs: Record): Record { + return mkdict(Object.entries(xs) + .filter(([, v]) => v !== undefined) + .map(([k, v]) => [k, { value: v }] as const)); +} + +function definedArray(xs: A[]): A[] | undefined { + return xs.length > 0 ? xs : undefined; +} + +/** + * If lines in the buildspec start with '!WINDOWS!' or '!LINUX!', only render them on that platform. + * + * Very private protocol for now, but may come in handy in other libraries as well. + */ +function filterBuildSpecCommands(buildSpec: codebuild.BuildSpec, osType: ec2.OperatingSystemType) { + if (!buildSpec.isImmediate) { return buildSpec; } + const spec = (buildSpec as any).spec; + + const winTag = '!WINDOWS!'; + const linuxTag = '!LINUX!'; + const expectedTag = osType === ec2.OperatingSystemType.WINDOWS ? winTag : linuxTag; + + return codebuild.BuildSpec.fromObject(recurse(spec)); + + function recurse(x: any): any { + if (Array.isArray(x)) { + const ret: any[] = []; + for (const el of x) { + const [tag, payload] = extractTag(el); + if (tag === undefined || tag === expectedTag) { + ret.push(payload); + } + } + return ret; + } + if (x && typeof x === 'object') { + return mapValues(x, recurse); + } + return x; + } + + function extractTag(x: any): [string | undefined, any] { + if (typeof x !== 'string') { return [undefined, x]; } + for (const tag of [winTag, linuxTag]) { + if (x.startsWith(tag)) { return [tag, x.substr(tag.length)]; } + } + return [undefined, x]; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/artifact-map.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/artifact-map.ts new file mode 100644 index 0000000000000..4ec4b086815c7 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/artifact-map.ts @@ -0,0 +1,71 @@ +import * as cp from '@aws-cdk/aws-codepipeline'; +import { FileSet } from '../blueprint'; +import { PipelineGraph } from '../helpers-internal'; + +/** + * Translate FileSets to CodePipeline Artifacts + */ +export class ArtifactMap { + private artifacts = new Map(); + private usedNames = new Set(); + + /** + * Return the matching CodePipeline artifact for a FileSet + */ + public toCodePipeline(x: FileSet): cp.Artifact { + if (x instanceof CodePipelineFileSet) { + return x._artifact; + } + + let ret = this.artifacts.get(x); + if (!ret) { + // They all need a name + const artifactName = this.makeUniqueName(`${x.producer.id}.${x.id}`); + this.usedNames.add(artifactName); + this.artifacts.set(x, ret = new cp.Artifact(artifactName)); + } + return ret; + } + + private makeUniqueName(baseName: string) { + let i = 1; + baseName = sanitizeArtifactName(baseName); + let name = baseName; + while (this.usedNames.has(name)) { + name = `${baseName}${++i}`; + } + return name; + } +} + +function sanitizeArtifactName(x: string): string { + // FIXME: Does this REALLY not allow '.'? The docs don't mention it, but action names etc. do! + return x.replace(/[^A-Za-z0-9@\-_]/g, '_'); +} + +/** + * A FileSet created from a CodePipeline artifact + * + * You only need to use this if you want to add CDK Pipeline stages + * add the end of an existing CodePipeline, which should be very rare. + */ +export class CodePipelineFileSet extends FileSet { + /** + * Turn a CodePipeline Artifact into a FileSet + */ + public static fromArtifact(artifact: cp.Artifact) { + return new CodePipelineFileSet(artifact); + } + + /** + * The artifact this class is wrapping + * + * @internal + */ + public readonly _artifact: cp.Artifact; + + private constructor(artifact: cp.Artifact) { + super(artifact.artifactName ?? 'Imported', PipelineGraph.NO_STEP); + this._artifact = artifact; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts new file mode 100644 index 0000000000000..064c283ef2f12 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts @@ -0,0 +1,189 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { ShellStep, ShellStepProps } from '../blueprint'; + +/** + * Construction props for SimpleSynthAction + */ +export interface CodeBuildStepProps extends ShellStepProps { + /** + * Name for the generated CodeBuild project + * + * @default - Automatically generated + */ + readonly projectName?: string; + + /** + * Additional configuration that can only be configured via BuildSpec + * + * You should not use this to specify output artifacts; those + * should be supplied via the other properties of this class, otherwise + * CDK Pipelines won't be able to inspect the artifacts. + * + * Set the `commands` to an empty array if you want to fully specify + * the BuildSpec using this field. + * + * The BuildSpec must be available inline--it cannot reference a file + * on disk. + * + * @default - BuildSpec completely derived from other properties + */ + readonly partialBuildSpec?: codebuild.BuildSpec; + + /** + * The VPC where to execute the SimpleSynth. + * + * @default - No VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * Only used if 'vpc' is supplied. + * + * @default - All private subnets. + */ + readonly subnetSelection?: ec2.SubnetSelection; + + /** + * Policy statements to add to role used during the synth + * + * Can be used to add acces to a CodeArtifact repository etc. + * + * @default - No policy statements added to CodeBuild Project Role + */ + readonly rolePolicyStatements?: iam.PolicyStatement[]; + + /** + * Custom execution role to be used for the CodeBuild project + * + * @default - A role is automatically created + */ + readonly role?: iam.IRole; + + /** + * Changes to environment + * + * This environment will be combined with the pipeline's default + * environment. + * + * @default - Use the pipeline's default build environment + */ + readonly buildEnvironment?: codebuild.BuildEnvironment; + + /** + * Which security group to associate with the script's project network interfaces. + * If no security group is identified, one will be created automatically. + * + * Only used if 'vpc' is supplied. + * + * @default - Security group will be automatically created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; +} + +/** + * Run a script as a CodeBuild Project + */ +export class CodeBuildStep extends ShellStep { + /** + * Name for the generated CodeBuild project + * + * @default - No value specified at construction time, use defaults + */ + public readonly projectName?: string; + + /** + * Additional configuration that can only be configured via BuildSpec + * + * @default - No value specified at construction time, use defaults + */ + public readonly partialBuildSpec?: codebuild.BuildSpec; + + /** + * The VPC where to execute the SimpleSynth. + * + * @default - No value specified at construction time, use defaults + */ + public readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * @default - No value specified at construction time, use defaults + */ + public readonly subnetSelection?: ec2.SubnetSelection; + + /** + * Policy statements to add to role used during the synth + * + * @default - No value specified at construction time, use defaults + */ + public readonly rolePolicyStatements?: iam.PolicyStatement[]; + + /** + * Custom execution role to be used for the CodeBuild project + * + * @default - No value specified at construction time, use defaults + */ + public readonly role?: iam.IRole; + + /** + * Build environment + * + * @default - No value specified at construction time, use defaults + */ + readonly buildEnvironment?: codebuild.BuildEnvironment; + + /** + * Which security group to associate with the script's project network interfaces. + * + * @default - No value specified at construction time, use defaults + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + private _project?: codebuild.IProject; + + constructor(id: string, props: CodeBuildStepProps) { + super(id, props); + + this.projectName = props.projectName; + this.buildEnvironment = props.buildEnvironment; + this.partialBuildSpec = props.partialBuildSpec; + this.vpc = props.vpc; + this.subnetSelection = props.subnetSelection; + this.role = props.role; + this.rolePolicyStatements = props.rolePolicyStatements; + this.securityGroups = props.securityGroups; + } + + /** + * CodeBiuld Project generated for the pipeline + * + * Will only be available after the pipeline has been built. + */ + public get project(): codebuild.IProject { + if (!this._project) { + throw new Error('Project becomes available after the pipeline has been built'); + } + return this._project; + } + + /** + * The CodeBuild Project's principal + */ + public get grantPrincipal(): iam.IPrincipal { + return this.project.grantPrincipal; + } + + /** + * Set the internal project value + * + * @internal + */ + public _setProject(project: codebuild.IProject) { + this._project = project; + } +} diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts new file mode 100644 index 0000000000000..89d419b56223d --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts @@ -0,0 +1,100 @@ +import * as cb from '@aws-cdk/aws-codebuild'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import { Construct } from 'constructs'; +import { PipelineBase } from '../main'; +import { ArtifactMap } from './artifact-map'; +import { CodeBuildOptions } from './codepipeline'; + +/** + * Options for the `CodePipelineActionFactory.produce()` method. + */ +export interface ProduceActionOptions { + /** + * Scope in which to create constructs + */ + readonly scope: Construct; + + /** + * Name the action should get + */ + readonly actionName: string; + + /** + * RunOrder the action should get + */ + readonly runOrder: number; + + /** + * Helper object to translate FileSets to CodePipeline Artifacts + */ + readonly artifacts: ArtifactMap; + + /** + * An input artifact that CodeBuild projects that don't actually need an input artifact can use + * + * CodeBuild Projects MUST have an input artifact in order to be added to the Pipeline. If + * the Project doesn't actually care about its input (it can be anything), it can use the + * Artifact passed here. + * + * @default - A fallback artifact does not exist + */ + readonly fallbackArtifact?: cp.Artifact; + + /** + * The pipeline the action is being generated for + */ + readonly pipeline: PipelineBase; + + /** + * If this action factory creates a CodeBuild step, default options to inherit + * + * @default - No CodeBuild project defaults + */ + readonly codeBuildDefaults?: CodeBuildOptions; + + /** + * Whether or not this action is inserted before self mutation. + * + * If it is, the action should take care to reflect some part of + * its own definition in the pipeline action definition, to + * trigger a restart after self-mutation (if necessary). + * + * @default false + */ + readonly beforeSelfMutation?: boolean; +} + +/** + * Factory for explicit CodePipeline Actions + * + * If you have specific types of Actions you want to add to a + * CodePipeline, write a subclass of `Step` that implements this + * interface, and add the action or actions you want in the `produce` method. + * + * There needs to be a level of indirection here, because some aspects of the + * Action creation need to be controlled by the workflow engine (name and + * runOrder). All the rest of the properties are controlled by the factory. + */ +export interface ICodePipelineActionFactory { + /** + * Create the desired Action and add it to the pipeline + */ + produceAction(stage: cp.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult; +} + +/** + * The result of adding actions to the pipeline + */ +export interface CodePipelineActionFactoryResult { + /** + * How many RunOrders were consumed + */ + readonly runOrdersConsumed: number; + + /** + * If a CodeBuild project got created, the project + * + * @default - This factory did not create a CodeBuild project + */ + readonly project?: cb.IProject; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts new file mode 100644 index 0000000000000..d97b4c5f925de --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -0,0 +1,354 @@ +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import { Artifact } from '@aws-cdk/aws-codepipeline'; +import * as cp_actions from '@aws-cdk/aws-codepipeline-actions'; +import { Action, CodeCommitTrigger, GitHubTrigger, S3Trigger } from '@aws-cdk/aws-codepipeline-actions'; +import * as iam from '@aws-cdk/aws-iam'; +import { IBucket } from '@aws-cdk/aws-s3'; +import { SecretValue } from '@aws-cdk/core'; +import { FileSet, Step } from '../blueprint'; +import { CodePipelineActionFactoryResult, ProduceActionOptions, ICodePipelineActionFactory } from './codepipeline-action-factory'; + +/** + * CodePipeline source steps + * + * This class contains a number of factory methods for the different types + * of sources that CodePipeline supports. + */ +export abstract class CodePipelineSource extends Step implements ICodePipelineActionFactory { + /** + * Returns a GitHub source, using OAuth tokens to authenticate with + * GitHub and a separate webhook to detect changes. This is no longer + * the recommended method. Please consider using `connection()` + * instead. + * + * Pass in the owner and repository in a single string, like this: + * + * ```ts + * CodePipelineSource.gitHub('owner/repo', 'main'); + * ``` + * + * Authentication will be done by a secret called `github-token` in AWS + * Secrets Manager (unless specified otherwise). + * + * The token should have these permissions: + * + * * **repo** - to read the repository + * * **admin:repo_hook** - if you plan to use webhooks (true by default) + */ + public static gitHub(repoString: string, branch: string, props: GitHubSourceOptions = {}): CodePipelineSource { + return new GitHubSource(repoString, branch, props); + } + + /** + * Returns an S3 source. + * + * @param bucket The bucket where the source code is located. + * @param props The options, which include the key that identifies the source code file and + * and how the pipeline should be triggered. + * + * Example: + * + * ```ts + * const bucket: IBucket = ... + * CodePipelineSource.s3(bucket, { + * key: 'path/to/file.zip', + * }); + * ``` + */ + public static s3(bucket: IBucket, objectKey: string, props: S3SourceOptions = {}): CodePipelineSource { + return new S3Source(bucket, objectKey, props); + } + + /** + * Returns a CodeStar connection source. A CodeStar connection allows AWS CodePipeline to + * access external resources, such as repositories in GitHub, GitHub Enterprise or + * BitBucket. + * + * To use this method, you first need to create a CodeStar connection + * using the AWS console. In the process, you may have to sign in to the external provider + * -- GitHub, for example -- to authorize AWS to read and modify your repository. + * Once you have done this, copy the connection ARN and use it to create the source. + * + * Example: + * + * ```ts + * CodePipelineSource.connection('owner/repo', 'main', { + * connectionArn: 'arn:aws:codestar-connections:us-east-1:222222222222:connection/7d2469ff-514a-4e4f-9003-5ca4a43cdc41', // Created using the AWS console + * }); + * ``` + * + * @param repoString A string that encodes owner and repository separated by a slash (e.g. 'owner/repo'). + * @param branch The branch to use. + * @param props The source properties, including the connection ARN. + * + * @see https://docs.aws.amazon.com/dtconsole/latest/userguide/welcome-connections.html + */ + public static connection(repoString: string, branch: string, props: ConnectionSourceOptions): CodePipelineSource { + return new CodeStarConnectionSource(repoString, branch, props); + } + + /** + * Returns a CodeCommit source. + * + * @param repository The CodeCommit repository. + * @param branch The branch to use. + * @param props The source properties. + * + * Example: + * + * ```ts + * const repository: IRepository = ... + * CodePipelineSource.codeCommit(repository, 'main'); + * ``` + */ + public static codeCommit(repository: codecommit.IRepository, branch: string, props: CodeCommitSourceOptions = {}): CodePipelineSource { + return new CodeCommitSource(repository, branch, props); + } + + // tells `PipelineGraph` to hoist a "Source" step + public readonly isSource = true; + + public produceAction(stage: cp.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { + const output = options.artifacts.toCodePipeline(this.primaryOutput!); + const action = this.getAction(output, options.actionName, options.runOrder); + stage.addAction(action); + return { runOrdersConsumed: 1 }; + } + + protected abstract getAction(output: Artifact, actionName: string, runOrder: number): Action; +} + +/** + * Options for GitHub sources + */ +export interface GitHubSourceOptions { + /** + * A GitHub OAuth token to use for authentication. + * + * It is recommended to use a Secrets Manager `Secret` to obtain the token: + * + * ```ts + * const oauth = cdk.SecretValue.secretsManager('my-github-token'); + * new GitHubSource(this, 'GitHubSource', { oauthToken: oauth, ... }); + * ``` + * + * The GitHub Personal Access Token should have these scopes: + * + * * **repo** - to read the repository + * * **admin:repo_hook** - if you plan to use webhooks (true by default) + * + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/GitHub-create-personal-token-CLI.html + * + * @default - SecretValue.secretsManager('github-token') + */ + readonly authentication?: SecretValue; + + /** + * How AWS CodePipeline should be triggered + * + * With the default value "WEBHOOK", a webhook is created in GitHub that triggers the action. + * With "POLL", CodePipeline periodically checks the source for changes. + * With "None", the action is not triggered through changes in the source. + * + * To use `WEBHOOK`, your GitHub Personal Access Token should have + * **admin:repo_hook** scope (in addition to the regular **repo** scope). + * + * @default GitHubTrigger.WEBHOOK + */ + readonly trigger?: GitHubTrigger; + +} + +/** + * Extend CodePipelineSource so we can type-test in the CodePipelineEngine. + */ +class GitHubSource extends CodePipelineSource { + private readonly owner: string; + private readonly repo: string; + private readonly authentication: SecretValue; + + constructor(repoString: string, readonly branch: string, readonly props: GitHubSourceOptions) { + super(repoString); + + const parts = repoString.split('/'); + if (parts.length !== 2) { + throw new Error(`GitHub repository name should look like '/', got '${repoString}'`); + } + this.owner = parts[0]; + this.repo = parts[1]; + this.authentication = props.authentication ?? SecretValue.secretsManager('github-token'); + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, actionName: string, runOrder: number) { + return new cp_actions.GitHubSourceAction({ + output, + actionName, + runOrder, + oauthToken: this.authentication, + owner: this.owner, + repo: this.repo, + branch: this.branch, + trigger: this.props.trigger, + }); + } +} + +/** + * Options for S3 sources + */ +export interface S3SourceOptions { + /** + * How should CodePipeline detect source changes for this Action. + * Note that if this is S3Trigger.EVENTS, you need to make sure to include the source Bucket in a CloudTrail Trail, + * as otherwise the CloudWatch Events will not be emitted. + * + * @default S3Trigger.POLL + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/log-s3-data-events.html + */ + readonly trigger?: S3Trigger; +} + +class S3Source extends CodePipelineSource { + constructor(readonly bucket: IBucket, private readonly objectKey: string, readonly props: S3SourceOptions) { + super(bucket.bucketName); + + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, actionName: string, runOrder: number) { + return new cp_actions.S3SourceAction({ + output, + actionName, + runOrder, + bucketKey: this.objectKey, + trigger: this.props.trigger, + bucket: this.bucket, + }); + } +} + +/** + * Configuration options for CodeStar source + */ +export interface ConnectionSourceOptions { + /** + * The ARN of the CodeStar Connection created in the AWS console + * that has permissions to access this GitHub or BitBucket repository. + * + * @example 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh' + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/connections-create.html + */ + readonly connectionArn: string; + + + // long URL in @see + /** + * Whether the output should be the contents of the repository + * (which is the default), + * or a link that allows CodeBuild to clone the repository before building. + * + * **Note**: if this option is true, + * then only CodeBuild actions can use the resulting {@link output}. + * + * @default false + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodestarConnectionSource.html#action-reference-CodestarConnectionSource-config + */ + readonly codeBuildCloneOutput?: boolean; + + /** + * Controls automatically starting your pipeline when a new commit + * is made on the configured repository and branch. If unspecified, + * the default value is true, and the field does not display by default. + * + * @default true + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodestarConnectionSource.html + */ + readonly triggerOnPush?: boolean; +} + +class CodeStarConnectionSource extends CodePipelineSource { + private readonly owner: string; + private readonly repo: string; + + constructor(repoString: string, readonly branch: string, readonly props: ConnectionSourceOptions) { + super(repoString); + + const parts = repoString.split('/'); + if (parts.length !== 2) { + throw new Error(`CodeStar repository name should look like '/', got '${repoString}'`); + } + this.owner = parts[0]; + this.repo = parts[1]; + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, actionName: string, runOrder: number) { + return new cp_actions.CodeStarConnectionsSourceAction({ + output, + actionName, + runOrder, + connectionArn: this.props.connectionArn, + owner: this.owner, + repo: this.repo, + branch: this.branch, + codeBuildCloneOutput: this.props.codeBuildCloneOutput, + triggerOnPush: this.props.triggerOnPush, + }); + } +} + +/** + * Configuration options for a CodeCommit source + */ +export interface CodeCommitSourceOptions { + /** + * How should CodePipeline detect source changes for this Action. + * + * @default CodeCommitTrigger.EVENTS + */ + readonly trigger?: CodeCommitTrigger; + + /** + * Role to be used by on commit event rule. + * Used only when trigger value is CodeCommitTrigger.EVENTS. + * + * @default a new role will be created. + */ + readonly eventRole?: iam.IRole; + + /** + * Whether the output should be the contents of the repository + * (which is the default), + * or a link that allows CodeBuild to clone the repository before building. + * + * **Note**: if this option is true, + * then only CodeBuild actions can use the resulting {@link output}. + * + * @default false + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodeCommit.html + */ + readonly codeBuildCloneOutput?: boolean; +} + +class CodeCommitSource extends CodePipelineSource { + constructor(readonly repository: codecommit.IRepository, readonly branch: string, readonly props: CodeCommitSourceOptions) { + super(repository.repositoryName); + + this.configurePrimaryOutput(new FileSet('Source', this)); + } + + protected getAction(output: Artifact, actionName: string, runOrder: number) { + return new cp_actions.CodeCommitSourceAction({ + output, + actionName, + runOrder, + branch: this.branch, + trigger: this.props.trigger, + repository: this.repository, + eventRole: this.props.eventRole, + codeBuildCloneOutput: this.props.codeBuildCloneOutput, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts new file mode 100644 index 0000000000000..1c61a2c8cc42d --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts @@ -0,0 +1,961 @@ +import * as path from 'path'; +import * as cb from '@aws-cdk/aws-codebuild'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import * as cpa from '@aws-cdk/aws-codepipeline-actions'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { Aws, Fn, IDependable, Lazy, PhysicalName, Stack } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { Construct, Node } from 'constructs'; +import { AssetType, FileSet, IFileSetProducer, ManualApprovalStep, ShellStep, StackAsset, StackDeployment, Step } from '../blueprint'; +import { DockerCredential, dockerCredentialsInstallCommands, DockerCredentialUsage } from '../docker-credentials'; +import { GraphNode, GraphNodeCollection, isGraph, AGraphNode, PipelineGraph } from '../helpers-internal'; +import { PipelineBase } from '../main'; +import { appOf, assemblyBuilderOf, embeddedAsmPath, obtainScope } from '../private/construct-internals'; +import { toPosixPath } from '../private/fs'; +import { enumerate, flatten, maybeSuffix, noUndefined } from '../private/javascript'; +import { writeTemplateConfiguration } from '../private/template-configuration'; +import { CodeBuildFactory, mergeCodeBuildOptions, stackVariableNamespace } from './_codebuild-factory'; +import { ArtifactMap } from './artifact-map'; +import { CodeBuildStep } from './codebuild-step'; +import { CodePipelineActionFactoryResult, ICodePipelineActionFactory } from './codepipeline-action-factory'; + + +/** + * Properties for a `CodePipeline` + */ +export interface CodePipelineProps { + /** + * The build step that produces the CDK Cloud Assembly + * + * The primary output of this step needs to be the `cdk.out` directory + * generated by the `cdk synth` command. + * + * If you use a `ShellStep` here and you don't configure an output directory, + * the output directory will automatically be assumed to be `cdk.out`. + */ + readonly synth: IFileSetProducer; + + /** + * The name of the CodePipeline pipeline + * + * @default - Automatically generated + */ + readonly pipelineName?: string; + + /** + * Create KMS keys for the artifact buckets, allowing cross-account deployments + * + * The artifact buckets have to be encrypted to support deploying CDK apps to + * another account, so if you want to do that or want to have your artifact + * buckets encrypted, be sure to set this value to `true`. + * + * Be aware there is a cost associated with maintaining the KMS keys. + * + * @default false + */ + readonly crossAccountKeys?: boolean; + + /** + * CDK CLI version to use in self-mutation and asset publishing steps + * + * If you want to lock the CDK CLI version used in the pipeline, by steps + * that are automatically generated for you, specify the version here. + * + * You should not typically need to specify this value. + * + * @default - Latest version + */ + readonly cliVersion?: string; + + /** + * Whether the pipeline will update itself + * + * This needs to be set to `true` to allow the pipeline to reconfigure + * itself when assets or stages are being added to it, and `true` is the + * recommended setting. + * + * You can temporarily set this to `false` while you are iterating + * on the pipeline itself and prefer to deploy changes using `cdk deploy`. + * + * @default true + */ + readonly selfMutation?: boolean; + + /** + * Enable Docker for the self-mutate step + * + * Set this to true if the pipeline itself uses Docker container assets + * (for example, if you use `LinuxBuildImage.fromAsset()` as the build + * image of a CodeBuild step in the pipeline). + * + * You do not need to set it if you build Docker image assets in the + * application Stages and Stacks that are *deployed* by this pipeline. + * + * Configures privileged mode for the self-mutation CodeBuild action. + * + * If you are about to turn this on in an already-deployed Pipeline, + * set the value to `true` first, commit and allow the pipeline to + * self-update, and only then use the Docker asset in the pipeline. + * + * @default false + */ + readonly dockerEnabledForSelfMutation?: boolean; + + /** + * Enable Docker for the 'synth' step + * + * Set this to true if you are using file assets that require + * "bundling" anywhere in your application (meaning an asset + * compilation step will be run with the tools provided by + * a Docker image), both for the Pipeline stack as well as the + * application stacks. + * + * A common way to use bundling assets in your application is by + * using the `@aws-cdk/aws-lambda-nodejs` library. + * + * Configures privileged mode for the synth CodeBuild action. + * + * If you are about to turn this on in an already-deployed Pipeline, + * set the value to `true` first, commit and allow the pipeline to + * self-update, and only then use the bundled asset. + * + * @default false + */ + readonly dockerEnabledForSynth?: boolean; + + /** + * Customize the CodeBuild projects created for this pipeline + * + * @default - All projects run non-privileged build, SMALL instance, LinuxBuildImage.STANDARD_5_0 + */ + readonly codeBuildDefaults?: CodeBuildOptions; + + /** + * Additional customizations to apply to the asset publishing CodeBuild projects + * + * @default - Only `codeBuildProjectDefaults` are applied + */ + readonly assetPublishingCodeBuildDefaults?: CodeBuildOptions; + + /** + * Additional customizations to apply to the self mutation CodeBuild projects + * + * @default - Only `codeBuildProjectDefaults` are applied + */ + readonly selfMutationCodeBuildDefaults?: CodeBuildOptions; + + /** + * Publish assets in multiple CodeBuild projects + * + * If set to false, use one Project per type to publish all assets. + * + * Publishing in parallel improves concurrency and may reduce publishing + * latency, but may also increase overall provisioning time of the CodeBuild + * projects. + * + * Experiment and see what value works best for you. + * + * @default true + */ + readonly publishAssetsInParallel?: boolean; + + /** + * A list of credentials used to authenticate to Docker registries. + * + * Specify any credentials necessary within the pipeline to build, synth, update, or publish assets. + * + * @default [] + */ + readonly dockerCredentials?: DockerCredential[]; + + /** + * An existing Pipeline to be reused and built upon. + * + * [disable-awslint:ref-via-interface] + * + * @default - a new underlying pipeline is created. + */ + readonly codePipeline?: cp.Pipeline; +} + +/** + * Options for customizing a single CodeBuild project + */ +export interface CodeBuildOptions { + /** + * Partial build environment, will be combined with other build environments that apply + * + * @default - Non-privileged build, SMALL instance, LinuxBuildImage.STANDARD_5_0 + */ + readonly buildEnvironment?: cb.BuildEnvironment; + + /** + * Policy statements to add to role + * + * @default - No policy statements added to CodeBuild Project Role + */ + readonly rolePolicy?: iam.PolicyStatement[]; + + /** + * Partial buildspec, will be combined with other buildspecs that apply + * + * The BuildSpec must be available inline--it cannot reference a file + * on disk. + * + * @default - No initial BuildSpec + */ + readonly partialBuildSpec?: cb.BuildSpec; + + /** + * Which security group(s) to associate with the project network interfaces. + * + * Only used if 'vpc' is supplied. + * + * @default - Security group will be automatically created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * The VPC where to create the CodeBuild network interfaces in. + * + * @default - No VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * Only used if 'vpc' is supplied. + * + * @default - All private subnets. + */ + readonly subnetSelection?: ec2.SubnetSelection; +} + + +/** + * A CDK Pipeline that uses CodePipeline to deploy CDK apps + * + * This is a `Pipeline` with its `engine` property set to + * `CodePipelineEngine`, and exists for nicer ergonomics for + * users that don't need to switch out engines. + */ +export class CodePipeline extends PipelineBase { + private _pipeline?: cp.Pipeline; + private artifacts = new ArtifactMap(); + private _synthProject?: cb.IProject; + private readonly selfMutation: boolean; + private _myCxAsmRoot?: string; + private readonly dockerCredentials: DockerCredential[]; + + /** + * Asset roles shared for publishing + */ + private readonly assetCodeBuildRoles: Record = {}; + + /** + * Policies created for the build projects that they have to depend on + */ + private readonly assetAttachedPolicies: Record = {}; + + /** + * Per asset type, the target role ARNs that need to be assumed + */ + private readonly assetPublishingRoles: Record> = {}; + + /** + * This is set to the very first artifact produced in the pipeline + */ + private _fallbackArtifact?: cp.Artifact; + + private _cloudAssemblyFileSet?: FileSet; + + private readonly singlePublisherPerAssetType: boolean; + + constructor(scope: Construct, id: string, private readonly props: CodePipelineProps) { + super(scope, id, props); + + this.selfMutation = props.selfMutation ?? true; + this.dockerCredentials = props.dockerCredentials ?? []; + this.singlePublisherPerAssetType = !(props.publishAssetsInParallel ?? true); + } + + /** + * The CodeBuild project that performs the Synth + * + * Only available after the pipeline has been built. + */ + public get synthProject(): cb.IProject { + if (!this._synthProject) { + throw new Error('Call pipeline.buildPipeline() before reading this property'); + } + return this._synthProject; + } + + /** + * The CodePipeline pipeline that deploys the CDK app + * + * Only available after the pipeline has been built. + */ + public get pipeline(): cp.Pipeline { + if (!this._pipeline) { + throw new Error('Pipeline not created yet'); + } + return this._pipeline; + } + + + protected doBuildPipeline(): void { + if (this._pipeline) { + throw new Error('Pipeline already created'); + } + + this._myCxAsmRoot = path.resolve(assemblyBuilderOf(appOf(this)).outdir); + + if (this.props.codePipeline) { + if (this.props.pipelineName) { + throw new Error('Cannot set \'pipelineName\' if an existing CodePipeline is given using \'codePipeline\''); + } + if (this.props.crossAccountKeys !== undefined) { + throw new Error('Cannot set \'crossAccountKeys\' if an existing CodePipeline is given using \'codePipeline\''); + } + + this._pipeline = this.props.codePipeline; + } else { + this._pipeline = new cp.Pipeline(this, 'Pipeline', { + pipelineName: this.props.pipelineName, + crossAccountKeys: this.props.crossAccountKeys ?? false, + // This is necessary to make self-mutation work (deployments are guaranteed + // to happen only after the builds of the latest pipeline definition). + restartExecutionOnUpdate: true, + }); + } + + const graphFromBp = new PipelineGraph(this, { + selfMutation: this.selfMutation, + singlePublisherPerAssetType: this.singlePublisherPerAssetType, + }); + this._cloudAssemblyFileSet = graphFromBp.cloudAssemblyFileSet; + + this.pipelineStagesAndActionsFromGraph(graphFromBp); + } + + private get myCxAsmRoot(): string { + if (!this._myCxAsmRoot) { + throw new Error('Can\'t read \'myCxAsmRoot\' if build deployment not called yet'); + } + return this._myCxAsmRoot; + } + + /** + * Scope for Assets-related resources. + * + * Purely exists for construct tree backwards compatibility with legacy pipelines + */ + private get assetsScope(): Construct { + return obtainScope(this, 'Assets'); + } + + private pipelineStagesAndActionsFromGraph(structure: PipelineGraph) { + // Translate graph into Pipeline Stages and Actions + let beforeSelfMutation = this.selfMutation; + for (const stageNode of flatten(structure.graph.sortedChildren())) { + if (!isGraph(stageNode)) { + throw new Error(`Top-level children must be graphs, got '${stageNode}'`); + } + + // Group our ordered tranches into blocks of 50. + // We can map these onto stages without exceeding the capacity of a Stage. + const chunks = chunkTranches(50, stageNode.sortedLeaves()); + const actionsOverflowStage = chunks.length > 1; + for (const [i, tranches] of enumerate(chunks)) { + const stageName = actionsOverflowStage ? `${stageNode.id}.${i + 1}` : stageNode.id; + const pipelineStage = this.pipeline.addStage({ stageName }); + + const sharedParent = new GraphNodeCollection(flatten(tranches)).commonAncestor(); + + let runOrder = 1; + for (const tranche of tranches) { + const runOrdersConsumed = [0]; + + for (const node of tranche) { + const factory = this.actionFromNode(node); + + const nodeType = this.nodeTypeFromNode(node); + + const result = factory.produceAction(pipelineStage, { + actionName: actionName(node, sharedParent), + runOrder, + artifacts: this.artifacts, + scope: obtainScope(this.pipeline, stageName), + fallbackArtifact: this._fallbackArtifact, + pipeline: this, + // If this step happens to produce a CodeBuild job, set the default options + codeBuildDefaults: nodeType ? this.codeBuildDefaultsFor(nodeType) : undefined, + beforeSelfMutation, + }); + + if (node.data?.type === 'self-update') { + beforeSelfMutation = false; + } + + this.postProcessNode(node, result); + + runOrdersConsumed.push(result.runOrdersConsumed); + } + + runOrder += Math.max(...runOrdersConsumed); + } + } + } + } + + /** + * Do additional things after the action got added to the pipeline + * + * Some minor state manipulation of CodeBuild projects and pipeline + * artifacts. + */ + private postProcessNode(node: AGraphNode, result: CodePipelineActionFactoryResult) { + const nodeType = this.nodeTypeFromNode(node); + + if (result.project) { + const dockerUsage = dockerUsageFromCodeBuild(nodeType ?? CodeBuildProjectType.STEP); + if (dockerUsage) { + for (const c of this.dockerCredentials) { + c.grantRead(result.project, dockerUsage); + } + } + + if (nodeType === CodeBuildProjectType.SYNTH) { + this._synthProject = result.project; + } + } + + if (node.data?.type === 'step' && node.data.step.primaryOutput?.primaryOutput && !this._fallbackArtifact) { + this._fallbackArtifact = this.artifacts.toCodePipeline(node.data.step.primaryOutput?.primaryOutput); + } + } + + /** + * Make an action from the given node and/or step + */ + private actionFromNode(node: AGraphNode): ICodePipelineActionFactory { + switch (node.data?.type) { + // Nothing for these, they are groupings (shouldn't even have popped up here) + case 'group': + case 'stack-group': + case undefined: + throw new Error(`actionFromNode: did not expect to get group nodes: ${node.data?.type}`); + + case 'self-update': + return this.selfMutateAction(); + + case 'publish-assets': + return this.publishAssetsAction(node, node.data.assets); + + case 'prepare': + return this.createChangeSetAction(node.data.stack); + + case 'execute': + return this.executeChangeSetAction(node.data.stack, node.data.captureOutputs); + + case 'step': + return this.actionFromStep(node, node.data.step); + } + } + + /** + * Take a Step and turn it into a CodePipeline Action + * + * There are only 3 types of Steps we need to support: + * + * - RunScript (generic) + * - ManualApproval (generic) + * - CodePipelineActionFactory (CodePipeline-specific) + * + * The rest is expressed in terms of these 3, or in terms of graph nodes + * which are handled elsewhere. + */ + private actionFromStep(node: AGraphNode, step: Step): ICodePipelineActionFactory { + const nodeType = this.nodeTypeFromNode(node); + + // CodePipeline-specific steps first -- this includes Sources + if (isCodePipelineActionFactory(step)) { + return step; + } + + // Now built-in steps + if (step instanceof ShellStep || step instanceof CodeBuildStep) { + // The 'CdkBuildProject' will be the construct ID of the CodeBuild project, necessary for backwards compat + let constructId = nodeType === CodeBuildProjectType.SYNTH + ? 'CdkBuildProject' + : step.id; + + return step instanceof CodeBuildStep + ? CodeBuildFactory.fromCodeBuildStep(constructId, step) + : CodeBuildFactory.fromShellStep(constructId, step); + } + + if (step instanceof ManualApprovalStep) { + return { + produceAction: (stage, options) => { + stage.addAction(new cpa.ManualApprovalAction({ + actionName: options.actionName, + runOrder: options.runOrder, + additionalInformation: step.comment, + })); + return { runOrdersConsumed: 1 }; + }, + }; + } + + throw new Error(`Deployment step '${step}' is not supported for CodePipeline-backed pipelines`); + } + + private createChangeSetAction(stack: StackDeployment): ICodePipelineActionFactory { + const changeSetName = 'PipelineChange'; + + const templateArtifact = this.artifacts.toCodePipeline(this._cloudAssemblyFileSet!); + const templateConfigurationPath = this.writeTemplateConfiguration(stack); + + const region = stack.region !== Stack.of(this).region ? stack.region : undefined; + const account = stack.account !== Stack.of(this).account ? stack.account : undefined; + + const relativeTemplatePath = path.relative(this.myCxAsmRoot, stack.absoluteTemplatePath); + + return { + produceAction: (stage, options) => { + stage.addAction(new cpa.CloudFormationCreateReplaceChangeSetAction({ + actionName: options.actionName, + runOrder: options.runOrder, + changeSetName, + stackName: stack.stackName, + templatePath: templateArtifact.atPath(toPosixPath(relativeTemplatePath)), + adminPermissions: true, + role: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.assumeRoleArn), + deploymentRole: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.executionRoleArn), + region: region, + templateConfiguration: templateConfigurationPath + ? templateArtifact.atPath(toPosixPath(templateConfigurationPath)) + : undefined, + })); + return { runOrdersConsumed: 1 }; + }, + }; + } + + private executeChangeSetAction(stack: StackDeployment, captureOutputs: boolean): ICodePipelineActionFactory { + const changeSetName = 'PipelineChange'; + + const region = stack.region !== Stack.of(this).region ? stack.region : undefined; + const account = stack.account !== Stack.of(this).account ? stack.account : undefined; + + return { + produceAction: (stage, options) => { + stage.addAction(new cpa.CloudFormationExecuteChangeSetAction({ + actionName: options.actionName, + runOrder: options.runOrder, + changeSetName, + stackName: stack.stackName, + role: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.assumeRoleArn), + region: region, + variablesNamespace: captureOutputs ? stackVariableNamespace(stack) : undefined, + })); + + return { runOrdersConsumed: 1 }; + }, + }; + } + + private selfMutateAction(): ICodePipelineActionFactory { + const installSuffix = this.props.cliVersion ? `@${this.props.cliVersion}` : ''; + + const pipelineStack = Stack.of(this.pipeline); + const pipelineStackIdentifier = pipelineStack.node.path ?? pipelineStack.stackName; + + const step = new CodeBuildStep('SelfMutate', { + projectName: maybeSuffix(this.props.pipelineName, '-selfupdate'), + input: this._cloudAssemblyFileSet, + installCommands: [ + `npm install -g aws-cdk${installSuffix}`, + ], + commands: [ + `cdk -a ${toPosixPath(embeddedAsmPath(this.pipeline))} deploy ${pipelineStackIdentifier} --require-approval=never --verbose`, + ], + + rolePolicyStatements: [ + // allow the self-mutating project permissions to assume the bootstrap Action role + new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: [`arn:*:iam::${Stack.of(this.pipeline).account}:role/*`], + conditions: { + 'ForAnyValue:StringEquals': { + 'iam:ResourceTag/aws-cdk:bootstrap-role': ['image-publishing', 'file-publishing', 'deploy'], + }, + }, + }), + new iam.PolicyStatement({ + actions: ['cloudformation:DescribeStacks'], + resources: ['*'], // this is needed to check the status of the bootstrap stack when doing `cdk deploy` + }), + // S3 checks for the presence of the ListBucket permission + new iam.PolicyStatement({ + actions: ['s3:ListBucket'], + resources: ['*'], + }), + ], + }); + + // Different on purpose -- id needed for backwards compatible LogicalID + return CodeBuildFactory.fromCodeBuildStep('SelfMutation', step, { + additionalConstructLevel: false, + scope: obtainScope(this, 'UpdatePipeline'), + }); + } + + private publishAssetsAction(node: AGraphNode, assets: StackAsset[]): ICodePipelineActionFactory { + const installSuffix = this.props.cliVersion ? `@${this.props.cliVersion}` : ''; + + const commands = assets.map(asset => { + const relativeAssetManifestPath = path.relative(this.myCxAsmRoot, asset.assetManifestPath); + return `cdk-assets --path "${toPosixPath(relativeAssetManifestPath)}" --verbose publish "${asset.assetSelector}"`; + }); + + const assetType = assets[0].assetType; + if (assets.some(a => a.assetType !== assetType)) { + throw new Error('All assets in a single publishing step must be of the same type'); + } + + const publishingRoles = this.assetPublishingRoles[assetType] = (this.assetPublishingRoles[assetType] ?? new Set()); + for (const asset of assets) { + if (asset.assetPublishingRoleArn) { + publishingRoles.add(asset.assetPublishingRoleArn); + } + } + + const assetBuildConfig = this.obtainAssetCodeBuildRole(assets[0].assetType); + + // The base commands that need to be run + const script = new CodeBuildStep(node.id, { + commands, + installCommands: [ + `npm install -g cdk-assets${installSuffix}`, + ], + input: this._cloudAssemblyFileSet, + buildEnvironment: { + privileged: assets.some(asset => asset.assetType === AssetType.DOCKER_IMAGE), + }, + role: assetBuildConfig.role, + }); + + // Customizations that are not accessible to regular users + return CodeBuildFactory.fromCodeBuildStep(node.id, script, { + additionalConstructLevel: false, + additionalDependable: assetBuildConfig.dependable, + + // If we use a single publisher, pass buildspec via file otherwise it'll + // grow too big. + passBuildSpecViaCloudAssembly: this.singlePublisherPerAssetType, + scope: this.assetsScope, + }); + } + + private nodeTypeFromNode(node: AGraphNode) { + if (node.data?.type === 'step') { + return !!node.data?.isBuildStep ? CodeBuildProjectType.SYNTH : CodeBuildProjectType.STEP; + } + if (node.data?.type === 'publish-assets') { + return CodeBuildProjectType.ASSETS; + } + if (node.data?.type === 'self-update') { + return CodeBuildProjectType.SELF_MUTATE; + } + return undefined; + } + + private codeBuildDefaultsFor(nodeType: CodeBuildProjectType): CodeBuildOptions | undefined { + const defaultOptions: CodeBuildOptions = { + buildEnvironment: { + buildImage: cb.LinuxBuildImage.STANDARD_5_0, + computeType: cb.ComputeType.SMALL, + }, + }; + + const typeBasedCustomizations = { + [CodeBuildProjectType.SYNTH]: this.props.dockerEnabledForSynth + ? { buildEnvironment: { privileged: true } } + : {}, + + [CodeBuildProjectType.ASSETS]: this.props.assetPublishingCodeBuildDefaults, + + [CodeBuildProjectType.SELF_MUTATE]: this.props.dockerEnabledForSelfMutation + ? mergeCodeBuildOptions(this.props.selfMutationCodeBuildDefaults, { buildEnvironment: { privileged: true } }) + : this.props.selfMutationCodeBuildDefaults, + + [CodeBuildProjectType.STEP]: {}, + }; + + const dockerUsage = dockerUsageFromCodeBuild(nodeType); + const dockerCommands = dockerUsage !== undefined + ? dockerCredentialsInstallCommands(dockerUsage, this.dockerCredentials, 'both') + : []; + const typeBasedDockerCommands = dockerCommands.length > 0 ? { + partialBuildSpec: cb.BuildSpec.fromObject({ + version: '0.2', + phases: { + pre_build: { + commands: dockerCommands, + }, + }, + }), + } : {}; + + return mergeCodeBuildOptions( + defaultOptions, + this.props.codeBuildDefaults, + typeBasedCustomizations[nodeType], + typeBasedDockerCommands, + ); + } + + private roleFromPlaceholderArn(scope: Construct, region: string | undefined, + account: string | undefined, arn: string): iam.IRole; + private roleFromPlaceholderArn(scope: Construct, region: string | undefined, + account: string | undefined, arn: string | undefined): iam.IRole | undefined; + private roleFromPlaceholderArn(scope: Construct, region: string | undefined, + account: string | undefined, arn: string | undefined): iam.IRole | undefined { + + if (!arn) { return undefined; } + + // Use placeholdered arn as construct ID. + const id = arn; + + // https://github.com/aws/aws-cdk/issues/7255 + let existingRole = Node.of(scope).tryFindChild(`ImmutableRole${id}`) as iam.IRole; + if (existingRole) { return existingRole; } + // For when #7255 is fixed. + existingRole = Node.of(scope).tryFindChild(id) as iam.IRole; + if (existingRole) { return existingRole; } + + const arnToImport = cxapi.EnvironmentPlaceholders.replace(arn, { + region: region ?? Aws.REGION, + accountId: account ?? Aws.ACCOUNT_ID, + partition: Aws.PARTITION, + }); + return iam.Role.fromRoleArn(scope, id, arnToImport, { mutable: false, addGrantsToResources: true }); + } + + /** + * Non-template config files for CodePipeline actions + * + * Currently only supports tags. + */ + private writeTemplateConfiguration(stack: StackDeployment): string | undefined { + if (Object.keys(stack.tags).length === 0) { return undefined; } + + const absConfigPath = `${stack.absoluteTemplatePath}.config.json`; + const relativeConfigPath = path.relative(this.myCxAsmRoot, absConfigPath); + + // Write the template configuration file (for parameters into CreateChangeSet call that + // cannot be configured any other way). They must come from a file, and there's unfortunately + // no better hook to write this file (`construct.onSynthesize()` would have been the prime candidate + // but that is being deprecated--and DeployCdkStackAction isn't even a construct). + writeTemplateConfiguration(absConfigPath, { + Tags: noUndefined(stack.tags), + }); + + return relativeConfigPath; + } + + /** + * This role is used by both the CodePipeline build action and related CodeBuild project. Consolidating these two + * roles into one, and re-using across all assets, saves significant size of the final synthesized output. + * Modeled after the CodePipeline role and 'CodePipelineActionRole' roles. + * Generates one role per asset type to separate file and Docker/image-based permissions. + */ + private obtainAssetCodeBuildRole(assetType: AssetType): AssetCodeBuildRole { + if (this.assetCodeBuildRoles[assetType]) { + return { + role: this.assetCodeBuildRoles[assetType], + dependable: this.assetAttachedPolicies[assetType], + }; + } + + const stack = Stack.of(this); + + const rolePrefix = assetType === AssetType.DOCKER_IMAGE ? 'Docker' : 'File'; + const assetRole = new iam.Role(this.assetsScope, `${rolePrefix}Role`, { + roleName: PhysicalName.GENERATE_IF_NEEDED, + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('codebuild.amazonaws.com'), + new iam.AccountPrincipal(stack.account), + ), + }); + + // Logging permissions + const logGroupArn = stack.formatArn({ + service: 'logs', + resource: 'log-group', + sep: ':', + resourceName: '/aws/codebuild/*', + }); + assetRole.addToPolicy(new iam.PolicyStatement({ + resources: [logGroupArn], + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + })); + + // CodeBuild report groups + const codeBuildArn = stack.formatArn({ + service: 'codebuild', + resource: 'report-group', + resourceName: '*', + }); + assetRole.addToPolicy(new iam.PolicyStatement({ + actions: [ + 'codebuild:CreateReportGroup', + 'codebuild:CreateReport', + 'codebuild:UpdateReport', + 'codebuild:BatchPutTestCases', + 'codebuild:BatchPutCodeCoverages', + ], + resources: [codeBuildArn], + })); + + // CodeBuild start/stop + assetRole.addToPolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 'codebuild:BatchGetBuilds', + 'codebuild:StartBuild', + 'codebuild:StopBuild', + ], + })); + + // Publishing role access + // The ARNs include raw AWS pseudo parameters (e.g., ${AWS::Partition}), which need to be substituted. + // Lazy-evaluated so all asset publishing roles are included. + assetRole.addToPolicy(new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: Lazy.list({ produce: () => Array.from(this.assetPublishingRoles[assetType] ?? []).map(arn => Fn.sub(arn)) }), + })); + + // Grant pull access for any ECR registries and secrets that exist + if (assetType === AssetType.DOCKER_IMAGE) { + this.dockerCredentials.forEach(reg => reg.grantRead(assetRole, DockerCredentialUsage.ASSET_PUBLISHING)); + } + + // Artifact access + this.pipeline.artifactBucket.grantRead(assetRole); + + // VPC permissions required for CodeBuild + // Normally CodeBuild itself takes care of this but we're creating a singleton role so now + // we need to do this. + const assetCodeBuildOptions = this.codeBuildDefaultsFor(CodeBuildProjectType.ASSETS); + if (assetCodeBuildOptions?.vpc) { + const vpcPolicy = new iam.Policy(assetRole, 'VpcPolicy', { + statements: [ + new iam.PolicyStatement({ + resources: [`arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:network-interface/*`], + actions: ['ec2:CreateNetworkInterfacePermission'], + conditions: { + StringEquals: { + 'ec2:Subnet': assetCodeBuildOptions.vpc + .selectSubnets(assetCodeBuildOptions.subnetSelection).subnetIds + .map(si => `arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:subnet/${si}`), + 'ec2:AuthorizedService': 'codebuild.amazonaws.com', + }, + }, + }), + new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeVpcs', + ], + }), + ], + }); + assetRole.attachInlinePolicy(vpcPolicy); + this.assetAttachedPolicies[assetType] = vpcPolicy; + } + + this.assetCodeBuildRoles[assetType] = assetRole.withoutPolicyUpdates(); + return { + role: this.assetCodeBuildRoles[assetType], + dependable: this.assetAttachedPolicies[assetType], + }; + } +} + +function dockerUsageFromCodeBuild(cbt: CodeBuildProjectType): DockerCredentialUsage | undefined { + switch (cbt) { + case CodeBuildProjectType.ASSETS: return DockerCredentialUsage.ASSET_PUBLISHING; + case CodeBuildProjectType.SELF_MUTATE: return DockerCredentialUsage.SELF_UPDATE; + case CodeBuildProjectType.SYNTH: return DockerCredentialUsage.SYNTH; + case CodeBuildProjectType.STEP: return undefined; + } +} + +interface AssetCodeBuildRole { + readonly role: iam.IRole; + readonly dependable?: IDependable; +} + +enum CodeBuildProjectType { + SYNTH = 'SYNTH', + ASSETS = 'ASSETS', + SELF_MUTATE = 'SELF_MUTATE', + STEP = 'STEP', +} + +function actionName(node: GraphNode, parent: GraphNode) { + const names = node.ancestorPath(parent).map(n => n.id); + return names.map(sanitizeName).join('.'); +} + +function sanitizeName(x: string): string { + return x.replace(/[^A-Za-z0-9.@\-_]/g, '_'); +} + +/** + * Take a set of tranches and split them up into groups so + * that no set of tranches has more than n items total + */ +function chunkTranches(n: number, xss: A[][]): A[][][] { + const ret: A[][][] = []; + + while (xss.length > 0) { + const tranches: A[][] = []; + let count = 0; + + while (xss.length > 0) { + const xs = xss[0]; + const spaceRemaining = n - count; + if (xs.length <= spaceRemaining) { + tranches.push(xs); + count += xs.length; + xss.shift(); + } else { + tranches.push(xs.splice(0, spaceRemaining)); + count = n; + break; + } + } + + ret.push(tranches); + } + + + return ret; +} + +function isCodePipelineActionFactory(x: any): x is ICodePipelineActionFactory { + return !!(x as ICodePipelineActionFactory).produceAction; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts new file mode 100644 index 0000000000000..00e10509bb0df --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts @@ -0,0 +1,5 @@ +export * from './artifact-map'; +export * from './codebuild-step'; +export * from './codepipeline'; +export * from './codepipeline-action-factory'; +export * from './codepipeline-source'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/docker-credentials.ts b/packages/@aws-cdk/pipelines/lib/docker-credentials.ts index a2a5b2ca39d64..77b7d2c1b4381 100644 --- a/packages/@aws-cdk/pipelines/lib/docker-credentials.ts +++ b/packages/@aws-cdk/pipelines/lib/docker-credentials.ts @@ -104,11 +104,11 @@ export interface EcrDockerCredentialOptions { /** Defines which stages of a pipeline require the specified credentials */ export enum DockerCredentialUsage { /** Synth/Build */ - SYNTH, + SYNTH = 'SYNTH', /** Self-update */ - SELF_UPDATE, + SELF_UPDATE = 'SELF_UPDATE', /** Asset publishing */ - ASSET_PUBLISHING, + ASSET_PUBLISHING = 'ASSET_PUBLISHING', }; /** DockerCredential defined by registry domain and a secret */ @@ -202,7 +202,7 @@ interface DockerCredentialCredentialSource { export function dockerCredentialsInstallCommands( usage: DockerCredentialUsage, registries?: DockerCredential[], - osType?: ec2.OperatingSystemType): string[] { + osType?: ec2.OperatingSystemType | 'both'): string[] { const relevantRegistries = (registries ?? []).filter(reg => reg._applicableForUsage(usage)); if (!relevantRegistries || relevantRegistries.length === 0) { return []; } @@ -216,15 +216,25 @@ export function dockerCredentialsInstallCommands( domainCredentials, }; - if (osType === ec2.OperatingSystemType.WINDOWS) { + const windowsCommands = [ + 'mkdir %USERPROFILE%\\.cdk', + `echo '${JSON.stringify(cdkAssetsConfigFile)}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, + ]; + + const linuxCommands = [ + 'mkdir $HOME/.cdk', + `echo '${JSON.stringify(cdkAssetsConfigFile)}' > $HOME/.cdk/cdk-docker-creds.json`, + ]; + + if (osType === 'both') { return [ - 'mkdir %USERPROFILE%\\.cdk', - `echo '${JSON.stringify(cdkAssetsConfigFile)}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, + // These tags are magic and will be stripped when rendering the project + ...windowsCommands.map(c => `!WINDOWS!${c}`), + ...linuxCommands.map(c => `!LINUX!${c}`), ]; + } else if (osType === ec2.OperatingSystemType.WINDOWS) { + return windowsCommands; } else { - return [ - 'mkdir $HOME/.cdk', - `echo '${JSON.stringify(cdkAssetsConfigFile)}' > $HOME/.cdk/cdk-docker-creds.json`, - ]; + return linuxCommands; } } diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts new file mode 100644 index 0000000000000..6b1c2d85ee701 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts @@ -0,0 +1,385 @@ +/** + * A library for nested graphs + */ +import { addAll, extract, flatMap, isDefined } from '../private/javascript'; +import { topoSort } from './toposort'; + +export interface GraphNodeProps { + readonly data?: A; +} + +export class GraphNode { + public static of(id: string, data: A) { + return new GraphNode(id, { data }); + } + + public readonly dependencies: GraphNode[] = []; + public readonly data?: A; + private _parentGraph?: Graph; + + constructor(public readonly id: string, props: GraphNodeProps = {}) { + this.data = props.data; + } + + /** + * A graph-wide unique identifier for this node. Rendered by joining the IDs + * of all ancestors with hyphens. + */ + public get uniqueId(): string { + return this.ancestorPath(this.root).map(x => x.id).join('-'); + } + + /** + * The union of all dependencies of this node and the dependencies of all + * parent graphs. + */ + public get allDeps(): GraphNode[] { + const fromParent = this.parentGraph?.allDeps ?? []; + return [...this.dependencies, ...fromParent]; + } + + public dependOn(...dependencies: Array | undefined>) { + if (dependencies.includes(this)) { + throw new Error(`Cannot add dependency on self: ${this}`); + } + this.dependencies.push(...dependencies.filter(isDefined)); + } + + public ancestorPath(upTo: GraphNode): GraphNode[] { + let x: GraphNode = this; + const ret = [x]; + while (x.parentGraph && x.parentGraph !== upTo) { + x = x.parentGraph; + ret.unshift(x); + } + return ret; + } + + public rootPath(): GraphNode[] { + let x: GraphNode = this; + const ret = [x]; + while (x.parentGraph) { + x = x.parentGraph; + ret.unshift(x); + } + return ret; + } + + public get root() { + let x: GraphNode = this; + while (x.parentGraph) { + x = x.parentGraph; + } + return x; + } + + public get parentGraph() { + return this._parentGraph; + } + + /** + * @internal + */ + public _setParentGraph(parentGraph: Graph) { + if (this._parentGraph) { + throw new Error('Node already has a parent'); + } + this._parentGraph = parentGraph; + } + + public toString() { + return `${this.constructor.name}(${this.id})`; + } +} + +/** + * A dependency set that can be constructed partially and later finished + * + * It doesn't matter in what order sources and targets for the dependency + * relationship(s) get added. This class can serve as a synchronization + * point if the order in which graph nodes get added to the graph is not + * well-defined. + * + * Useful utility during graph building. + */ +export class DependencyBuilder { + private readonly targets: GraphNode[] = []; + private readonly sources: GraphNode[] = []; + + public dependOn(...targets: GraphNode[]) { + for (const target of targets) { + for (const source of this.sources) { + source.dependOn(target); + } + this.targets.push(target); + } + return this; + } + + public dependBy(...sources: GraphNode[]) { + for (const source of sources) { + for (const target of this.targets) { + source.dependOn(target); + } + this.sources.push(source); + } + return this; + } +} + +export class DependencyBuilders { + private readonly builders = new Map>(); + + public get(key: K) { + const b = this.builders.get(key); + if (b) { return b; } + const ret = new DependencyBuilder(); + this.builders.set(key, ret); + return ret; + } +} + +export interface GraphProps extends GraphNodeProps { + /** + * Initial nodes in the workflow + */ + readonly nodes?: GraphNode[]; +} + +export class Graph extends GraphNode { + public static of(id: string, data: A, nodes?: GraphNode[]) { + return new Graph(id, { data, nodes }); + } + + private readonly children = new Map>(); + + constructor(name: string, props: GraphProps={}) { + super(name, props); + + if (props.nodes) { + this.add(...props.nodes); + } + } + + public get nodes() { + return new Set(this.children.values()); + } + + public tryGetChild(name: string) { + return this.children.get(name); + } + + public contains(node: GraphNode) { + return this.nodes.has(node); + } + + public add(...nodes: Array>) { + for (const node of nodes) { + node._setParentGraph(this); + if (this.children.has(node.id)) { + throw new Error(`Node with duplicate id: ${node.id}`); + } + this.children.set(node.id, node); + } + } + + public absorb(other: Graph) { + this.add(...other.nodes); + } + + /** + * Return topologically sorted tranches of nodes at this graph level + */ + public sortedChildren(): GraphNode[][] { + // Project dependencies to current children + const nodes = this.nodes; + const projectedDependencies = projectDependencies(this.deepDependencies(), (node) => { + while (!nodes.has(node) && node.parentGraph) { + node = node.parentGraph; + } + return nodes.has(node) ? [node] : []; + }); + + return topoSort(nodes, projectedDependencies); + } + + /** + * Return a topologically sorted list of non-Graph nodes in the entire subgraph + */ + public sortedLeaves(): GraphNode[][] { + // Project dependencies to leaf nodes + const descendantsMap = new Map, GraphNode[]>(); + findDescendants(this); + + function findDescendants(node: GraphNode): GraphNode[] { + const ret: GraphNode[] = []; + + if (node instanceof Graph) { + for (const child of node.nodes) { + ret.push(...findDescendants(child)); + } + } else { + ret.push(node); + } + + descendantsMap.set(node, ret); + return ret; + } + + const projectedDependencies = projectDependencies(this.deepDependencies(), (node) => descendantsMap.get(node) ?? []); + return topoSort(new Set(projectedDependencies.keys()), projectedDependencies); + } + + public consoleLog(indent: number = 0) { + process.stdout.write(' '.repeat(indent) + this + depString(this) + '\n'); + for (const node of this.nodes) { + if (node instanceof Graph) { + node.consoleLog(indent + 2); + } else { + process.stdout.write(' '.repeat(indent + 2) + node + depString(node) + '\n'); + } + } + + function depString(node: GraphNode) { + if (node.dependencies.length > 0) { + return ` -> ${Array.from(node.dependencies).join(', ')}`; + } + return ''; + } + } + + /** + * Return the union of all dependencies of the descendants of this graph + */ + private deepDependencies() { + const ret = new Map, Set>>(); + for (const node of this.nodes) { + recurse(node); + } + return ret; + + function recurse(node: GraphNode) { + let deps = ret.get(node); + if (!deps) { + ret.set(node, deps = new Set()); + } + for (let dep of node.dependencies) { + deps.add(dep); + } + if (node instanceof Graph) { + for (const child of node.nodes) { + recurse(child); + } + } + } + } + + /** + * Return all non-Graph nodes + */ + public allLeaves(): GraphNodeCollection { + const ret: GraphNode[] = []; + recurse(this); + return new GraphNodeCollection(ret); + + function recurse(node: GraphNode) { + if (node instanceof Graph) { + for (const child of node.nodes) { + recurse(child); + } + } else { + ret.push(node); + } + } + } +} + +/** + * A collection of graph nodes + */ +export class GraphNodeCollection { + public readonly nodes: GraphNode[]; + + constructor(nodes: Iterable>) { + this.nodes = Array.from(nodes); + } + + public dependOn(...dependencies: Array | undefined>) { + for (const node of this.nodes) { + node.dependOn(...dependencies.filter(isDefined)); + } + } + + /** + * Returns the graph node that's shared between these nodes + */ + public commonAncestor() { + const paths = new Array[]>(); + for (const x of this.nodes) { + paths.push(x.rootPath()); + } + + if (paths.length === 0) { + throw new Error('Cannot find common ancestor between an empty set of nodes'); + } + if (paths.length === 1) { + const path = paths[0]; + + if (path.length < 2) { + throw new Error(`Cannot find ancestor of node without ancestor: ${path[0]}`); + } + return path[path.length - 2]; + } + + const originalPaths = [...paths]; + + // Remove the first element of every path as long as the 2nd elements are all + // the same -- this leaves the shared element in first place. + // + // A, B, C, 1, 2 }---> C + // A, B, C, 3 } + while (paths.every(path => paths[0].length >= 2 && path.length >= 2 && path[1] === paths[0][1])) { + for (const path of paths) { + path.shift(); + } + } + + // If any of the paths are left with 1 element, there's no shared parent. + if (paths.some(path => path.length < 2)) { + throw new Error(`Could not determine a shared parent between nodes: ${originalPaths.map(nodes => nodes.map(n => n.id).join('/'))}`); + } + + return paths[0][0]; + } +} + +/** + * Dependency map of nodes in this graph, taking into account dependencies between nodes in subgraphs + * + * Guaranteed to return an entry in the map for every node in the current graph. + */ +function projectDependencies(dependencies: Map, Set>>, project: (x: GraphNode) => GraphNode[]) { + // Project keys + for (const node of dependencies.keys()) { + const projectedNodes = project(node); + if (projectedNodes.length === 1 && projectedNodes[0] === node) { continue; } // Nothing to do, just for efficiency + + const deps = extract(dependencies, node)!; + for (const projectedNode of projectedNodes) { + addAll(dependencies.get(projectedNode)!, deps); + } + } + + // Project values. Ignore self-dependencies, they were just between nodes that were collapsed into the same node. + for (const [node, deps] of dependencies.entries()) { + const depset = new Set(flatMap(deps, project)); + depset.delete(node); + dependencies.set(node, depset); + } + + return dependencies; +} + +export function isGraph(x: GraphNode): x is Graph { + return x instanceof Graph; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts new file mode 100644 index 0000000000000..6709f7c84488f --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts @@ -0,0 +1,2 @@ +export * from './pipeline-graph'; +export * from './graph'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts new file mode 100644 index 0000000000000..0adcea551ff32 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts @@ -0,0 +1,319 @@ +import { AssetType, FileSet, ShellStep, StackAsset, StackDeployment, StageDeployment, Step, Wave } from '../blueprint'; +import { PipelineBase } from '../main/pipeline-base'; +import { DependencyBuilders, Graph, GraphNode, GraphNodeCollection } from './graph'; +import { PipelineQueries } from './pipeline-queries'; + +export interface PipelineGraphProps { + /** + * Add a self-mutation step. + * + * @default false + */ + readonly selfMutation?: boolean; + + /** + * Publishes the template asset to S3. + * + * @default false + */ + readonly publishTemplate?: boolean; + + /** + * Whether to combine asset publishers for the same type into one step + * + * @default false + */ + readonly singlePublisherPerAssetType?: boolean; + + /** + * Add a "prepare" step for each stack which can be used to create the change + * set. If this is disbled, only the "execute" step will be included. + * + * @default true + */ + readonly prepareStep?: boolean; +} + +/** + * Logic to turn the deployment blueprint into a graph + * + * This code makes all the decisions on how to lay out the CodePipeline + */ +export class PipelineGraph { + /** + * A Step object that may be used as the producer of FileSets that should not be represented in the graph + */ + public static readonly NO_STEP: Step = new class extends Step { } ('NO_STEP'); + + public readonly graph: AGraph = Graph.of('', { type: 'group' }); + public readonly cloudAssemblyFileSet: FileSet; + public readonly queries: PipelineQueries; + + private readonly added = new Map(); + private readonly assetNodes = new Map(); + private readonly assetNodesByType = new Map(); + private readonly synthNode?: AGraphNode; + private readonly selfMutateNode?: AGraphNode; + private readonly stackOutputDependencies = new DependencyBuilders(); + private readonly publishTemplate: boolean; + private readonly prepareStep: boolean; + private readonly singlePublisher: boolean; + + private lastPreparationNode?: AGraphNode; + private _fileAssetCtr = 0; + private _dockerAssetCtr = 0; + + constructor(public readonly pipeline: PipelineBase, props: PipelineGraphProps = {}) { + this.publishTemplate = props.publishTemplate ?? false; + this.prepareStep = props.prepareStep ?? true; + this.singlePublisher = props.singlePublisherPerAssetType ?? false; + + this.queries = new PipelineQueries(pipeline); + + if (pipeline.synth instanceof Step) { + this.synthNode = this.addBuildStep(pipeline.synth); + if (this.synthNode?.data?.type === 'step') { + this.synthNode.data.isBuildStep = true; + } + } + this.lastPreparationNode = this.synthNode; + + const cloudAssembly = pipeline.synth.primaryOutput?.primaryOutput; + if (!cloudAssembly) { + throw new Error(`The synth step must produce the cloud assembly artifact, but doesn't: ${pipeline.synth}`); + } + + this.cloudAssemblyFileSet = cloudAssembly; + + if (props.selfMutation) { + const stage: AGraph = Graph.of('UpdatePipeline', { type: 'group' }); + this.graph.add(stage); + this.selfMutateNode = GraphNode.of('SelfMutate', { type: 'self-update' }); + stage.add(this.selfMutateNode); + + this.selfMutateNode.dependOn(this.synthNode); + this.lastPreparationNode = this.selfMutateNode; + } + + const waves = pipeline.waves.map(w => this.addWave(w)); + + // Make sure the waves deploy sequentially + for (let i = 1; i < waves.length; i++) { + waves[i].dependOn(waves[i - 1]); + } + + // Add additional dependencies between steps that depend on stack outputs and the stacks + // that produce them. + } + + public isSynthNode(node: AGraphNode) { + return this.synthNode === node; + } + + private addBuildStep(step: Step) { + return this.addAndRecurse(step, this.topLevelGraph('Build')); + } + + private addWave(wave: Wave): AGraph { + // If the wave only has one Stage in it, don't add an additional Graph around it + const retGraph: AGraph = wave.stages.length === 1 + ? this.addStage(wave.stages[0]) + : Graph.of(wave.id, { type: 'group' }, wave.stages.map(s => this.addStage(s))); + + this.addPrePost(wave.pre, wave.post, retGraph); + retGraph.dependOn(this.lastPreparationNode); + this.graph.add(retGraph); + + return retGraph; + } + + private addStage(stage: StageDeployment): AGraph { + const retGraph: AGraph = Graph.of(stage.stageName, { type: 'group' }); + + const stackGraphs = new Map(); + + for (const stack of stage.stacks) { + const stackGraph: AGraph = Graph.of(this.simpleStackName(stack.stackName, stage.stageName), { type: 'stack-group', stack }); + const prepareNode: AGraphNode | undefined = this.prepareStep ? GraphNode.of('Prepare', { type: 'prepare', stack }) : undefined; + const deployNode: AGraphNode = GraphNode.of('Deploy', { + type: 'execute', + stack, + captureOutputs: this.queries.stackOutputsReferenced(stack).length > 0, + }); + + retGraph.add(stackGraph); + + stackGraph.add(deployNode); + let firstDeployNode; + if (prepareNode) { + stackGraph.add(prepareNode); + deployNode.dependOn(prepareNode); + firstDeployNode = prepareNode; + } else { + firstDeployNode = deployNode; + } + + stackGraphs.set(stack, stackGraph); + + const cloudAssembly = this.cloudAssemblyFileSet; + + firstDeployNode.dependOn(this.addAndRecurse(cloudAssembly.producer, retGraph)); + + // add the template asset + if (this.publishTemplate) { + if (!stack.templateAsset) { + throw new Error(`"publishTemplate" is enabled, but stack ${stack.stackArtifactId} does not have a template asset`); + } + + firstDeployNode.dependOn(this.publishAsset(stack.templateAsset)); + } + + // Depend on Assets + // FIXME: Custom Cloud Assembly currently doesn't actually help separating + // out templates from assets!!! + for (const asset of stack.assets) { + const assetNode = this.publishAsset(asset); + firstDeployNode.dependOn(assetNode); + } + + // Add stack output synchronization point + if (this.queries.stackOutputsReferenced(stack).length > 0) { + this.stackOutputDependencies.get(stack).dependOn(deployNode); + } + } + + for (const stack of stage.stacks) { + for (const dep of stack.stackDependencies) { + const stackNode = stackGraphs.get(stack); + const depNode = stackGraphs.get(dep); + if (!stackNode) { + throw new Error(`cannot find node for ${stack.stackName}`); + } + if (!depNode) { + throw new Error(`cannot find node for ${dep.stackName}`); + } + stackNode.dependOn(depNode); + } + } + + this.addPrePost(stage.pre, stage.post, retGraph); + + return retGraph; + } + + private addPrePost(pre: Step[], post: Step[], parent: AGraph) { + const currentNodes = new GraphNodeCollection(parent.nodes); + for (const p of pre) { + const preNode = this.addAndRecurse(p, parent); + currentNodes.dependOn(preNode); + } + for (const p of post) { + const postNode = this.addAndRecurse(p, parent); + postNode?.dependOn(...currentNodes.nodes); + } + } + + private topLevelGraph(name: string): AGraph { + let ret = this.graph.tryGetChild(name); + if (!ret) { + ret = new Graph(name); + this.graph.add(ret); + } + return ret as AGraph; + } + + private addAndRecurse(step: Step, parent: AGraph) { + if (step === PipelineGraph.NO_STEP) { return undefined; } + + const previous = this.added.get(step); + if (previous) { return previous; } + + const node: AGraphNode = GraphNode.of(step.id, { type: 'step', step }); + + // If the step is a source step, change the parent to a special "Source" stage + // (CodePipeline wants it that way) + if (step.isSource) { + parent = this.topLevelGraph('Source'); + } + + parent.add(node); + this.added.set(step, node); + + for (const dep of step.dependencies) { + const producerNode = this.addAndRecurse(dep, parent); + node.dependOn(producerNode); + } + + // Add stack dependencies (by use of the dependencybuilder this also works + // if we encounter the Step before the Stack has been properly added yet) + if (step instanceof ShellStep) { + for (const output of Object.values(step.envFromCfnOutputs)) { + const stack = this.queries.producingStack(output); + this.stackOutputDependencies.get(stack).dependBy(node); + } + } + + return node; + } + + private publishAsset(stackAsset: StackAsset): AGraphNode { + const assetsGraph = this.topLevelGraph('Assets'); + + let assetNode = this.assetNodes.get(stackAsset.assetId); + if (assetNode) { + // If there's already a node pubishing this asset, add as a new publishing + // destination to the same node. + } else if (this.singlePublisher && this.assetNodesByType.has(stackAsset.assetType)) { + // If we're doing a single node per type, lookup by that + assetNode = this.assetNodesByType.get(stackAsset.assetType)!; + } else { + // Otherwise add a new one + const id = stackAsset.assetType === AssetType.FILE + ? (this.singlePublisher ? 'FileAsset' : `FileAsset${++this._fileAssetCtr}`) + : (this.singlePublisher ? 'DockerAsset' : `DockerAsset${++this._dockerAssetCtr}`); + + assetNode = GraphNode.of(id, { type: 'publish-assets', assets: [] }); + assetsGraph.add(assetNode); + assetNode.dependOn(this.lastPreparationNode); + + this.assetNodesByType.set(stackAsset.assetType, assetNode); + this.assetNodes.set(stackAsset.assetId, assetNode); + } + + const data = assetNode.data; + if (data?.type !== 'publish-assets') { + throw new Error(`${assetNode} has the wrong data.type: ${data?.type}`); + } + if (!data.assets.some(a => a.assetSelector === stackAsset.assetSelector)) { + data.assets.push(stackAsset); + } + + return assetNode; + } + + /** + * Simplify the stack name by removing the `Stage-` prefix if it exists. + */ + private simpleStackName(stackName: string, stageName: string) { + return stripPrefix(stackName, `${stageName}-`); + } +} + +type GraphAnnotation = + { readonly type: 'group' } + | { readonly type: 'stack-group'; readonly stack: StackDeployment } + | { readonly type: 'publish-assets'; readonly assets: StackAsset[] } + | { readonly type: 'step'; readonly step: Step; isBuildStep?: boolean } + | { readonly type: 'self-update' } + | { readonly type: 'prepare'; readonly stack: StackDeployment } + | { readonly type: 'execute'; readonly stack: StackDeployment; readonly captureOutputs: boolean } + ; + +// Type aliases for the graph nodes tagged with our specific annotation type +// (to save on generics in the code above). +export type AGraphNode = GraphNode; +export type AGraph = Graph; + +function stripPrefix(s: string, prefix: string) { + return s.startsWith(prefix) ? s.substr(prefix.length) : s; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-queries.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-queries.ts new file mode 100644 index 0000000000000..d3306e4e0a934 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-queries.ts @@ -0,0 +1,67 @@ +import { Step, ShellStep, StackOutputReference, StackDeployment, StackAsset, StageDeployment } from '../blueprint'; +import { PipelineBase } from '../main/pipeline-base'; + +/** + * Answer some questions about a pipeline blueprint + */ +export class PipelineQueries { + constructor(private readonly pipeline: PipelineBase) { + } + + /** + * Return the names of all outputs for the given stack that are referenced in this blueprint + */ + public stackOutputsReferenced(stack: StackDeployment): string[] { + const steps = new Array(); + for (const wave of this.pipeline.waves) { + steps.push(...wave.pre, ...wave.post); + for (const stage of wave.stages) { + steps.push(...stage.pre, ...stage.post); + } + } + + const ret = new Array(); + for (const step of steps) { + if (!(step instanceof ShellStep)) { continue; } + + for (const outputRef of Object.values(step.envFromCfnOutputs)) { + if (outputRef.isProducedBy(stack)) { + ret.push(outputRef.outputName); + } + } + } + return ret; + } + + /** + * Find the stack deployment that is producing the given reference + */ + public producingStack(outputReference: StackOutputReference): StackDeployment { + for (const wave of this.pipeline.waves) { + for (const stage of wave.stages) { + for (const stack of stage.stacks) { + if (outputReference.isProducedBy(stack)) { + return stack; + } + } + } + } + + throw new Error(`Stack '${outputReference.stackDescription}' (producing output '${outputReference.outputName}') is not in the pipeline; call 'addStage()' to add the stack's Stage to the pipeline`); + } + + /** + * All assets referenced in all the Stacks of a StageDeployment + */ + public assetsInStage(stage: StageDeployment): StackAsset[] { + const assets = new Map(); + + for (const stack of stage.stacks) { + for (const asset of stack.assets) { + assets.set(asset.assetSelector, asset); + } + } + + return Array.from(assets.values()); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts new file mode 100644 index 0000000000000..eb5e0cc3483aa --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/toposort.ts @@ -0,0 +1,67 @@ +import { GraphNode } from './graph'; + +export function printDependencyMap(dependencies: Map, Set>>) { + const lines = ['---']; + for (const [k, vs] of dependencies.entries()) { + lines.push(`${k} -> ${Array.from(vs)}`); + } + // eslint-disable-next-line no-console + console.log(lines.join('\n')); +} + +export function topoSort(nodes: Set>, dependencies: Map, Set>>): GraphNode[][] { + const remaining = new Set>(nodes); + + const ret: GraphNode[][] = []; + while (remaining.size > 0) { + // All elements with no more deps in the set can be ordered + const selectable = Array.from(remaining.values()).filter(e => { + if (!dependencies.has(e)) { + throw new Error(`No key for ${e}`); + } + return dependencies.get(e)!.size === 0; + }); + selectable.sort((a, b) => a.id < b.id ? -1 : b.id < a.id ? 1 : 0); + + // If we didn't make any progress, we got stuck + if (selectable.length === 0) { + const cycle = findCycle(dependencies); + throw new Error(`Dependency cycle in graph: ${cycle.map(n => n.id).join(' => ')}`); + } + + ret.push(selectable); + + for (const selected of selectable) { + remaining.delete(selected); + for (const depSet of dependencies.values()) { + depSet.delete(selected); + } + } + } + + return ret; +} + +/** + * Find cycles in a graph + * + * Not the fastest, but effective and should be rare + */ +function findCycle(deps: Map, Set>>): GraphNode[] { + for (const node of deps.keys()) { + const cycle = recurse(node, [node]); + if (cycle) { return cycle; } + } + throw new Error('No cycle found. Assertion failure!'); + + function recurse(node: GraphNode, path: GraphNode[]): GraphNode[] | undefined { + for (const dep of deps.get(node) ?? []) { + if (dep === path[0]) { return [...path, dep]; } + + const cycle = recurse(dep, [...path, dep]); + if (cycle) { return cycle; } + } + + return undefined; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/index.ts b/packages/@aws-cdk/pipelines/lib/index.ts index 2e63ee1d083a9..5f469e9fd5ce6 100644 --- a/packages/@aws-cdk/pipelines/lib/index.ts +++ b/packages/@aws-cdk/pipelines/lib/index.ts @@ -1,6 +1,5 @@ -export * from './pipeline'; -export * from './stage'; -export * from './synths'; -export * from './actions'; -export * from './docker-credentials'; -export * from './validation'; +export * from './legacy'; +export * from './blueprint'; +export * from './codepipeline'; +export * from './main'; +export * from './docker-credentials'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts similarity index 98% rename from packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts index 095ed581302aa..af6b7821a308d 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/deploy-cdk-stack-action.ts @@ -7,8 +7,8 @@ import * as iam from '@aws-cdk/aws-iam'; import { Aws, CfnCapabilities, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct, Node } from 'constructs'; -import { appOf, assemblyBuilderOf } from '../private/construct-internals'; -import { toPosixPath } from '../private/fs'; +import { appOf, assemblyBuilderOf } from '../../private/construct-internals'; +import { toPosixPath } from '../../private/fs'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line diff --git a/packages/@aws-cdk/pipelines/lib/actions/index.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/index.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/actions/index.ts rename to packages/@aws-cdk/pipelines/lib/legacy/actions/index.ts diff --git a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts similarity index 96% rename from packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts index a9661da20a9c2..72f2924e1690a 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts @@ -8,27 +8,13 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { IDependable, ISynthesisSession, Lazy, Stack, attachCustomSynthesis } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { toPosixPath } from '../private/fs'; +import { AssetType } from '../../blueprint/asset-type'; +import { toPosixPath } from '../../private/fs'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; -/** - * Type of the asset that is being published - */ -export enum AssetType { - /** - * A file - */ - FILE = 'file', - - /** - * A Docker image - */ - DOCKER_IMAGE = 'docker-image', -} - /** * Props for a PublishAssetsAction */ diff --git a/packages/@aws-cdk/pipelines/lib/actions/update-pipeline-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts similarity index 97% rename from packages/@aws-cdk/pipelines/lib/actions/update-pipeline-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts index 42b3c51c1da3a..cc866c20e51d8 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/update-pipeline-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts @@ -5,8 +5,8 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; -import { embeddedAsmPath } from '../private/construct-internals'; +import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../../docker-credentials'; +import { embeddedAsmPath } from '../../private/construct-internals'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line diff --git a/packages/@aws-cdk/pipelines/lib/legacy/index.ts b/packages/@aws-cdk/pipelines/lib/legacy/index.ts new file mode 100644 index 0000000000000..ca2b108fcb0d8 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/legacy/index.ts @@ -0,0 +1,5 @@ +export * from './pipeline'; +export * from './stage'; +export * from './synths'; +export * from './actions'; +export * from './validation'; diff --git a/packages/@aws-cdk/pipelines/lib/pipeline.ts b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts similarity index 98% rename from packages/@aws-cdk/pipelines/lib/pipeline.ts rename to packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts index a3084d9f489d7..95a828b981ea1 100644 --- a/packages/@aws-cdk/pipelines/lib/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts @@ -4,9 +4,10 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { Annotations, App, Aws, CfnOutput, Fn, Lazy, PhysicalName, Stack, Stage } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AssetType, DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; -import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from './docker-credentials'; -import { appOf, assemblyBuilderOf } from './private/construct-internals'; +import { AssetType } from '../blueprint/asset-type'; +import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; +import { appOf, assemblyBuilderOf } from '../private/construct-internals'; +import { DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from './stage'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. diff --git a/packages/@aws-cdk/pipelines/lib/stage.ts b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts similarity index 98% rename from packages/@aws-cdk/pipelines/lib/stage.ts rename to packages/@aws-cdk/pipelines/lib/legacy/stage.ts index 4d5eda62762d3..55c847d984a58 100644 --- a/packages/@aws-cdk/pipelines/lib/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts @@ -3,9 +3,10 @@ import * as cpactions from '@aws-cdk/aws-codepipeline-actions'; import { Stage, Aspects } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { AssetType, DeployCdkStackAction } from './actions'; -import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from './private/asset-manifest'; -import { topologicalSort } from './private/toposort'; +import { AssetType } from '../blueprint/asset-type'; +import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; +import { topologicalSort } from '../private/toposort'; +import { DeployCdkStackAction } from './actions'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line diff --git a/packages/@aws-cdk/pipelines/lib/synths/_util.ts b/packages/@aws-cdk/pipelines/lib/legacy/synths/_util.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/synths/_util.ts rename to packages/@aws-cdk/pipelines/lib/legacy/synths/_util.ts diff --git a/packages/@aws-cdk/pipelines/lib/synths/index.ts b/packages/@aws-cdk/pipelines/lib/legacy/synths/index.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/synths/index.ts rename to packages/@aws-cdk/pipelines/lib/legacy/synths/index.ts diff --git a/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts similarity index 99% rename from packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts index 8380a9c859698..b0fe2bcd466fb 100644 --- a/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts @@ -7,8 +7,8 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; -import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; -import { toPosixPath } from '../private/fs'; +import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../../docker-credentials'; +import { toPosixPath } from '../../private/fs'; import { copyEnvironmentVariables, filterEmpty } from './_util'; const DEFAULT_OUTPUT_DIR = 'cdk.out'; diff --git a/packages/@aws-cdk/pipelines/lib/validation/_files.ts b/packages/@aws-cdk/pipelines/lib/legacy/validation/_files.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/validation/_files.ts rename to packages/@aws-cdk/pipelines/lib/legacy/validation/_files.ts diff --git a/packages/@aws-cdk/pipelines/lib/validation/index.ts b/packages/@aws-cdk/pipelines/lib/legacy/validation/index.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/validation/index.ts rename to packages/@aws-cdk/pipelines/lib/legacy/validation/index.ts diff --git a/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/validation/shell-script-action.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts rename to packages/@aws-cdk/pipelines/lib/legacy/validation/shell-script-action.ts diff --git a/packages/@aws-cdk/pipelines/lib/main/index.ts b/packages/@aws-cdk/pipelines/lib/main/index.ts new file mode 100644 index 0000000000000..af40f3df33635 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/main/index.ts @@ -0,0 +1 @@ +export * from './pipeline-base'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts new file mode 100644 index 0000000000000..563697746a8cf --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts @@ -0,0 +1,132 @@ +import { Aspects, Stage } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { AddStageOpts as StageOptions, WaveOptions, Wave, IFileSetProducer, ShellStep } from '../blueprint'; + +// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. +// eslint-disable-next-line +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properties for a `Pipeline` + */ +export interface PipelineBaseProps { + /** + * The build step that produces the CDK Cloud Assembly + * + * The primary output of this step needs to be the `cdk.out` directory + * generated by the `cdk synth` command. + * + * If you use a `ShellStep` here and you don't configure an output directory, + * the output directory will automatically be assumed to be `cdk.out`. + */ + readonly synth: IFileSetProducer; +} + +/** + * A generic CDK Pipelines pipeline + * + * Different deployment systems will provide subclasses of `Pipeline` that generate + * the deployment infrastructure necessary to deploy CDK apps, specific to that system. + * + * This library comes with the `CodePipeline` class, which uses AWS CodePipeline + * to deploy CDK apps. + * + * The actual pipeline infrastructure is constructed (by invoking the engine) + * when `buildPipeline()` is called, or when `app.synth()` is called (whichever + * happens first). + */ +export abstract class PipelineBase extends CoreConstruct { + /** + * The build step that produces the CDK Cloud Assembly + */ + public readonly synth: IFileSetProducer; + + /** + * The waves in this pipeline + */ + public readonly waves: Wave[]; + + private built = false; + + constructor(scope: Construct, id: string, props: PipelineBaseProps) { + super(scope, id); + + if (props.synth instanceof ShellStep && !props.synth.primaryOutput) { + props.synth.primaryOutputDirectory('cdk.out'); + } + + this.synth = props.synth; + this.waves = []; + + if (!props.synth.primaryOutput) { + throw new Error(`synthStep ${props.synth} must produce a primary output, but is not producing anything. Configure the Step differently or use a different Step type.`); + } + + Aspects.of(this).add({ visit: () => this.buildJustInTime() }); + } + + /** + * Deploy a single Stage by itself + * + * Add a Stage to the pipeline, to be deployed in sequence with other + * Stages added to the pipeline. All Stacks in the stage will be deployed + * in an order automatically determined by their relative dependencies. + */ + public addStage(stage: Stage, options?: StageOptions) { + if (this.built) { + throw new Error('addStage: can\'t add Stages anymore after buildPipeline() has been called'); + } + + return this.addWave(stage.stageName).addStage(stage, options); + } + + /** + * Add a Wave to the pipeline, for deploying multiple Stages in parallel + * + * Use the return object of this method to deploy multiple stages in parallel. + * + * Example: + * + * ```ts + * const wave = pipeline.addWave('MyWave'); + * wave.addStage(new MyStage('Stage1', ...)); + * wave.addStage(new MyStage('Stage2', ...)); + * ``` + */ + public addWave(id: string, options?: WaveOptions) { + if (this.built) { + throw new Error('addWave: can\'t add Waves anymore after buildPipeline() has been called'); + } + + const wave = new Wave(id, options); + this.waves.push(wave); + return wave; + } + + /** + * Send the current pipeline definition to the engine, and construct the pipeline + * + * It is not possible to modify the pipeline after calling this method. + */ + public buildPipeline() { + if (this.built) { + throw new Error('build() has already been called: can only call it once'); + } + this.doBuildPipeline(); + this.built = true; + } + + /** + * Implemented by subclasses to do the actual pipeline construction + */ + protected abstract doBuildPipeline(): void; + + /** + * Automatically call 'build()' just before synthesis if the user hasn't explicitly called it yet + */ + private buildJustInTime() { + if (!this.built) { + this.buildPipeline(); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/cloud-assembly-internals.ts b/packages/@aws-cdk/pipelines/lib/private/cloud-assembly-internals.ts new file mode 100644 index 0000000000000..114ec2e228fbf --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/cloud-assembly-internals.ts @@ -0,0 +1,13 @@ +import * as cxapi from '@aws-cdk/cx-api'; + +export function isAssetManifest(s: cxapi.CloudArtifact): s is cxapi.AssetManifestArtifact { + // instanceof is too risky, and we're at a too late stage to properly fix. + // return s instanceof cxapi.AssetManifestArtifact; + return s.constructor.name === 'AssetManifestArtifact'; +} + +export function isStackArtifact(a: cxapi.CloudArtifact): a is cxapi.CloudFormationStackArtifact { + // instanceof is too risky, and we're at a too late stage to properly fix. + // return a instanceof cxapi.CloudFormationStackArtifact; + return a.constructor.name === 'CloudFormationStackArtifact'; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts b/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts index 496d33c5a1f7c..fe2ddf1953f64 100644 --- a/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts +++ b/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts @@ -4,7 +4,10 @@ import * as path from 'path'; import { App, Stage } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { IConstruct, Node } from 'constructs'; +import { Construct, IConstruct, Node } from 'constructs'; + +// eslint-disable-next-line no-duplicate-imports,import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; export function appOf(construct: IConstruct): App { const root = Node.of(construct).root; @@ -35,4 +38,12 @@ export function embeddedAsmPath(scope: IConstruct) { */ export function cloudAssemblyBuildSpecDir(scope: IConstruct) { return assemblyBuilderOf(appOf(scope)).outdir; +} + +export function obtainScope(parent: Construct, id: string): Construct { + const existing = Node.of(parent).tryFindChild(id); + if (existing) { + return existing as Construct; + } + return new CoreConstruct(parent, id); } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/javascript.ts b/packages/@aws-cdk/pipelines/lib/private/javascript.ts new file mode 100644 index 0000000000000..bed84e5eb3932 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/javascript.ts @@ -0,0 +1,90 @@ +export function addAll(into: Set, from: Iterable) { + for (const x of from) { + into.add(x); + } +} + +export function extract(from: Map, key: A): B | undefined { + const ret = from.get(key); + from.delete(key); + return ret; +} + +export function* flatMap(xs: Iterable, fn: (x: A) => Iterable): IterableIterator { + for (const x of xs) { + for (const y of fn(x)) { + yield y; + } + } +} + +export function* enumerate(xs: Iterable): IterableIterator<[number, A]> { + let i = 0; + for (const x of xs) { + yield [i++, x]; + } +} + + +export function expectProp(obj: A, key: B): NonNullable { + if (!obj[key]) { throw new Error(`Expecting '${key}' to be set!`); } + return obj[key] as any; +} + +export function* flatten(xs: Iterable): IterableIterator { + for (const x of xs) { + for (const y of x) { + yield y; + } + } +} + +export function filterEmpty(xs: Array): string[] { + return xs.filter(x => x) as any; +} + +export function mapValues(xs: Record, fn: (x: A) => B): Record { + const ret: Record = {}; + for (const [k, v] of Object.entries(xs)) { + ret[k] = fn(v); + } + return ret; +} + +export function mkdict(xs: Array): Record { + const ret: Record = {}; + for (const [k, v] of xs) { + ret[k] = v; + } + return ret; +} + +export function noEmptyObject(xs: Record): Record | undefined { + if (Object.keys(xs).length === 0) { return undefined; } + return xs; +} + +export function noUndefined(xs: Record): Record> { + return mkdict(Object.entries(xs).filter(([_, v]) => isDefined(v))) as any; +} + +export function maybeSuffix(x: string | undefined, suffix: string): string | undefined { + if (x === undefined) { return undefined; } + return `${x}${suffix}`; +} + +/** + * Partition a collection by dividing it into two collections, one that matches the predicate and one that don't + */ +export function partition(xs: T[], pred: (x: T) => boolean): [T[], T[]] { + const yes: T[] = []; + const no: T[] = []; + for (const x of xs) { + (pred(x) ? yes : no).push(x); + } + return [yes, no]; +} + +export function isDefined(x: A): x is NonNullable { + return x !== undefined; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/template-configuration.ts b/packages/@aws-cdk/pipelines/lib/private/template-configuration.ts new file mode 100644 index 0000000000000..bc78424ce22a4 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/template-configuration.ts @@ -0,0 +1,21 @@ +import * as fs from 'fs'; + +/** + * Write template configuration to the given file + */ +export function writeTemplateConfiguration(filename: string, config: TemplateConfiguration) { + fs.writeFileSync(filename, JSON.stringify(config, undefined, 2), { encoding: 'utf-8' }); +} + +/** + * Template configuration in a CodePipeline + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-cfn-artifacts.html#w2ab1c13c17c15 + */ +export interface TemplateConfiguration { + readonly Parameters?: Record; + readonly Tags?: Record; + readonly StackPolicy?: { + readonly Statements: Array>; + }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 4963206e1d21f..1db513581a96e 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -38,12 +38,15 @@ "cfn2ts": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, "peerDependencies": { "constructs": "^3.3.69", "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", @@ -53,12 +56,14 @@ "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/cx-api": "0.0.0" }, "dependencies": { "constructs": "^3.3.69", "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", @@ -68,6 +73,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/cx-api": "0.0.0" }, diff --git a/packages/@aws-cdk/pipelines/test/actions/update-pipeline-action.test.ts b/packages/@aws-cdk/pipelines/test/actions/update-pipeline-action.test.ts deleted file mode 100644 index d47167d6a7e6e..0000000000000 --- a/packages/@aws-cdk/pipelines/test/actions/update-pipeline-action.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - arrayWith, -} from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as cp from '@aws-cdk/aws-codepipeline'; -import { Stack } from '@aws-cdk/core'; -import * as cdkp from '../../lib'; -import { behavior } from '../helpers/compliance'; -import { TestApp } from '../testutil'; - -let app: TestApp; -let pipelineStack: Stack; - -behavior('self-update project role has proper permissions', (suite) => { - suite.legacy(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack'); - - new cdkp.UpdatePipelineAction(pipelineStack, 'Update', { - cloudAssemblyInput: new cp.Artifact(), - pipelineStackHierarchicalId: pipelineStack.node.path, - projectName: 'pipeline-selfupdate', - }); - - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith( - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Resource: { 'Fn::Join': ['', ['arn:*:iam::', { Ref: 'AWS::AccountId' }, ':role/*']] }, - Condition: { - 'ForAnyValue:StringEquals': { - 'iam:ResourceTag/aws-cdk:bootstrap-role': ['image-publishing', 'file-publishing', 'deploy'], - }, - }, - }, - { - Action: 'cloudformation:DescribeStacks', - Effect: 'Allow', - Resource: '*', - }, - { - Action: 's3:ListBucket', - Effect: 'Allow', - Resource: '*', - }, - ), - }, - }); - }); -}); diff --git a/packages/@aws-cdk/pipelines/test/blueprint/fixtures/file-asset1.txt b/packages/@aws-cdk/pipelines/test/blueprint/fixtures/file-asset1.txt new file mode 100644 index 0000000000000..6765125a23c6b --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/fixtures/file-asset1.txt @@ -0,0 +1 @@ +Hello, file! \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts new file mode 100644 index 0000000000000..f577ffae4f80c --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/dependencies.test.ts @@ -0,0 +1,52 @@ +import { GraphNode } from '../../../lib/helpers-internal'; +import { mkGraph, nodeNames } from './util'; + +describe('with nested graphs', () => { + const graph = mkGraph('G', G => { + let aa: GraphNode; + + const A = G.graph('A', [], GA => { + aa = GA.node('aa'); + }); + + // B -> A, (same-level dependency) + G.graph('B', [A], B => { + // bbb -> bb + const bb = B.node('bb'); + B.node('bbb', [bb]); + }); + + // cc -> aa (cross-subgraph dependency) + G.graph('C', [], C => { + C.node('cc', [aa]); + }); + + // D -> aa (down-dependency) + G.graph('D', [aa!], C => { + C.node('dd', [aa]); + }); + + // ee -> A (up-dependency) + G.graph('E', [], C => { + C.node('ee', [A]); + }); + }); + + test('can get up-projected dependency list from graph', () => { + const sorted = graph.sortedChildren(); + + expect(nodeNames(sorted)).toEqual([ + ['A'], + ['B', 'C', 'D', 'E'], + ]); + }); + + test('can get down-projected dependency list from graph', () => { + const sorted = graph.sortedLeaves(); + expect(nodeNames(sorted)).toEqual([ + ['aa'], + ['bb', 'cc', 'dd', 'ee'], + ['bbb'], + ]); + }); +}); diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/graph.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/graph.test.ts new file mode 100644 index 0000000000000..30a022e347932 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/graph.test.ts @@ -0,0 +1,40 @@ +import { GraphNode } from '../../../lib/helpers-internal'; +import { flatten } from '../../../lib/private/javascript'; +import { mkGraph } from './util'; + + +test('"uniqueId" renders a graph-wide unique id for each node', () => { + const g = mkGraph('MyGraph', G => { + G.graph('g1', [], G1 => { + G1.node('n1'); + G1.node('n2'); + G1.graph('g2', [], G2 => { + G2.node('n3'); + }); + }); + G.node('n4'); + }); + + expect(Array.from(flatten(g.sortedLeaves())).map(n => n.uniqueId)).toStrictEqual([ + 'g1-n1', + 'g1-n2', + 'g1-g2-n3', + 'n4', + ]); +}); + +test('"allDeps" combines node deps and parent deps', () => { + let n4: any; + mkGraph('MyGraph', G => { + G.graph('g1', [], G1 => { + G1.node('n1'); + const n2 = G1.node('n2'); + G1.graph('g2', [n2], G2 => { + const n3 = G2.node('n3'); + n4 = G2.node('n4', [n3]); + }); + }); + }); + + expect((n4 as GraphNode).allDeps.map(x => x.uniqueId)).toStrictEqual(['g1-g2-n3', 'g1-n2']); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts new file mode 100644 index 0000000000000..3b28d4f410a61 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts @@ -0,0 +1,264 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import '@aws-cdk/assert-internal/jest'; +import * as cdkp from '../../../lib'; +import { Graph, GraphNode, PipelineGraph } from '../../../lib/helpers-internal'; +import { flatten } from '../../../lib/private/javascript'; +import { AppWithOutput, OneStackApp, TestApp } from '../../testhelpers/test-app'; + +let app: TestApp; + +beforeEach(() => { + app = new TestApp(); +}); + +afterEach(() => { + app.cleanup(); +}); + +describe('blueprint with one stage', () => { + let blueprint: Blueprint; + beforeEach(() => { + blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['build'], + }), + }); + blueprint.addStage(new OneStackApp(app, 'CrossAccount', { env: { account: 'you' } })); + }); + + test('simple app gets graphed correctly', () => { + // WHEN + const graph = new PipelineGraph(blueprint).graph; + + // THEN + expect(childrenAt(graph)).toEqual([ + 'Source', + 'Build', + 'CrossAccount', + ]); + + expect(childrenAt(graph, 'CrossAccount')).toEqual([ + 'Stack', + ]); + + expect(childrenAt(graph, 'CrossAccount', 'Stack')).toEqual([ + 'Prepare', + 'Deploy', + ]); + }); + + test('self mutation gets inserted at the right place', () => { + // WHEN + const graph = new PipelineGraph(blueprint, { selfMutation: true }).graph; + + // THEN + expect(childrenAt(graph)).toEqual([ + 'Source', + 'Build', + 'UpdatePipeline', + 'CrossAccount', + ]); + + expect(childrenAt(graph, 'UpdatePipeline')).toEqual([ + 'SelfMutate', + ]); + }); +}); + +describe('blueprint with wave and stage', () => { + let blueprint: Blueprint; + beforeEach(() => { + blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['build'], + }), + }); + + const wave = blueprint.addWave('Wave'); + wave.addStage(new OneStackApp(app, 'Alpha')); + wave.addStage(new OneStackApp(app, 'Beta')); + }); + + test('post-action gets added inside stage graph', () => { + // GIVEN + blueprint.waves[0].stages[0].addPost(new cdkp.ManualApprovalStep('Approve')); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + + // THEN + expect(childrenAt(graph, 'Wave')).toEqual([ + 'Alpha', + 'Beta', + ]); + + expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([ + 'Stack', + 'Approve', + ]); + }); + + test('pre-action gets added inside stage graph', () => { + // GIVEN + blueprint.waves[0].stages[0].addPre(new cdkp.ManualApprovalStep('Gogogo')); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + + // THEN + expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([ + 'Gogogo', + 'Stack', + ]); + }); +}); + +describe('options for other engines', () => { + test('"publishTemplate" will add steps to publish CFN templates as assets', () => { + // GIVEN + const blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + commands: ['build'], + }), + }); + blueprint.addStage(new OneStackApp(app, 'Alpha')); + + // WHEN + const graph = new PipelineGraph(blueprint, { + publishTemplate: true, + }); + + // THEN + expect(childrenAt(graph.graph, 'Assets')).toStrictEqual(['FileAsset1']); + }); + + test('"prepareStep: false" can be used to disable the "prepare" step for stack deployments', () => { + // GIVEN + const blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + commands: ['build'], + }), + }); + blueprint.addStage(new OneStackApp(app, 'Alpha')); + + // WHEN + const graph = new PipelineGraph(blueprint, { + prepareStep: false, + }); + + // THEN + // if "prepareStep" was true (default), the "Stack" node would have "Prepare" and "Deploy" + // since "prepareStep" is false, it only has "Deploy". + expect(childrenAt(graph.graph, 'Alpha', 'Stack')).toStrictEqual(['Deploy']); + }); +}); + + +describe('with app with output', () => { + let blueprint: Blueprint; + let myApp: AppWithOutput; + let scriptStep: cdkp.ShellStep; + beforeEach(() => { + blueprint = new Blueprint(app, 'Bp', { + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['build'], + }), + }); + + myApp = new AppWithOutput(app, 'Alpha'); + scriptStep = new cdkp.ShellStep('PrintBucketName', { + envFromCfnOutputs: { + BUCKET_NAME: myApp.theOutput, + }, + commands: ['echo $BUCKET_NAME'], + }); + }); + + test('post-action using stack output has dependency on execute node', () => { + // GIVEN + blueprint.addStage(myApp, { + post: [scriptStep], + }); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + + // THEN + expect(childrenAt(graph, 'Alpha')).toEqual([ + 'Stack', + 'PrintBucketName', + ]); + + expect(nodeAt(graph, 'Alpha', 'PrintBucketName').dependencies).toContain( + nodeAt(graph, 'Alpha', 'Stack', 'Deploy')); + }); + + test('pre-action cannot use stack output', () => { + // GIVEN + blueprint.addStage(myApp, { + pre: [scriptStep], + }); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + expect(() => { + assertGraph(nodeAt(graph, 'Alpha')).sortedLeaves(); + }).toThrow(/Dependency cycle/); + }); + + test('cannot use output from stack not in the pipeline', () => { + // GIVEN + blueprint.addStage(new AppWithOutput(app, 'OtherApp'), { + pre: [scriptStep], + }); + + // WHEN + expect(() => { + new PipelineGraph(blueprint).graph; + }).toThrow(/is not in the pipeline/); + }); +}); + +function childrenAt(g: Graph, ...descend: string[]) { + for (const d of descend) { + const child = g.tryGetChild(d); + if (!child) { + throw new Error(`No node named '${d}' in ${g}`); + } + g = assertGraph(child); + } + return childNames(g); +} + +function nodeAt(g: Graph, ...descend: string[]) { + for (const d of descend.slice(0, descend.length - 1)) { + const child = g.tryGetChild(d); + if (!child) { + throw new Error(`No node named '${d}' in ${g}`); + } + g = assertGraph(child); + } + const child = g.tryGetChild(descend[descend.length - 1]); + if (!child) { + throw new Error(`No node named '${descend[descend.length - 1]}' in ${g}`); + } + return child; +} + +function childNames(g: Graph) { + return Array.from(flatten(g.sortedChildren())).map(n => n.id); +} + +function assertGraph(g: GraphNode | undefined): Graph { + if (!g) { throw new Error('Expected a graph node, got undefined'); } + if (!(g instanceof Graph)) { throw new Error(`Expected a Graph, got: ${g}`); } + return g; +} + +class Blueprint extends cdkp.PipelineBase { + protected doBuildPipeline(): void { + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/util.ts b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/util.ts new file mode 100644 index 0000000000000..61e899aef71ce --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/util.ts @@ -0,0 +1,38 @@ +import { Graph, GraphNode } from '../../../lib/helpers-internal'; + +class PlainNode extends GraphNode { } + +export function mkGraph(name: string, block: (b: GraphBuilder) => void) { + const graph = new Graph(name); + block({ + graph(name2, deps, block2) { + const innerG = mkGraph(name2, block2); + innerG.dependOn(...deps); + graph.add(innerG); + return innerG; + }, + node(name2, deps) { + const innerN = new PlainNode(name2); + innerN.dependOn(...deps ?? []); + graph.add(innerN); + return innerN; + }, + }); + return graph; +} + + +interface GraphBuilder { + graph(name: string, deps: GraphNode[], block: (b: GraphBuilder) => void): Graph; + node(name: string, deps?: GraphNode[]): GraphNode; +} + + +export function nodeNames(n: GraphNode): string; +export function nodeNames(ns: GraphNode[]): string[]; +export function nodeNames(ns: GraphNode[][]): string[][]; +export function nodeNames(n: any): any { + if (n instanceof GraphNode) { return n.id; } + if (Array.isArray(n)) { return n.map(nodeNames); } + throw new Error('oh no'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/logicalid-stability.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/logicalid-stability.test.ts new file mode 100644 index 0000000000000..319d25203c92b --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/logicalid-stability.test.ts @@ -0,0 +1,122 @@ +import '@aws-cdk/assert-internal/jest'; +import { Stack } from '@aws-cdk/core'; +import { mkdict } from '../../lib/private/javascript'; +import { PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, MegaAssetsApp, stackTemplate } from '../testhelpers'; + +let legacyApp: TestApp; +let modernApp: TestApp; + +let legacyPipelineStack: Stack; +let modernPipelineStack: Stack; + +beforeEach(() => { + legacyApp = new TestApp({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + 'aws:cdk:enable-path-metadata': true, + }, + }); + modernApp = new TestApp({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + 'aws:cdk:enable-path-metadata': true, + }, + }); + legacyPipelineStack = new Stack(legacyApp, 'PipelineStack', { env: PIPELINE_ENV }); + modernPipelineStack = new Stack(modernApp, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + legacyApp.cleanup(); + modernApp.cleanup(); +}); + +test('stateful or nameable resources have the same logicalID between old and new API', () => { + const legacyPipe = new LegacyTestGitHubNpmPipeline(legacyPipelineStack, 'Cdk'); + legacyPipe.addApplicationStage(new MegaAssetsApp(legacyPipelineStack, 'MyApp', { + numAssets: 2, + })); + + const modernPipe = new ModernTestGitHubNpmPipeline(modernPipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + modernPipe.addStage(new MegaAssetsApp(modernPipelineStack, 'MyApp', { + numAssets: 2, + })); + + const legacyTemplate = stackTemplate(legacyPipelineStack).template; + const modernTemplate = stackTemplate(modernPipelineStack).template; + + const legacyStateful = filterR(legacyTemplate.Resources, isStateful); + const modernStateful = filterR(modernTemplate.Resources, isStateful); + + expect(mapR(modernStateful, typeOfRes)).toEqual(mapR(legacyStateful, typeOfRes)); +}); + +test('nameable resources have the same names between old and new API', () => { + const legacyPipe = new LegacyTestGitHubNpmPipeline(legacyPipelineStack, 'Cdk', { + pipelineName: 'asdf', + }); + legacyPipe.addApplicationStage(new MegaAssetsApp(legacyPipelineStack, 'MyApp', { + numAssets: 2, + })); + + const modernPipe = new ModernTestGitHubNpmPipeline(modernPipelineStack, 'Cdk', { + pipelineName: 'asdf', + crossAccountKeys: true, + }); + modernPipe.addStage(new MegaAssetsApp(modernPipelineStack, 'MyApp', { + numAssets: 2, + })); + + const legacyTemplate = stackTemplate(legacyPipelineStack).template; + const modernTemplate = stackTemplate(modernPipelineStack).template; + + const legacyNamed = filterR(legacyTemplate.Resources, hasName); + const modernNamed = filterR(modernTemplate.Resources, hasName); + + expect(mapR(modernNamed, nameProps)).toEqual(mapR(legacyNamed, nameProps)); +}); + + +const STATEFUL_TYPES = [ + // Holds state + 'AWS::S3::Bucket', + 'AWS::KMS::Key', + 'AWS::KMS::Alias', + // Can be physical-named so will be impossible to replace + 'AWS::CodePipeline::Pipeline', + 'AWS::CodeBuild::Project', +]; + +function filterR(resources: Record, fn: (x: Resource) => boolean): Record { + return mkdict(Object.entries(resources).filter(([, resource]) => fn(resource))); +} + +function mapR(resources: Record, fn: (x: Resource) => A): Record { + return mkdict(Object.entries(resources).map(([lid, resource]) => [lid, fn(resource)] as const)); +} + +function typeOfRes(r: Resource) { + return r.Type; +} + +function isStateful(r: Resource) { + return STATEFUL_TYPES.includes(r.Type); +} + +function nameProps(r: Resource) { + return Object.entries(r.Properties).filter(([prop, _]) => + // Don't care about policy names + prop.endsWith('Name') && prop !== 'PolicyName'); +} + +function hasName(r: Resource) { + return nameProps(r).length > 0; +} + +interface Resource { + readonly Type: string; + readonly Properties: Record; + readonly Metadata?: Record; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/blueprint/stack-deployment.test.ts b/packages/@aws-cdk/pipelines/test/blueprint/stack-deployment.test.ts new file mode 100644 index 0000000000000..ee9d5b29240ce --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/blueprint/stack-deployment.test.ts @@ -0,0 +1,66 @@ +import * as path from 'path'; +import * as assets from '@aws-cdk/aws-s3-assets'; +import { Stack, Stage } from '@aws-cdk/core'; +import { StageDeployment } from '../../lib'; +import { TestApp } from '../testhelpers/test-app'; + +test('"templateAsset" represents the CFN template of the stack', () => { + // GIVEN + const stage = new Stage(new TestApp(), 'MyStage'); + new Stack(stage, 'MyStack'); + + // WHEN + const sd = StageDeployment.fromStage(stage); + + // THEN + expect(sd.stacks[0].templateAsset).not.toBeUndefined(); + expect(sd.stacks[0].templateAsset?.assetId).not.toBeUndefined(); + expect(sd.stacks[0].templateAsset?.assetManifestPath).not.toBeUndefined(); + expect(sd.stacks[0].templateAsset?.assetSelector).not.toBeUndefined(); + expect(sd.stacks[0].templateAsset?.assetType).toBe('file'); + expect(sd.stacks[0].templateAsset?.isTemplate).toBeTruthy(); +}); + +describe('templateUrl', () => { + test('includes the https:// s3 URL of the template file', () => { + // GIVEN + const stage = new Stage(new TestApp(), 'MyStage', { env: { account: '111', region: 'us-east-1' } }); + new Stack(stage, 'MyStack'); + + // WHEN + const sd = StageDeployment.fromStage(stage); + + // THEN + expect(sd.stacks[0].templateUrl).toBe('https://cdk-hnb659fds-assets-111-us-east-1.s3.us-east-1.amazonaws.com/4ef627170a212f66f5d1d9240d967ef306f4820ff9cb05b3a7ec703df6af6c3e.json'); + }); + + test('without region', () => { + // GIVEN + const stage = new Stage(new TestApp(), 'MyStage', { env: { account: '111' } }); + new Stack(stage, 'MyStack'); + + // WHEN + const sd = StageDeployment.fromStage(stage); + + // THEN + expect(sd.stacks[0].templateUrl).toBe('https://cdk-hnb659fds-assets-111-.s3.amazonaws.com/$%7BAWS::Region%7D/4ef627170a212f66f5d1d9240d967ef306f4820ff9cb05b3a7ec703df6af6c3e.json'); + }); + +}); + + +test('"requiredAssets" contain only assets that are not the template', () => { + // GIVEN + const stage = new Stage(new TestApp(), 'MyStage'); + const stack = new Stack(stage, 'MyStack'); + new assets.Asset(stack, 'Asset', { path: path.join(__dirname, 'fixtures') }); + + // WHEN + const sd = StageDeployment.fromStage(stage); + + // THEN + expect(sd.stacks[0].assets.length).toBe(1); + expect(sd.stacks[0].assets[0].assetType).toBe('file'); + expect(sd.stacks[0].assets[0].isTemplate).toBeFalsy(); +}); + diff --git a/packages/@aws-cdk/pipelines/test/build-role-policy-statements.test.ts b/packages/@aws-cdk/pipelines/test/build-role-policy-statements.test.ts deleted file mode 100644 index 01650e730d53e..0000000000000 --- a/packages/@aws-cdk/pipelines/test/build-role-policy-statements.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { arrayWith, deepObjectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import { PolicyStatement } from '@aws-cdk/aws-iam'; -import { Stack } from '@aws-cdk/core'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let sourceArtifact: codepipeline.Artifact; -let cloudAssemblyArtifact: codepipeline.Artifact; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStackPolicy', { env: PIPELINE_ENV }); - sourceArtifact = new codepipeline.Artifact(); - cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('Build project includes codeartifact policy statements for role', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - rolePolicyStatements: [ - new PolicyStatement({ - actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], - resources: ['arn:my:arn'], - }), - ], - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith(deepObjectLike({ - Action: [ - 'codeartifact:*', - 'sts:GetServiceBearerToken', - ], - Resource: 'arn:my:arn', - })), - }, - }); - }); -}); diff --git a/packages/@aws-cdk/pipelines/test/builds.test.ts b/packages/@aws-cdk/pipelines/test/builds.test.ts deleted file mode 100644 index 70c4d31a18907..0000000000000 --- a/packages/@aws-cdk/pipelines/test/builds.test.ts +++ /dev/null @@ -1,621 +0,0 @@ -import { arrayWith, deepObjectLike, encodedJson, objectLike, Capture } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as cbuild from '@aws-cdk/aws-codebuild'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecr from '@aws-cdk/aws-ecr'; -import * as s3 from '@aws-cdk/aws-s3'; -import { Stack } from '@aws-cdk/core'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let sourceArtifact: codepipeline.Artifact; -let cloudAssemblyArtifact: codepipeline.Artifact; - -beforeEach(() => { - app = new TestApp({ outdir: 'testcdk.out' }); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - sourceArtifact = new codepipeline.Artifact(); - cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('SimpleSynthAction takes arrays of commands', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: new cdkp.SimpleSynthAction({ - sourceArtifact, - cloudAssemblyArtifact, - installCommands: ['install1', 'install2'], - buildCommands: ['build1', 'build2'], - testCommands: ['test1', 'test2'], - synthCommand: 'cdk synth', - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: [ - 'install1', - 'install2', - ], - }, - build: { - commands: [ - 'build1', - 'build2', - 'test1', - 'test2', - 'cdk synth', - ], - }, - }, - })), - }, - }); - }); -}); - -behavior('%s build automatically determines artifact base-directory', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ sourceArtifact, cloudAssemblyArtifact }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - artifacts: { - 'base-directory': 'cdk.out', - }, - })), - }, - }); - }); -}); - -behavior('%s build respects subdirectory', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ - sourceArtifact, - cloudAssemblyArtifact, - subdirectory: 'subdir', - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: arrayWith('cd subdir'), - }, - }, - artifacts: { - 'base-directory': 'subdir/cdk.out', - }, - })), - }, - }); - }); -}); - -behavior('%s build sets UNSAFE_PERM=true', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ - sourceArtifact, - cloudAssemblyArtifact, - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - EnvironmentVariables: [ - { - Name: 'NPM_CONFIG_UNSAFE_PERM', - Type: 'PLAINTEXT', - Value: 'true', - }, - ], - }, - }); - }); -}); - -behavior('%s assumes no build step by default', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ sourceArtifact, cloudAssemblyArtifact }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: ['npx cdk synth'], - }, - }, - })), - }, - }); - }); -}); - -behavior('environmentVariables must be rendered in the action', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: new cdkp.SimpleSynthAction({ - sourceArtifact, - cloudAssemblyArtifact, - environmentVariables: { - VERSION: { value: codepipeline.GlobalVariables.executionId }, - }, - synthCommand: 'synth', - }), - }); - - // THEN - const theHash = Capture.aString(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Build', - Actions: [ - objectLike({ - Name: 'Synth', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ - { - name: 'VERSION', - type: 'PLAINTEXT', - value: '#{codepipeline.PipelineExecutionId}', - }, - { - name: '_PROJECT_CONFIG_HASH', - type: 'PLAINTEXT', - value: theHash.capture(), - }, - ]), - }), - }), - ], - }), - }); - }); -}); - -behavior('complex setup with environment variables still renders correct project', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: new cdkp.SimpleSynthAction({ - sourceArtifact, - cloudAssemblyArtifact, - environmentVariables: { - SOME_ENV_VAR: { value: 'SomeValue' }, - }, - environment: { - environmentVariables: { - INNER_VAR: { value: 'InnerValue' }, - }, - privileged: true, - }, - installCommands: [ - 'install1', - 'install2', - ], - synthCommand: 'synth', - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ - PrivilegedMode: true, - EnvironmentVariables: [ - { - Name: 'INNER_VAR', - Type: 'PLAINTEXT', - Value: 'InnerValue', - }, - ], - }), - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: ['install1', 'install2'], - }, - build: { - commands: ['synth'], - }, - }, - })), - }, - }); - }); -}); - -behavior('%s can have its install command overridden', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ - sourceArtifact, - cloudAssemblyArtifact, - installCommand: '/bin/true', - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: ['/bin/true'], - }, - }, - })), - }, - }); - }); -}); - -behavior('%s can have its test commands set', (suite) => { - suite.each(['npm', 'yarn']).legacy((npmYarn) => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: npmYarnBuild(npmYarn)({ - sourceArtifact, - cloudAssemblyArtifact, - installCommand: '/bin/true', - testCommands: ['echo "Running tests"'], - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(objectLike({ - phases: { - pre_build: { - commands: ['/bin/true'], - }, - build: { - commands: ['echo "Running tests"', 'npx cdk synth'], - }, - }, - })), - }, - }); - }); -}); - -behavior('Standard (NPM) synth can output additional artifacts', (suite) => { - suite.legacy(() => { - // WHEN - const addlArtifact = new codepipeline.Artifact('IntegTest'); - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - additionalArtifacts: [ - { - artifact: addlArtifact, - directory: 'test', - }, - ], - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - artifacts: { - 'secondary-artifacts': { - CloudAsm: { - 'base-directory': 'cdk.out', - 'files': '**/*', - }, - IntegTest: { - 'base-directory': 'test', - 'files': '**/*', - }, - }, - }, - })), - }, - }); - }); -}); - -behavior('Standard (NPM) synth can run in a VPC', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - vpc: new ec2.Vpc(pipelineStack, 'NpmSynthTestVpc'), - sourceArtifact, - cloudAssemblyArtifact, - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - VpcConfig: { - SecurityGroupIds: [ - { 'Fn::GetAtt': ['CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', 'GroupId'] }, - ], - Subnets: [ - { Ref: 'NpmSynthTestVpcPrivateSubnet1Subnet81E3AA56' }, - { Ref: 'NpmSynthTestVpcPrivateSubnet2SubnetC1CA3EF0' }, - { Ref: 'NpmSynthTestVpcPrivateSubnet3SubnetA04163EE' }, - ], - VpcId: { Ref: 'NpmSynthTestVpc5E703F25' }, - }, - }); - - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - Roles: [ - { Ref: 'CdkPipelineBuildSynthCdkBuildProjectRole5E173C62' }, - ], - PolicyDocument: { - Statement: arrayWith({ - Action: arrayWith('ec2:DescribeSecurityGroups'), - Effect: 'Allow', - Resource: '*', - }), - }, - }); - }); -}); - -behavior('Standard (Yarn) synth can run in a VPC', (suite) => { - suite.legacy(() => { - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardYarnSynth({ - vpc: new ec2.Vpc(pipelineStack, 'YarnSynthTestVpc'), - sourceArtifact, - cloudAssemblyArtifact, - }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - VpcConfig: { - SecurityGroupIds: [ - { - 'Fn::GetAtt': [ - 'CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', - 'GroupId', - ], - }, - ], - Subnets: [ - { - Ref: 'YarnSynthTestVpcPrivateSubnet1Subnet2805334B', - }, - { - Ref: 'YarnSynthTestVpcPrivateSubnet2SubnetDCFBF596', - }, - { - Ref: 'YarnSynthTestVpcPrivateSubnet3SubnetE11E0C86', - }, - ], - VpcId: { - Ref: 'YarnSynthTestVpc5F654735', - }, - }, - }); - }); -}); - -behavior('Pipeline action contains a hash that changes as the buildspec changes', (suite) => { - suite.legacy(() => { - const hash1 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - })); - - // To make sure the hash is not just random :) - const hash1prime = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - })); - - const hash2 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - installCommand: 'do install', - })); - const hash3 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - environment: { - computeType: cbuild.ComputeType.LARGE, - }, - })); - const hash4 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact: sa, - cloudAssemblyArtifact: cxa, - environment: { - environmentVariables: { - xyz: { value: 'SOME-VALUE' }, - }, - }, - })); - - expect(hash1).toEqual(hash1prime); - - expect(hash1).not.toEqual(hash2); - expect(hash1).not.toEqual(hash3); - expect(hash1).not.toEqual(hash4); - expect(hash2).not.toEqual(hash3); - expect(hash2).not.toEqual(hash4); - expect(hash3).not.toEqual(hash4); - - function synthWithAction(cb: (sourceArtifact: codepipeline.Artifact, cloudAssemblyArtifact: codepipeline.Artifact) => codepipeline.IAction) { - const _app = new TestApp({ outdir: 'testcdk.out' }); - const _pipelineStack = new Stack(_app, 'PipelineStack', { env: PIPELINE_ENV }); - const _sourceArtifact = new codepipeline.Artifact(); - const _cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); - - new TestGitHubNpmPipeline(_pipelineStack, 'Cdk', { - sourceArtifact: _sourceArtifact, - cloudAssemblyArtifact: _cloudAssemblyArtifact, - synthAction: cb(_sourceArtifact, _cloudAssemblyArtifact), - }); - - const theHash = Capture.aString(); - expect(_pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Build', - Actions: [ - objectLike({ - Name: 'Synth', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ - { - name: '_PROJECT_CONFIG_HASH', - type: 'PLAINTEXT', - value: theHash.capture(), - }, - ]), - }), - }), - ], - }), - }); - - return theHash.capturedValue; - } - }); -}); - -behavior('SimpleSynthAction is IGrantable', (suite) => { - suite.legacy(() => { - // GIVEN - const synthAction = cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - }); - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction, - }); - const bucket = new s3.Bucket(pipelineStack, 'Bucket'); - - // WHEN - bucket.grantRead(synthAction); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith(deepObjectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - })), - }, - }); - }); -}); - -behavior('SimpleSynthAction can reference an imported ECR repo', (suite) => { - suite.legacy(() => { - // Repro from https://github.com/aws/aws-cdk/issues/10535 - - // WHEN - new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - environment: { - buildImage: cbuild.LinuxBuildImage.fromEcrRepository( - ecr.Repository.fromRepositoryName(pipelineStack, 'ECRImage', 'my-repo-name'), - ), - }, - }), - }); - - // THEN -- no exception (necessary for linter) - expect(true).toBeTruthy(); - }); -}); - -function npmYarnBuild(npmYarn: string) { - if (npmYarn === 'npm') { return cdkp.SimpleSynthAction.standardNpmSynth; } - if (npmYarn === 'yarn') { return cdkp.SimpleSynthAction.standardYarnSynth; } - throw new Error(`Expecting npm|yarn: ${npmYarn}`); -} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts similarity index 56% rename from packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts rename to packages/@aws-cdk/pipelines/test/compliance/assets.test.ts index 55e45d1808476..0f71dbde34650 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts @@ -1,18 +1,11 @@ import * as fs from 'fs'; import * as path from 'path'; -import { arrayWith, deepObjectLike, encodedJson, notMatching, objectLike, ResourcePart, stringLike, SynthUtils } from '@aws-cdk/assert-internal'; +import { arrayWith, Capture, deepObjectLike, encodedJson, notMatching, objectLike, ResourcePart, stringLike, SynthUtils } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; import * as cb from '@aws-cdk/aws-codebuild'; -import * as cp from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; -import * as s3_assets from '@aws-cdk/aws-s3-assets'; -import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Stack, Stage, StageProps } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubAction, TestGitHubNpmPipeline } from './testutil'; +import { Stack } from '@aws-cdk/core'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, FileAssetApp, MegaAssetsApp, TwoFileAssetsApp, DockerAssetApp, PlainStackApp } from '../testhelpers'; const FILE_ASSET_SOURCE_HASH = '8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5'; const FILE_ASSET_SOURCE_HASH2 = 'ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e'; @@ -22,40 +15,58 @@ const IMAGE_PUBLISHING_ROLE = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role let app: TestApp; let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); afterEach(() => { app.cleanup(); }); describe('basic pipeline', () => { - beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); - }); - behavior('no assets stage if the application has no assets', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new PlainStackApp(app, 'App')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new PlainStackApp(app, 'App')); + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // THEN expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: notMatching(arrayWith(objectLike({ Name: 'Assets', }))), }); - }); + } }); describe('asset stage placement', () => { behavior('assets stage comes before any user-defined stages', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new FileAssetApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ objectLike({ Name: 'Source' }), @@ -65,52 +76,26 @@ describe('basic pipeline', () => { objectLike({ Name: 'App' }), ], }); - }); + } }); - behavior('assets stage inserted after existing pipeline actions', (suite) => { + behavior('up to 50 assets fit in a single stage', (suite) => { suite.legacy(() => { // WHEN - const sourceArtifact = new cp.Artifact(); - const cloudAssemblyArtifact = new cp.Artifact(); - const existingCodePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { - stages: [ - { - stageName: 'CustomSource', - actions: [new TestGitHubAction(sourceArtifact)], - }, - { - stageName: 'CustomBuild', - actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], - }, - ], - }); - pipeline = new cdkp.CdkPipeline(pipelineStack, 'CdkEmptyPipeline', { - cloudAssemblyArtifact: cloudAssemblyArtifact, - selfMutating: false, - codePipeline: existingCodePipeline, - // No source/build actions - }); - pipeline.addApplicationStage(new FileAssetApp(app, 'App')); + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 50 })); - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'CustomSource' }), - objectLike({ Name: 'CustomBuild' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'App' }), - ], - }); + THEN_codePipelineExpectation(); }); - }); - behavior('up to 50 assets fit in a single stage', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 50 })); + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new MegaAssetsApp(app, 'App', { numAssets: 50 })); + + THEN_codePipelineExpectation(); + }); - // THEN + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ objectLike({ Name: 'Source' }), @@ -120,55 +105,87 @@ describe('basic pipeline', () => { objectLike({ Name: 'App' }), ], }); - }); + } }); behavior('51 assets triggers a second stage', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 51 })); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new MegaAssetsApp(app, 'App', { numAssets: 51 })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ objectLike({ Name: 'Source' }), objectLike({ Name: 'Build' }), objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'Assets2' }), + objectLike({ Name: stringLike('Assets*') }), + objectLike({ Name: stringLike('Assets*2') }), objectLike({ Name: 'App' }), ], }); - }); + } }); behavior('101 assets triggers a third stage', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 101 })); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new MegaAssetsApp(app, 'App', { numAssets: 101 })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ objectLike({ Name: 'Source' }), objectLike({ Name: 'Build' }), objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'Assets2' }), - objectLike({ Name: 'Assets3' }), + objectLike({ Name: stringLike('Assets*') }), // 'Assets' vs 'Assets.1' + objectLike({ Name: stringLike('Assets*2') }), + objectLike({ Name: stringLike('Assets*3') }), objectLike({ Name: 'App' }), ], }); - }); + } }); }); behavior('command line properly locates assets in subassembly', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new FileAssetApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', @@ -183,15 +200,26 @@ describe('basic pipeline', () => { })), }, }); - }); + } }); behavior('multiple assets are published in parallel', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: arrayWith({ Name: 'Assets', @@ -201,7 +229,7 @@ describe('basic pipeline', () => { ], }), }); - }); + } }); behavior('assets are also published when using the lower-level addStackArtifactDeployment', (suite) => { @@ -210,6 +238,7 @@ describe('basic pipeline', () => { const asm = new FileAssetApp(app, 'FileAssetApp').synth(); // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addStage('SomeStage').addStackArtifactDeployment(asm.getStackByName('FileAssetApp-Stack')); // THEN @@ -225,14 +254,29 @@ describe('basic pipeline', () => { }), }); }); + + // This function does not exist in the modern API + suite.doesNotApply.modern(); }); behavior('file image asset publishers do not use privilegedmode', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new FileAssetApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Source: { BuildSpec: encodedJson(deepObjectLike({ @@ -248,15 +292,25 @@ describe('basic pipeline', () => { Image: 'aws/codebuild/standard:5.0', }), }); - }); + } }); behavior('docker image asset publishers use privilegedmode', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new DockerAssetApp(app, 'DockerAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Source: { BuildSpec: encodedJson(deepObjectLike({ @@ -272,20 +326,30 @@ describe('basic pipeline', () => { PrivilegedMode: true, }), }); - }); + } }); - behavior('can control fix/CLI version used in pipeline selfupdate', (suite) => { + behavior('can control fix/CLI version used in asset publishing', (suite) => { suite.legacy(() => { - // WHEN - const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); - const pipeline2 = new TestGitHubNpmPipeline(stack2, 'Cdk2', { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { cdkCliVersion: '1.2.3', }); - pipeline2.addApplicationStage(new FileAssetApp(stack2, 'FileAssetApp')); + pipeline.addApplicationStage(new FileAssetApp(pipelineStack, 'FileAssetApp')); - // THEN - expect(stack2).toHaveResourceLike('AWS::CodeBuild::Project', { + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + cliVersion: '1.2.3', + }); + pipeline.addStage(new FileAssetApp(pipelineStack, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, @@ -299,14 +363,29 @@ describe('basic pipeline', () => { })), }, }); - }); + } }); describe('asset roles and policies', () => { behavior('includes file publishing assets role for apps with file assets', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + pipeline.addStage(new FileAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ @@ -325,11 +404,12 @@ describe('basic pipeline', () => { }); expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); - }); + } }); behavior('publishing assets role may assume roles from multiple environments', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); pipeline.addApplicationStage(new FileAssetApp(app, 'App2', { env: { @@ -338,27 +418,80 @@ describe('basic pipeline', () => { }, })); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + + pipeline.addStage(new FileAssetApp(app, 'App1')); + pipeline.addStage(new FileAssetApp(app, 'App2', { + env: { + account: '0123456789012', + region: 'eu-west-1', + }, + })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy([FILE_PUBLISHING_ROLE, 'arn:${AWS::Partition}:iam::0123456789012:role/cdk-hnb659fds-file-publishing-role-0123456789012-eu-west-1'], 'CdkAssetsFileRole6BE17A07')); - }); + } }); behavior('publishing assets role de-dupes assumed roles', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); pipeline.addApplicationStage(new FileAssetApp(app, 'App2')); pipeline.addApplicationStage(new FileAssetApp(app, 'App3')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + pipeline.addStage(new FileAssetApp(app, 'App1')); + pipeline.addStage(new FileAssetApp(app, 'App2')); + pipeline.addStage(new FileAssetApp(app, 'App3')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); - }); + } }); behavior('includes image publishing assets role for apps with Docker assets', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ @@ -377,37 +510,72 @@ describe('basic pipeline', () => { }); expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(IMAGE_PUBLISHING_ROLE, 'CdkAssetsDockerRole484B6DD3')); - }); + } }); behavior('includes both roles for apps with both file and Docker assets', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); pipeline.addApplicationStage(new DockerAssetApp(app, 'App2')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + // Expectation expects to see KMS key policy permissions + crossAccountKeys: true, + }); + pipeline.addStage(new FileAssetApp(app, 'App1')); + pipeline.addStage(new DockerAssetApp(app, 'App2')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', expectedAssetRolePolicy(IMAGE_PUBLISHING_ROLE, 'CdkAssetsDockerRole484B6DD3')); - }); + } }); }); }); - behavior('can supply pre-install scripts to asset upload', (suite) => { suite.legacy(() => { - // WHEN - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { assetPreInstallCommands: [ 'npm config set registry https://registry.com', ], }); pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + assetPublishingCodeBuildDefaults: { + partialBuildSpec: cb.BuildSpec.fromObject({ + version: '0.2', + phases: { + install: { + commands: [ + 'npm config set registry https://registry.com', + ], + }, + }, + }), + }, + }); + pipeline.addStage(new FileAssetApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', @@ -422,27 +590,35 @@ behavior('can supply pre-install scripts to asset upload', (suite) => { })), }, }); - - app.cleanup(); - }); + } }); describe('pipeline with VPC', () => { let vpc: ec2.Vpc; beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); vpc = new ec2.Vpc(pipelineStack, 'Vpc'); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - vpc, - }); }); behavior('asset CodeBuild Project uses VPC subnets', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + vpc, + }); pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + pipeline.addStage(new DockerAssetApp(app, 'DockerAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // THEN expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { VpcConfig: objectLike({ @@ -457,16 +633,27 @@ describe('pipeline with VPC', () => { VpcId: { Ref: 'Vpc8378EB38' }, }), }); - }); + } }); behavior('Pipeline-generated CodeBuild Projects have appropriate execution role permissions', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + vpc, + }); pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); - // THEN + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + pipeline.addStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); + function THEN_codePipelineExpectation() { // Assets Project expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { Roles: [ @@ -480,13 +667,28 @@ describe('pipeline with VPC', () => { }), }, }); - }); + } }); behavior('Asset publishing CodeBuild Projects have a dependency on attached policies to the role', (suite) => { suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + vpc, + }); pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + pipeline.addStage(new DockerAssetApp(app, 'DockerAssetApp')); + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // Assets Project expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Properties: { @@ -501,32 +703,33 @@ describe('pipeline with VPC', () => { 'CdkAssetsDockerRoleVpcPolicy86CA024B', ], }, ResourcePart.CompleteDefinition); - }); + } }); }); describe('pipeline with single asset publisher', () => { - let otherPipelineStack: Stack; - let otherPipeline: cdkp.CdkPipeline; - - beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - singlePublisherPerType: true, - }); - otherPipelineStack = new Stack(app, 'OtherPipelineStack', { env: PIPELINE_ENV }); - otherPipeline = new TestGitHubNpmPipeline(otherPipelineStack, 'Cdk', { - singlePublisherPerType: true, - }); - }); - behavior('multiple assets are using the same job in singlePublisherMode', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + singlePublisherPerType: true, + }); pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + publishAssetsInParallel: false, + }); + pipeline.addStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // THEN + const buildSpecName = Capture.aString(); expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: arrayWith({ Name: 'Assets', @@ -541,306 +744,69 @@ describe('pipeline with single asset publisher', () => { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml', + BuildSpec: buildSpecName.capture(stringLike('buildspec-*.yaml')), }, }); const assembly = SynthUtils.synthesize(pipelineStack, { skipValidation: true }).assembly; - const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml')).toString()); + + const actualFileName = buildSpecName.capturedValue; + + const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, actualFileName), { encoding: 'utf-8' })); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`); - }); + } }); behavior('other pipeline writes to separate assets build spec file', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + singlePublisherPerType: true, + }); pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); - otherPipeline.addApplicationStage(new TwoFileAssetsApp(app, 'OtherFileAssetApp')); - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Source: { - BuildSpec: 'buildspec-assets-PipelineStack-Cdk-Assets-FileAsset.yaml', - }, + const pipelineStack2 = new Stack(app, 'PipelineStack2', { env: PIPELINE_ENV }); + const otherPipeline = new LegacyTestGitHubNpmPipeline(pipelineStack2, 'Cdk', { + singlePublisherPerType: true, }); - expect(otherPipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Source: { - BuildSpec: 'buildspec-assets-OtherPipelineStack-Cdk-Assets-FileAsset.yaml', - }, - }); - }); - }); -}); - -describe('pipeline with Docker credentials', () => { - const secretSynthArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:synth-012345'; - const secretUpdateArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:update-012345'; - const secretPublishArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:publish-012345'; - let secretSynth: secretsmanager.ISecret; - let secretUpdate: secretsmanager.ISecret; - let secretPublish: secretsmanager.ISecret; + otherPipeline.addApplicationStage(new TwoFileAssetsApp(app, 'OtherFileAssetApp')); - beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - - secretSynth = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret1', secretSynthArn); - secretUpdate = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret2', secretUpdateArn); - secretPublish = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret3', secretPublishArn); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - dockerCredentials: [ - cdkp.DockerCredential.customRegistry('synth.example.com', secretSynth, { - usages: [cdkp.DockerCredentialUsage.SYNTH], - }), - cdkp.DockerCredential.customRegistry('selfupdate.example.com', secretUpdate, { - usages: [cdkp.DockerCredentialUsage.SELF_UPDATE], - }), - cdkp.DockerCredential.customRegistry('publish.example.com', secretPublish, { - usages: [cdkp.DockerCredentialUsage.ASSET_PUBLISHING], - }), - ], + THEN_codePipelineExpectation(pipelineStack2); }); - }); - behavior('synth action receives install commands and access to relevant credentials', (suite) => { - suite.legacy(() => { - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); - - const expectedCredsConfig = JSON.stringify({ - version: '1.0', - domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + publishAssetsInParallel: false, }); + pipeline.addStage(new TwoFileAssetsApp(app, 'FileAssetApp')); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { Image: 'aws/codebuild/standard:5.0' }, - // Prove we're looking at the Synth project - ServiceRole: { 'Fn::GetAtt': ['CdkPipelineBuildSynthCdkBuildProjectRole5E173C62', 'Arn'] }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: [ - 'npm ci', - 'mkdir $HOME/.cdk', - `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - ], - }, - }, - })), - }, + const pipelineStack2 = new Stack(app, 'PipelineStack2', { env: PIPELINE_ENV }); + const otherPipeline = new ModernTestGitHubNpmPipeline(pipelineStack2, 'Cdk', { + publishAssetsInParallel: false, }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith({ - Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], - Effect: 'Allow', - Resource: secretSynthArn, - }), - Version: '2012-10-17', - }, - Roles: [{ Ref: 'CdkPipelineBuildSynthCdkBuildProjectRole5E173C62' }], - }); - }); - }); + otherPipeline.addStage(new TwoFileAssetsApp(app, 'OtherFileAssetApp')); - behavior('synth action receives Windows install commands if a Windows image is detected', (suite) => { - suite.legacy(() => { - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk2', { - dockerCredentials: [ - cdkp.DockerCredential.customRegistry('synth.example.com', secretSynth, { - usages: [cdkp.DockerCredentialUsage.SYNTH], - }), - cdkp.DockerCredential.customRegistry('selfupdate.example.com', secretUpdate, { - usages: [cdkp.DockerCredentialUsage.SELF_UPDATE], - }), - cdkp.DockerCredential.customRegistry('publish.example.com', secretPublish, { - usages: [cdkp.DockerCredentialUsage.ASSET_PUBLISHING], - }), - ], - npmSynthOptions: { - environment: { - buildImage: cb.WindowsBuildImage.WINDOWS_BASE_2_0, - }, - }, - }); - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); - - const expectedCredsConfig = JSON.stringify({ - version: '1.0', - domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, - }); - - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { Image: 'aws/codebuild/windows-base:2.0' }, - // Prove we're looking at the Synth project - ServiceRole: { 'Fn::GetAtt': ['Cdk2PipelineBuildSynthCdkBuildProjectRole9869128F', 'Arn'] }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - pre_build: { - commands: [ - 'npm ci', - 'mkdir %USERPROFILE%\\.cdk', - `echo '${expectedCredsConfig}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, - ], - }, - }, - })), - }, - }); + THEN_codePipelineExpectation(pipelineStack2); }); - }); - - behavior('self-update receives install commands and access to relevant credentials', (suite) => { - suite.legacy(() => { - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); - - const expectedCredsConfig = JSON.stringify({ - version: '1.0', - domainCredentials: { 'selfupdate.example.com': { secretsManagerSecretId: secretUpdateArn } }, - }); + function THEN_codePipelineExpectation(pipelineStack2: Stack) { + // THEN + const buildSpecName1 = Capture.aString(); + const buildSpecName2 = Capture.aString(); expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { Image: 'aws/codebuild/standard:5.0' }, - // Prove we're looking at the SelfMutate project - ServiceRole: { 'Fn::GetAtt': ['CdkUpdatePipelineSelfMutationRoleAAF1B580', 'Arn'] }, Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: [ - 'npm install -g aws-cdk', - 'mkdir $HOME/.cdk', - `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - ], - }, - }, - })), - }, - }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith({ - Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], - Effect: 'Allow', - Resource: secretUpdateArn, - }), - Version: '2012-10-17', + BuildSpec: buildSpecName1.capture(stringLike('buildspec-*.yaml')), }, - Roles: [{ Ref: 'CdkUpdatePipelineSelfMutationRoleAAF1B580' }], }); - }); - }); - - behavior('asset publishing receives install commands and access to relevant credentials', (suite) => { - suite.legacy(() => { - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); - - const expectedCredsConfig = JSON.stringify({ - version: '1.0', - domainCredentials: { 'publish.example.com': { secretsManagerSecretId: secretPublishArn } }, - }); - - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { Image: 'aws/codebuild/standard:5.0' }, - // Prove we're looking at the Publishing project - ServiceRole: { 'Fn::GetAtt': ['CdkAssetsDockerRole484B6DD3', 'Arn'] }, + expect(pipelineStack2).toHaveResourceLike('AWS::CodeBuild::Project', { Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: [ - 'mkdir $HOME/.cdk', - `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - 'npm install -g cdk-assets', - ], - }, - }, - })), + BuildSpec: buildSpecName2.capture(stringLike('buildspec-*.yaml')), }, }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith({ - Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], - Effect: 'Allow', - Resource: secretPublishArn, - }), - Version: '2012-10-17', - }, - Roles: [{ Ref: 'CdkAssetsDockerRole484B6DD3' }], - }); - }); - }); - -}); - -class PlainStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - new BucketStack(this, 'Stack'); - } -} - -class FileAssetApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - const stack = new Stack(this, 'Stack'); - new s3_assets.Asset(stack, 'Asset', { - path: path.join(__dirname, 'test-file-asset.txt'), - }); - } -} -class TwoFileAssetsApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - const stack = new Stack(this, 'Stack'); - new s3_assets.Asset(stack, 'Asset1', { - path: path.join(__dirname, 'test-file-asset.txt'), - }); - new s3_assets.Asset(stack, 'Asset2', { - path: path.join(__dirname, 'test-file-asset-two.txt'), - }); - } -} - -class DockerAssetApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - const stack = new Stack(this, 'Stack'); - new ecr_assets.DockerImageAsset(stack, 'Asset', { - directory: path.join(__dirname, 'test-docker-asset'), - }); - } -} - -interface MegaAssetsAppProps extends StageProps { - readonly numAssets: number; -} - -// Creates a mix of file and image assets, up to a specified count -class MegaAssetsApp extends Stage { - constructor(scope: Construct, id: string, props: MegaAssetsAppProps) { - super(scope, id, props); - const stack = new Stack(this, 'Stack'); - - let assetCount = 0; - for (; assetCount < props.numAssets / 2; assetCount++) { - new s3_assets.Asset(stack, `Asset${assetCount}`, { - path: path.join(__dirname, 'test-file-asset.txt'), - assetHash: `FileAsset${assetCount}`, - }); - } - for (; assetCount < props.numAssets; assetCount++) { - new ecr_assets.DockerImageAsset(stack, `Asset${assetCount}`, { - directory: path.join(__dirname, 'test-docker-asset'), - extraHash: `FileAsset${assetCount}`, - }); + expect(buildSpecName1.capturedValue).not.toEqual(buildSpecName2.capturedValue); } - } -} - + }); +}); function expectedAssetRolePolicy(assumeRolePattern: string | string[], attachedRole: string) { if (typeof assumeRolePattern === 'string') { assumeRolePattern = [assumeRolePattern]; } diff --git a/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts b/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts new file mode 100644 index 0000000000000..1248831737bdf --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts @@ -0,0 +1,228 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import * as fs from 'fs'; +import * as path from 'path'; +import { arrayWith, Capture, objectLike, stringLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { Stack, Stage, StageProps, Tags } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { behavior, LegacyTestGitHubNpmPipeline, OneStackApp, BucketStack, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('stack templates in nested assemblies are correctly addressed', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new OneStackApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'App', + Actions: arrayWith( + objectLike({ + Name: stringLike('*Prepare'), + InputArtifacts: [objectLike({})], + Configuration: objectLike({ + StackName: 'App-Stack', + TemplatePath: stringLike('*::assembly-App/*.template.json'), + }), + }), + ), + }), + }); + } +}); + +behavior('obvious error is thrown when stage contains no stacks', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + // WHEN + expect(() => { + pipeline.addApplicationStage(new Stage(app, 'EmptyStage')); + }).toThrow(/should contain at least one Stack/); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + // WHEN + expect(() => { + pipeline.addStage(new Stage(app, 'EmptyStage')); + }).toThrow(/should contain at least one Stack/); + }); +}); + +behavior('overridden stack names are respected', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackAppWithCustomName(app, 'App1')); + pipeline.addApplicationStage(new OneStackAppWithCustomName(app, 'App2')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new OneStackAppWithCustomName(app, 'App1')); + pipeline.addStage(new OneStackAppWithCustomName(app, 'App2')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith( + { + Name: 'App1', + Actions: arrayWith(objectLike({ + Name: stringLike('*Prepare'), + Configuration: objectLike({ + StackName: 'MyFancyStack', + }), + })), + }, + { + Name: 'App2', + Actions: arrayWith(objectLike({ + Name: stringLike('*Prepare'), + Configuration: objectLike({ + StackName: 'MyFancyStack', + }), + })), + }, + ), + }); + } +}); + +behavior('changing CLI version leads to a different pipeline structure (restarting it)', (suite) => { + suite.legacy(() => { + // GIVEN + const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); + const stack3 = new Stack(app, 'Stack3', { env: PIPELINE_ENV }); + + // WHEN + new LegacyTestGitHubNpmPipeline(stack2, 'Cdk', { + cdkCliVersion: '1.2.3', + }); + new LegacyTestGitHubNpmPipeline(stack3, 'Cdk', { + cdkCliVersion: '4.5.6', + }); + + THEN_codePipelineExpectation(stack2, stack3); + }); + + suite.modern(() => { + // GIVEN + const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); + const stack3 = new Stack(app, 'Stack3', { env: PIPELINE_ENV }); + + // WHEN + new ModernTestGitHubNpmPipeline(stack2, 'Cdk', { + cliVersion: '1.2.3', + }); + new ModernTestGitHubNpmPipeline(stack3, 'Cdk', { + cliVersion: '4.5.6', + }); + + THEN_codePipelineExpectation(stack2, stack3); + }); + + function THEN_codePipelineExpectation(stack2: Stack, stack3: Stack) { + // THEN + const structure2 = Capture.anyType(); + const structure3 = Capture.anyType(); + + expect(stack2).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: structure2.capture(), + }); + expect(stack3).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: structure3.capture(), + }); + + expect(JSON.stringify(structure2.capturedValue)).not.toEqual(JSON.stringify(structure3.capturedValue)); + } +}); + +behavior('tags get reflected in pipeline', (suite) => { + suite.legacy(() => { + // WHEN + const stage = new OneStackApp(app, 'App'); + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + Tags.of(stage).add('CostCenter', 'F00B4R'); + pipeline.addApplicationStage(stage); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const stage = new OneStackApp(app, 'App'); + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + Tags.of(stage).add('CostCenter', 'F00B4R'); + pipeline.addStage(stage); + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + const templateConfig = Capture.aString(); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'App', + Actions: arrayWith( + objectLike({ + Name: stringLike('*Prepare'), + InputArtifacts: [objectLike({})], + Configuration: objectLike({ + StackName: 'App-Stack', + TemplateConfiguration: templateConfig.capture(stringLike('*::assembly-App/*.template.*json')), + }), + }), + ), + }), + }); + + const [, relConfigFile] = templateConfig.capturedValue.split('::'); + const absConfigFile = path.join(app.outdir, relConfigFile); + const configFile = JSON.parse(fs.readFileSync(absConfigFile, { encoding: 'utf-8' })); + expect(configFile).toEqual(expect.objectContaining({ + Tags: { + CostCenter: 'F00B4R', + }, + })); + } +}); + +class OneStackAppWithCustomName extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + new BucketStack(this, 'Stack', { + stackName: 'MyFancyStack', + }); + } +} diff --git a/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts b/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts new file mode 100644 index 0000000000000..5ada88b49b937 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts @@ -0,0 +1,292 @@ +import { arrayWith, deepObjectLike, encodedJson, stringLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cb from '@aws-cdk/aws-codebuild'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cdkp from '../../lib'; +import { CodeBuildStep } from '../../lib'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, DockerAssetApp } from '../testhelpers'; + +const secretSynthArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:synth-012345'; +const secretUpdateArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:update-012345'; +const secretPublishArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:publish-012345'; + +let app: TestApp; +let pipelineStack: Stack; +let secretSynth: secretsmanager.ISecret; +let secretUpdate: secretsmanager.ISecret; +let secretPublish: secretsmanager.ISecret; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + secretSynth = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret1', secretSynthArn); + secretUpdate = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret2', secretUpdateArn); + secretPublish = secretsmanager.Secret.fromSecretCompleteArn(pipelineStack, 'Secret3', secretPublishArn); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('synth action receives install commands and access to relevant credentials', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + const expectedCredsConfig = JSON.stringify({ + version: '1.0', + domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { Image: 'aws/codebuild/standard:5.0' }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + pre_build: { + commands: arrayWith( + 'mkdir $HOME/.cdk', + `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, + ), + }, + // Prove we're looking at the Synth project + build: { + commands: arrayWith(stringLike('*cdk*synth*')), + }, + }, + })), + }, + }); + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], + Effect: 'Allow', + Resource: secretSynthArn, + }), + Version: '2012-10-17', + }, + Roles: [{ Ref: stringLike('Cdk*BuildProjectRole*') }], + }); + } +}); + +behavior('synth action receives Windows install commands if a Windows image is detected', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyPipelineWithCreds(pipelineStack, 'Cdk2', { + npmSynthOptions: { + environment: { + buildImage: cb.WindowsBuildImage.WINDOWS_BASE_2_0, + }, + }, + }); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernPipelineWithCreds(pipelineStack, 'Cdk2', { + synth: new CodeBuildStep('Synth', { + commands: ['cdk synth'], + primaryOutputDirectory: 'cdk.out', + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + buildEnvironment: { + buildImage: cb.WindowsBuildImage.WINDOWS_BASE_2_0, + computeType: cb.ComputeType.MEDIUM, + }, + }), + }); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + const expectedCredsConfig = JSON.stringify({ + version: '1.0', + domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { Image: 'aws/codebuild/windows-base:2.0' }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + pre_build: { + commands: arrayWith( + 'mkdir %USERPROFILE%\\.cdk', + `echo '${expectedCredsConfig}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, + ), + }, + // Prove we're looking at the Synth project + build: { + commands: arrayWith(stringLike('*cdk*synth*')), + }, + }, + })), + }, + }); + } +}); + +behavior('self-update receives install commands and access to relevant credentials', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation('install'); + }); + + suite.modern(() => { + const pipeline = new ModernPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation('pre_build'); + }); + + function THEN_codePipelineExpectation(expectedPhase: string) { + const expectedCredsConfig = JSON.stringify({ + version: '1.0', + domainCredentials: { 'selfupdate.example.com': { secretsManagerSecretId: secretUpdateArn } }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { Image: 'aws/codebuild/standard:5.0' }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [expectedPhase]: { + commands: arrayWith( + 'mkdir $HOME/.cdk', + `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, + ), + }, + // Prove we're looking at the SelfMutate project + build: { + commands: arrayWith( + stringLike('cdk * deploy PipelineStack*'), + ), + }, + }, + })), + }, + }); + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], + Effect: 'Allow', + Resource: secretUpdateArn, + }), + Version: '2012-10-17', + }, + Roles: [{ Ref: stringLike('*SelfMutat*Role*') }], + }); + } +}); + +behavior('asset publishing receives install commands and access to relevant credentials', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation('install'); + }); + + suite.modern(() => { + const pipeline = new ModernPipelineWithCreds(pipelineStack, 'Cdk'); + pipeline.addStage(new DockerAssetApp(app, 'App1')); + + THEN_codePipelineExpectation('pre_build'); + }); + + function THEN_codePipelineExpectation(expectedPhase: string) { + const expectedCredsConfig = JSON.stringify({ + version: '1.0', + domainCredentials: { 'publish.example.com': { secretsManagerSecretId: secretPublishArn } }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { Image: 'aws/codebuild/standard:5.0' }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [expectedPhase]: { + commands: arrayWith( + 'mkdir $HOME/.cdk', + `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, + ), + }, + // Prove we're looking at the Publishing project + build: { + commands: arrayWith(stringLike('cdk-assets*')), + }, + }, + })), + }, + }); + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], + Effect: 'Allow', + Resource: secretPublishArn, + }), + Version: '2012-10-17', + }, + Roles: [{ Ref: 'CdkAssetsDockerRole484B6DD3' }], + }); + } +}); + +class LegacyPipelineWithCreds extends LegacyTestGitHubNpmPipeline { + constructor(scope: Construct, id: string, props?: ConstructorParameters[2]) { + super(scope, id, { + dockerCredentials: [ + cdkp.DockerCredential.customRegistry('synth.example.com', secretSynth, { + usages: [cdkp.DockerCredentialUsage.SYNTH], + }), + cdkp.DockerCredential.customRegistry('selfupdate.example.com', secretUpdate, { + usages: [cdkp.DockerCredentialUsage.SELF_UPDATE], + }), + cdkp.DockerCredential.customRegistry('publish.example.com', secretPublish, { + usages: [cdkp.DockerCredentialUsage.ASSET_PUBLISHING], + }), + ], + ...props, + }); + } +} + +class ModernPipelineWithCreds extends ModernTestGitHubNpmPipeline { + constructor(scope: Construct, id: string, props?: ConstructorParameters[2]) { + super(scope, id, { + dockerCredentials: [ + cdkp.DockerCredential.customRegistry('synth.example.com', secretSynth, { + usages: [cdkp.DockerCredentialUsage.SYNTH], + }), + cdkp.DockerCredential.customRegistry('selfupdate.example.com', secretUpdate, { + usages: [cdkp.DockerCredentialUsage.SELF_UPDATE], + }), + cdkp.DockerCredential.customRegistry('publish.example.com', secretPublish, { + usages: [cdkp.DockerCredentialUsage.ASSET_PUBLISHING], + }), + ], + ...props, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts b/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts new file mode 100644 index 0000000000000..d30e5a423fcb3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts @@ -0,0 +1,391 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { arrayWith, objectLike, stringLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { Stack } from '@aws-cdk/core'; +import { behavior, LegacyTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('action has right settings for same-env deployment', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'Same')); + + THEN_codePipelineExpection(agnosticRole); + }); + + suite.additional('legacy: even if env is specified but the same as the pipeline', () => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'Same', { + env: PIPELINE_ENV, + })); + + THEN_codePipelineExpection(pipelineEnvRole); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new OneStackApp(app, 'Same')); + + THEN_codePipelineExpection(agnosticRole); + }); + + suite.additional('modern: even if env is specified but the same as the pipeline', () => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new OneStackApp(app, 'Same', { + env: PIPELINE_ENV, + })); + + THEN_codePipelineExpection(pipelineEnvRole); + }); + + function THEN_codePipelineExpection(roleArn: (x: string) => any) { + // THEN: pipeline structure is correct + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Same', + Actions: [ + objectLike({ + Name: stringLike('*Prepare'), + RoleArn: roleArn('deploy-role'), + Configuration: objectLike({ + StackName: 'Same-Stack', + RoleArn: roleArn('cfn-exec-role'), + }), + }), + objectLike({ + Name: stringLike('*Deploy'), + RoleArn: roleArn('deploy-role'), + Configuration: objectLike({ + StackName: 'Same-Stack', + }), + }), + ], + }), + }); + + // THEN: artifact bucket can be read by deploy role + expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Principal: { + AWS: roleArn('deploy-role'), + }, + })), + }, + }); + } +}); + +behavior('action has right settings for cross-account deployment', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'CrossAccount', { env: { account: 'you' } })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + pipeline.addStage(new OneStackApp(app, 'CrossAccount', { env: { account: 'you' } })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN: Pipelien structure is correct + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'CrossAccount', + Actions: [ + objectLike({ + Name: stringLike('*Prepare'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-deploy-role-you-', + { Ref: 'AWS::Region' }, + ]], + }, + Configuration: objectLike({ + StackName: 'CrossAccount-Stack', + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-cfn-exec-role-you-', + { Ref: 'AWS::Region' }, + ]], + }, + }), + }), + objectLike({ + Name: stringLike('*Deploy'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-deploy-role-you-', + { Ref: 'AWS::Region' }, + ]], + }, + Configuration: objectLike({ + StackName: 'CrossAccount-Stack', + }), + }), + ], + }), + }); + + // THEN: Artifact bucket can be read by deploy role + expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Principal: { + AWS: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + stringLike('*-deploy-role-*'), + { Ref: 'AWS::Region' }, + ]], + }, + }, + })), + }, + }); + } +}); + +behavior('action has right settings for cross-region deployment', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'CrossRegion', { env: { region: 'elsewhere' } })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + pipeline.addStage(new OneStackApp(app, 'CrossRegion', { env: { region: 'elsewhere' } })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'CrossRegion', + Actions: [ + objectLike({ + Name: stringLike('*Prepare'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::', + { Ref: 'AWS::AccountId' }, + ':role/cdk-hnb659fds-deploy-role-', + { Ref: 'AWS::AccountId' }, + '-elsewhere', + ]], + }, + Region: 'elsewhere', + Configuration: objectLike({ + StackName: 'CrossRegion-Stack', + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::', + { Ref: 'AWS::AccountId' }, + ':role/cdk-hnb659fds-cfn-exec-role-', + { Ref: 'AWS::AccountId' }, + '-elsewhere', + ]], + }, + }), + }), + objectLike({ + Name: stringLike('*Deploy'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::', + { Ref: 'AWS::AccountId' }, + ':role/cdk-hnb659fds-deploy-role-', + { Ref: 'AWS::AccountId' }, + '-elsewhere', + ]], + }, + Region: 'elsewhere', + Configuration: objectLike({ + StackName: 'CrossRegion-Stack', + }), + }), + ], + }), + }); + } +}); + +behavior('action has right settings for cross-account/cross-region deployment', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'CrossBoth', { + env: { + account: 'you', + region: 'elsewhere', + }, + })); + + THEN_codePipelineExpectations(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + pipeline.addStage(new OneStackApp(app, 'CrossBoth', { + env: { + account: 'you', + region: 'elsewhere', + }, + })); + + THEN_codePipelineExpectations(); + }); + + function THEN_codePipelineExpectations() { + // THEN: pipeline structure must be correct + expect(app.stackArtifact(pipelineStack)).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'CrossBoth', + Actions: [ + objectLike({ + Name: stringLike('*Prepare'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-deploy-role-you-elsewhere', + ]], + }, + Region: 'elsewhere', + Configuration: objectLike({ + StackName: 'CrossBoth-Stack', + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-cfn-exec-role-you-elsewhere', + ]], + }, + }), + }), + objectLike({ + Name: stringLike('*Deploy'), + RoleArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::you:role/cdk-hnb659fds-deploy-role-you-elsewhere', + ]], + }, + Region: 'elsewhere', + Configuration: objectLike({ + StackName: 'CrossBoth-Stack', + }), + }), + ], + }), + }); + + // THEN: artifact bucket can be read by deploy role + const supportStack = 'PipelineStack-support-elsewhere'; + expect(app.stackArtifact(supportStack)).toHaveResourceLike('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: arrayWith('s3:GetObject*', 's3:GetBucket*', 's3:List*'), + Principal: { + AWS: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + stringLike('*-deploy-role-*'), + ]], + }, + }, + })), + }, + }); + + // And the key to go along with it + expect(app.stackArtifact(supportStack)).toHaveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: arrayWith(objectLike({ + Action: arrayWith('kms:Decrypt', 'kms:DescribeKey'), + Principal: { + AWS: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + stringLike('*-deploy-role-*'), + ]], + }, + }, + })), + }, + }); + } +}); + + +function agnosticRole(roleName: string) { + return { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::', + { Ref: 'AWS::AccountId' }, + `:role/cdk-hnb659fds-${roleName}-`, + { Ref: 'AWS::AccountId' }, + '-', + { Ref: 'AWS::Region' }, + ]], + }; +} + +function pipelineEnvRole(roleName: string) { + return { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + `:iam::${PIPELINE_ENV.account}:role/cdk-hnb659fds-${roleName}-${PIPELINE_ENV.account}-${PIPELINE_ENV.region}`, + ]], + }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts b/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts new file mode 100644 index 0000000000000..82754f52d5cba --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts @@ -0,0 +1,274 @@ +import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import * as cpa from '@aws-cdk/aws-codepipeline-actions'; +import { SecretValue, Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { CodePipelineFileSet } from '../../lib'; +import { behavior, FileAssetApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, PIPELINE_ENV, TestApp, TestGitHubAction } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; +let sourceArtifact: cp.Artifact; +let cloudAssemblyArtifact: cp.Artifact; +let codePipeline: cp.Pipeline; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + sourceArtifact = new cp.Artifact(); + cloudAssemblyArtifact = new cp.Artifact(); +}); + +afterEach(() => { + app.cleanup(); +}); + +describe('with empty existing CodePipeline', () => { + beforeEach(() => { + codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline'); + }); + + behavior('both actions are required', (suite) => { + suite.legacy(() => { + // WHEN + expect(() => { + new cdkp.CdkPipeline(pipelineStack, 'Cdk', { cloudAssemblyArtifact, codePipeline }); + }).toThrow(/You must pass a 'sourceAction'/); + }); + + // 'synth' is not optional so this doesn't apply + suite.doesNotApply.modern(); + }); + + behavior('can give both actions', (suite) => { + suite.legacy(() => { + // WHEN + new cdkp.CdkPipeline(pipelineStack, 'Cdk', { + cloudAssemblyArtifact, + codePipeline, + sourceAction: new TestGitHubAction(sourceArtifact), + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + new cdkp.CodePipeline(pipelineStack, 'Cdk', { + codePipeline, + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['true'], + }), + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'Source' }), + objectLike({ Name: 'Build' }), + objectLike({ Name: 'UpdatePipeline' }), + ], + }); + } + }); +}); + +describe('with custom Source stage in existing Pipeline', () => { + beforeEach(() => { + codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [new TestGitHubAction(sourceArtifact)], + }, + ], + }); + }); + + behavior('Work with synthAction', (suite) => { + suite.legacy(() => { + // WHEN + new cdkp.CdkPipeline(pipelineStack, 'Cdk', { + codePipeline, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new cdkp.CodePipeline(pipelineStack, 'Cdk', { + codePipeline, + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineFileSet.fromArtifact(sourceArtifact), + commands: ['true'], + }), + }); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'CustomSource' }), + objectLike({ Name: 'Build' }), + objectLike({ Name: 'UpdatePipeline' }), + ], + }); + } + }); +}); + +describe('with Source and Build stages in existing Pipeline', () => { + beforeEach(() => { + codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [new TestGitHubAction(sourceArtifact)], + }, + { + stageName: 'CustomBuild', + actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], + }, + ], + }); + }); + + behavior('can supply no actions', (suite) => { + suite.legacy(() => { + // WHEN + new cdkp.CdkPipeline(pipelineStack, 'Cdk', { + codePipeline, + cloudAssemblyArtifact, + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new cdkp.CodePipeline(pipelineStack, 'Cdk', { + codePipeline, + synth: cdkp.CodePipelineFileSet.fromArtifact(cloudAssemblyArtifact), + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'CustomSource' }), + objectLike({ Name: 'CustomBuild' }), + objectLike({ Name: 'UpdatePipeline' }), + ], + }); + } + }); +}); + +behavior('can add another action to an existing stage', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.stage('Source').addAction(new cpa.GitHubSourceAction({ + actionName: 'GitHub2', + oauthToken: SecretValue.plainText('oops'), + output: new cp.Artifact(), + owner: 'OWNER', + repo: 'REPO', + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.buildPipeline(); + + pipeline.pipeline.stages[0].addAction(new cpa.GitHubSourceAction({ + actionName: 'GitHub2', + oauthToken: SecretValue.plainText('oops'), + output: new cp.Artifact(), + owner: 'OWNER', + repo: 'REPO', + })); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Source', + Actions: [ + objectLike({ ActionTypeId: objectLike({ Provider: 'GitHub' }) }), + objectLike({ ActionTypeId: objectLike({ Provider: 'GitHub' }), Name: 'GitHub2' }), + ], + }), + }); + } +}); + + +behavior('assets stage inserted after existing pipeline actions', (suite) => { + let existingCodePipeline: cp.Pipeline; + beforeEach(() => { + existingCodePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [new TestGitHubAction(sourceArtifact)], + }, + { + stageName: 'CustomBuild', + actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], + }, + ], + }); + }); + + suite.legacy(() => { + const pipeline = new cdkp.CdkPipeline(pipelineStack, 'CdkEmptyPipeline', { + cloudAssemblyArtifact: cloudAssemblyArtifact, + selfMutating: false, + codePipeline: existingCodePipeline, + // No source/build actions + }); + pipeline.addApplicationStage(new FileAssetApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new cdkp.CodePipeline(pipelineStack, 'CdkEmptyPipeline', { + codePipeline: existingCodePipeline, + selfMutation: false, + synth: CodePipelineFileSet.fromArtifact(cloudAssemblyArtifact), + // No source/build actions + }); + pipeline.addStage(new FileAssetApp(app, 'App')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'CustomSource' }), + objectLike({ Name: 'CustomBuild' }), + objectLike({ Name: 'Assets' }), + objectLike({ Name: 'App' }), + ], + }); + } +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts new file mode 100644 index 0000000000000..8aa1ed8293c30 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts @@ -0,0 +1,241 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { anything, arrayWith, deepObjectLike, encodedJson, notMatching, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import { Stack, Stage } from '@aws-cdk/core'; +import { behavior, LegacyTestGitHubNpmPipeline, PIPELINE_ENV, stackTemplate, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('CodePipeline has self-mutation stage', (suite) => { + suite.legacy(() => { + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'UpdatePipeline', + Actions: [ + objectLike({ + Name: 'SelfMutate', + Configuration: objectLike({ + ProjectName: { Ref: anything() }, + }), + }), + ], + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + install: { + commands: ['npm install -g aws-cdk'], + }, + build: { + commands: arrayWith('cdk -a . deploy PipelineStack --require-approval=never --verbose'), + }, + }, + })), + Type: 'CODEPIPELINE', + }, + }); + } +}); + +behavior('selfmutation stage correctly identifies nested assembly of pipeline stack', (suite) => { + suite.legacy(() => { + const pipelineStage = new Stage(app, 'PipelineStage'); + const nestedPipelineStack = new Stack(pipelineStage, 'PipelineStack', { env: PIPELINE_ENV }); + new LegacyTestGitHubNpmPipeline(nestedPipelineStack, 'Cdk'); + + THEN_codePipelineExpectation(nestedPipelineStack); + }); + + suite.modern(() => { + const pipelineStage = new Stage(app, 'PipelineStage'); + const nestedPipelineStack = new Stack(pipelineStage, 'PipelineStack', { env: PIPELINE_ENV }); + new ModernTestGitHubNpmPipeline(nestedPipelineStack, 'Cdk'); + + THEN_codePipelineExpectation(nestedPipelineStack); + }); + + function THEN_codePipelineExpectation(nestedPipelineStack: Stack) { + expect(stackTemplate(nestedPipelineStack)).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: arrayWith('cdk -a assembly-PipelineStage deploy PipelineStage/PipelineStack --require-approval=never --verbose'), + }, + }, + })), + }, + }); + } +}); + +behavior('selfmutation feature can be turned off', (suite) => { + suite.legacy(() => { + const cloudAssemblyArtifact = new cp.Artifact(); + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + cloudAssemblyArtifact, + selfMutating: false, + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + selfMutation: false, + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: notMatching(arrayWith({ + Name: 'UpdatePipeline', + Actions: anything(), + })), + }); + } +}); + +behavior('can control fix/CLI version used in pipeline selfupdate', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + pipelineName: 'vpipe', + cdkCliVersion: '1.2.3', + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + pipelineName: 'vpipe', + cliVersion: '1.2.3', + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Name: 'vpipe-selfupdate', + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + install: { + commands: ['npm install -g aws-cdk@1.2.3'], + }, + }, + })), + }, + }); + } +}); + +behavior('Pipeline stack itself can use assets (has implications for selfupdate)', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'PrivilegedPipeline', { + supportDockerAssets: true, + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + PrivilegedMode: true, + }, + }); + }); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'PrivilegedPipeline', { + dockerEnabledForSelfMutation: true, + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + PrivilegedMode: true, + }, + }); + }); +}); + +behavior('self-update project role uses tagged bootstrap-role permissions', (suite) => { + suite.legacy(() => { + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + THEN_codePipelineExpectations(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + THEN_codePipelineExpectations(); + }); + + function THEN_codePipelineExpectations() { + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith( + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: 'arn:*:iam::123pipeline:role/*', + Condition: { + 'ForAnyValue:StringEquals': { + 'iam:ResourceTag/aws-cdk:bootstrap-role': ['image-publishing', 'file-publishing', 'deploy'], + }, + }, + }, + { + Action: 'cloudformation:DescribeStacks', + Effect: 'Allow', + Resource: '*', + }, + { + Action: 's3:ListBucket', + Effect: 'Allow', + Resource: '*', + }, + ), + }, + }); + } +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/stack-ordering.test.ts b/packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts similarity index 67% rename from packages/@aws-cdk/pipelines/test/stack-ordering.test.ts rename to packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts index 50d1b892cc5cb..cb21139b16364 100644 --- a/packages/@aws-cdk/pipelines/test/stack-ordering.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts @@ -1,27 +1,32 @@ import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; -import { App, Stack, Stage, StageProps } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { sortedByRunOrder } from './testmatchers'; -import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; +import { App, Stack } from '@aws-cdk/core'; +import { behavior, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, sortedByRunOrder, TestApp, ThreeStackApp, TwoStackApp } from '../testhelpers'; let app: App; let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; beforeEach(() => { app = new TestApp(); pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); }); behavior('interdependent stacks are in the right order', (suite) => { suite.legacy(() => { - // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new TwoStackApp(app, 'MyApp')); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoStackApp(app, 'MyApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { // THEN expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: arrayWith({ @@ -34,15 +39,26 @@ behavior('interdependent stacks are in the right order', (suite) => { ]), }), }); - }); + } }); behavior('multiple independent stacks go in parallel', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new ThreeStackApp(app, 'MyApp')); - // THEN + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new ThreeStackApp(app, 'MyApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: arrayWith({ Name: 'MyApp', @@ -58,12 +74,13 @@ behavior('multiple independent stacks go in parallel', (suite) => { ]), }), }); - }); + } }); -behavior('manual approval is inserted in correct location', (suite) => { +behavior('user can request manual change set approvals', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new TwoStackApp(app, 'MyApp'), { manualApprovals: true, }); @@ -83,11 +100,15 @@ behavior('manual approval is inserted in correct location', (suite) => { }), }); }); + + // No change set approvals in Modern API for now. + suite.doesNotApply.modern(); }); -behavior('extra space for sequential intermediary actions is reserved', (suite) => { +behavior('user can request extra runorder space between prepare and deploy', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new TwoStackApp(app, 'MyApp'), { extraRunOrderSpace: 1, }); @@ -117,11 +138,15 @@ behavior('extra space for sequential intermediary actions is reserved', (suite) }), }); }); + + // No change set approvals in Modern API for now. + suite.doesNotApply.modern(); }); -behavior('combination of manual approval and extraRunOrderSpace', (suite) => { +behavior('user can request both manual change set approval and extraRunOrderSpace', (suite) => { suite.legacy(() => { // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new OneStackApp(app, 'MyApp'), { extraRunOrderSpace: 1, manualApprovals: true, @@ -133,7 +158,7 @@ behavior('combination of manual approval and extraRunOrderSpace', (suite) => { Name: 'MyApp', Actions: sortedByRunOrder([ objectLike({ - Name: 'Stack1.Prepare', + Name: 'Stack.Prepare', RunOrder: 1, }), objectLike({ @@ -141,46 +166,14 @@ behavior('combination of manual approval and extraRunOrderSpace', (suite) => { RunOrder: 2, }), objectLike({ - Name: 'Stack1.Deploy', + Name: 'Stack.Deploy', RunOrder: 4, }), ]), }), }); }); -}); - -class OneStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - - new BucketStack(this, 'Stack1'); - } -} -class TwoStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - - const stack2 = new BucketStack(this, 'Stack2'); - const stack1 = new BucketStack(this, 'Stack1'); - - stack2.addDependency(stack1); - } -} - -/** - * Three stacks where the last one depends on the earlier 2 - */ -class ThreeStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - - const stack1 = new BucketStack(this, 'Stack1'); - const stack2 = new BucketStack(this, 'Stack2'); - const stack3 = new BucketStack(this, 'Stack3'); - - stack3.addDependency(stack1); - stack3.addDependency(stack2); - } -} + // No change set approvals in Modern API for now. + suite.doesNotApply.modern(); +}); diff --git a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts new file mode 100644 index 0000000000000..92d1c9164fcba --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts @@ -0,0 +1,981 @@ +import { arrayWith, deepObjectLike, encodedJson, objectLike, Capture, anything } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as cbuild from '@aws-cdk/aws-codebuild'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecr from '@aws-cdk/aws-ecr'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { CodeBuildStep } from '../../lib'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, ModernTestGitHubNpmPipelineProps } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; +let sourceArtifact: codepipeline.Artifact; +let cloudAssemblyArtifact: codepipeline.Artifact; + +// Must be unique across all test files, but preferably also consistent +const OUTDIR = 'testcdk0.out'; + +// What phase install commands get rendered to +const LEGACY_INSTALLS = 'pre_build'; +const MODERN_INSTALLS = 'install'; + +beforeEach(() => { + app = new TestApp({ outdir: OUTDIR }); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + sourceArtifact = new codepipeline.Artifact(); + cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('synth takes arrays of commands', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: new cdkp.SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + installCommands: ['install1', 'install2'], + buildCommands: ['build1', 'build2'], + testCommands: ['test1', 'test2'], + synthCommand: 'cdk synth', + }), + }); + + THEN_codePipelineExpectation(LEGACY_INSTALLS); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + installCommands: ['install1', 'install2'], + commands: ['build1', 'build2', 'test1', 'test2', 'cdk synth'], + }); + + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + function THEN_codePipelineExpectation(installPhase: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [installPhase]: { + commands: [ + 'install1', + 'install2', + ], + }, + build: { + commands: [ + 'build1', + 'build2', + 'test1', + 'test2', + 'cdk synth', + ], + }, + }, + })), + }, + }); + } +}); + +behavior('synth sets artifact base-directory to cdk.out', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), + }); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + artifacts: { + 'base-directory': 'cdk.out', + }, + })), + }, + }); + } +}); + +behavior('synth supports setting subdirectory', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + subdirectory: 'subdir', + }), + }); + + THEN_codePipelineExpectation(LEGACY_INSTALLS); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + installCommands: ['cd subdir'], + commands: ['true'], + primaryOutputDirectory: 'subdir/cdk.out', + }); + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + function THEN_codePipelineExpectation(installPhase: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [installPhase]: { + commands: arrayWith('cd subdir'), + }, + }, + artifacts: { + 'base-directory': 'subdir/cdk.out', + }, + })), + }, + }); + } +}); + +behavior('npm synth sets, or allows setting, UNSAFE_PERM=true', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + env: { + NPM_CONFIG_UNSAFE_PERM: 'true', + }, + }); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + EnvironmentVariables: [ + { + Name: 'NPM_CONFIG_UNSAFE_PERM', + Type: 'PLAINTEXT', + Value: 'true', + }, + ], + }, + }); + } +}); + +behavior('synth assumes a JavaScript project by default (no build, yes synth)', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + pre_build: { + commands: ['npm ci'], + }, + build: { + commands: ['npx cdk synth'], + }, + }, + })), + }, + }); + }); + + // Modern pipeline does not assume anything anymore + suite.doesNotApply.modern(); +}); + +behavior('Magic CodePipeline variables passed to synth envvars must be rendered in the action', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: new cdkp.SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + environmentVariables: { + VERSION: { value: codepipeline.GlobalVariables.executionId }, + }, + synthCommand: 'synth', + }), + }); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + env: { + VERSION: codepipeline.GlobalVariables.executionId, + }, + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Build', + Actions: [ + objectLike({ + Name: 'Synth', + Configuration: objectLike({ + EnvironmentVariables: encodedJson(arrayWith( + { + name: 'VERSION', + type: 'PLAINTEXT', + value: '#{codepipeline.PipelineExecutionId}', + }, + )), + }), + }), + ], + }), + }); + } +}); + +behavior('CodeBuild: environment variables specified in multiple places are correctly merged', (suite) => { + // We don't support merging environment variables in this way in the legacy API + suite.doesNotApply.legacy(); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new CodeBuildStep('Synth', { + env: { + SOME_ENV_VAR: 'SomeValue', + }, + installCommands: [ + 'install1', + 'install2', + ], + commands: ['synth'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: 'cdk.out', + buildEnvironment: { + environmentVariables: { + INNER_VAR: { value: 'InnerValue' }, + }, + privileged: true, + }, + }), + }); + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + suite.additional('modern2, using the specific CodeBuild action', () => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.CodeBuildStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: '.', + env: { + SOME_ENV_VAR: 'SomeValue', + }, + installCommands: [ + 'install1', + 'install2', + ], + commands: ['synth'], + buildEnvironment: { + environmentVariables: { + INNER_VAR: { value: 'InnerValue' }, + }, + privileged: true, + }, + }), + }); + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + function THEN_codePipelineExpectation(installPhase: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: objectLike({ + PrivilegedMode: true, + EnvironmentVariables: arrayWith( + { + Name: 'SOME_ENV_VAR', + Type: 'PLAINTEXT', + Value: 'SomeValue', + }, + { + Name: 'INNER_VAR', + Type: 'PLAINTEXT', + Value: 'InnerValue', + }, + ), + }), + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [installPhase]: { + commands: ['install1', 'install2'], + }, + build: { + commands: ['synth'], + }, + }, + })), + }, + }); + } +}); + +behavior('install command can be overridden/specified', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + installCommand: '/bin/true', + }), + }); + + THEN_codePipelineExpectation(LEGACY_INSTALLS); + }); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + installCommands: ['/bin/true'], + }); + + THEN_codePipelineExpectation(MODERN_INSTALLS); + }); + + function THEN_codePipelineExpectation(installPhase: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + [installPhase]: { + commands: ['/bin/true'], + }, + }, + })), + }, + }); + } +}); + +behavior('synth can have its test commands set', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + installCommand: '/bin/true', + testCommands: ['echo "Running tests"'], + }), + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(objectLike({ + phases: { + pre_build: { + commands: ['/bin/true'], + }, + build: { + commands: ['echo "Running tests"', 'npx cdk synth'], + }, + }, + })), + }, + }); + }); + + // There are no implicit commands in modern synth + suite.doesNotApply.modern(); +}); + +behavior('Synth can output additional artifacts', (suite) => { + suite.legacy(() => { + // WHEN + const addlArtifact = new codepipeline.Artifact('IntegTest'); + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + additionalArtifacts: [ + { + artifact: addlArtifact, + directory: 'test', + }, + ], + }), + }); + + THEN_codePipelineExpectation('CloudAsm', 'IntegTest'); + }); + + suite.modern(() => { + // WHEN + const synth = new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['cdk synth'], + }); + synth.addOutputDirectory('test'); + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: synth, + }); + + THEN_codePipelineExpectation('Synth_Output', 'Synth_test'); + }); + + function THEN_codePipelineExpectation(asmArtifact: string, testArtifact: string) { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + artifacts: { + 'secondary-artifacts': { + [asmArtifact]: { + 'base-directory': 'cdk.out', + 'files': '**/*', + }, + [testArtifact]: { + 'base-directory': 'test', + 'files': '**/*', + }, + }, + }, + })), + }, + }); + } +}); + +behavior('Synth can be made to run in a VPC', (suite) => { + let vpc: ec2.Vpc; + beforeEach(() => { + vpc = new ec2.Vpc(pipelineStack, 'NpmSynthTestVpc'); + }); + + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + vpc, + sourceArtifact, + cloudAssemblyArtifact, + }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + }); + + suite.additional('Modern, using CodeBuildStep', () => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new CodeBuildStep('Synth', { + commands: ['asdf'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: 'cdk.out', + buildEnvironment: { + computeType: cbuild.ComputeType.LARGE, + }, + }), + codeBuildDefaults: { vpc }, + }); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + VpcConfig: { + SecurityGroupIds: [ + { 'Fn::GetAtt': ['CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', 'GroupId'] }, + ], + Subnets: [ + { Ref: 'NpmSynthTestVpcPrivateSubnet1Subnet81E3AA56' }, + { Ref: 'NpmSynthTestVpcPrivateSubnet2SubnetC1CA3EF0' }, + { Ref: 'NpmSynthTestVpcPrivateSubnet3SubnetA04163EE' }, + ], + VpcId: { Ref: 'NpmSynthTestVpc5E703F25' }, + }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Roles: [ + { Ref: 'CdkPipelineBuildSynthCdkBuildProjectRole5E173C62' }, + ], + PolicyDocument: { + Statement: arrayWith({ + Action: arrayWith('ec2:DescribeSecurityGroups'), + Effect: 'Allow', + Resource: '*', + }), + }, + }); + } +}); + +behavior('Pipeline action contains a hash that changes as the buildspec changes', (suite) => { + suite.legacy(() => { + const hash1 = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + })); + + // To make sure the hash is not just random :) + const hash1prime = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + })); + + const hash2 = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + installCommand: 'do install', + })); + const hash3 = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + environment: { + computeType: cbuild.ComputeType.LARGE, + }, + })); + const hash4 = legacySynthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact: sa, + cloudAssemblyArtifact: cxa, + environment: { + environmentVariables: { + xyz: { value: 'SOME-VALUE' }, + }, + }, + })); + + expect(hash1).toEqual(hash1prime); + + expect(hash1).not.toEqual(hash2); + expect(hash1).not.toEqual(hash3); + expect(hash1).not.toEqual(hash4); + expect(hash2).not.toEqual(hash3); + expect(hash2).not.toEqual(hash4); + expect(hash3).not.toEqual(hash4); + }); + + suite.modern(() => { + const hash1 = modernSynthWithAction(() => ({ commands: ['asdf'] })); + + // To make sure the hash is not just random :) + const hash1prime = modernSynthWithAction(() => ({ commands: ['asdf'] })); + + const hash2 = modernSynthWithAction(() => ({ + installCommands: ['do install'], + })); + const hash3 = modernSynthWithAction(() => ({ + synth: new CodeBuildStep('Synth', { + commands: ['asdf'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: 'cdk.out', + buildEnvironment: { + computeType: cbuild.ComputeType.LARGE, + }, + }), + })); + + const hash4 = modernSynthWithAction(() => ({ + env: { + xyz: 'SOME-VALUE', + }, + })); + + expect(hash1).toEqual(hash1prime); + + expect(hash1).not.toEqual(hash2); + expect(hash1).not.toEqual(hash3); + expect(hash1).not.toEqual(hash4); + expect(hash2).not.toEqual(hash3); + expect(hash2).not.toEqual(hash4); + expect(hash3).not.toEqual(hash4); + }); + + // eslint-disable-next-line max-len + function legacySynthWithAction(cb: (sourceArtifact: codepipeline.Artifact, cloudAssemblyArtifact: codepipeline.Artifact) => codepipeline.IAction) { + const _app = new TestApp({ outdir: OUTDIR }); + const _pipelineStack = new Stack(_app, 'PipelineStack', { env: PIPELINE_ENV }); + const _sourceArtifact = new codepipeline.Artifact(); + const _cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); + + new LegacyTestGitHubNpmPipeline(_pipelineStack, 'Cdk', { + sourceArtifact: _sourceArtifact, + cloudAssemblyArtifact: _cloudAssemblyArtifact, + synthAction: cb(_sourceArtifact, _cloudAssemblyArtifact), + }); + + return captureProjectConfigHash(_pipelineStack); + } + + function modernSynthWithAction(cb: () => ModernTestGitHubNpmPipelineProps) { + const _app = new TestApp({ outdir: OUTDIR }); + const _pipelineStack = new Stack(_app, 'PipelineStack', { env: PIPELINE_ENV }); + + new ModernTestGitHubNpmPipeline(_pipelineStack, 'Cdk', cb()); + + return captureProjectConfigHash(_pipelineStack); + } + + function captureProjectConfigHash(_pipelineStack: Stack) { + const theHash = Capture.aString(); + expect(_pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Build', + Actions: [ + objectLike({ + Name: 'Synth', + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { + name: '_PROJECT_CONFIG_HASH', + type: 'PLAINTEXT', + value: theHash.capture(), + }, + ]), + }), + }), + ], + }), + }); + + return theHash.capturedValue; + } +}); + +behavior('Synth CodeBuild project role can be granted permissions', (suite) => { + let bucket: s3.IBucket; + beforeEach(() => { + bucket = s3.Bucket.fromBucketArn(pipelineStack, 'Bucket', 'arn:aws:s3:::ThisParticularBucket'); + }); + + + suite.legacy(() => { + // GIVEN + const synthAction = cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + }); + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction, + }); + + // WHEN + bucket.grantRead(synthAction); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // GIVEN + const pipe = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipe.buildPipeline(); + + // WHEN + bucket.grantRead(pipe.synthProject); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(deepObjectLike({ + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Resource: ['arn:aws:s3:::ThisParticularBucket', 'arn:aws:s3:::ThisParticularBucket/*'], + })), + }, + }); + } +}); + +behavior('Synth can reference an imported ECR repo', (suite) => { + // Repro from https://github.com/aws/aws-cdk/issues/10535 + + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + environment: { + buildImage: cbuild.LinuxBuildImage.fromEcrRepository( + ecr.Repository.fromRepositoryName(pipelineStack, 'ECRImage', 'my-repo-name'), + ), + }, + }), + }); + + // THEN -- no exception (necessary for linter) + expect(true).toBeTruthy(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.CodeBuildStep('Synth', { + commands: ['build'], + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: 'cdk.out', + buildEnvironment: { + buildImage: cbuild.LinuxBuildImage.fromEcrRepository( + ecr.Repository.fromRepositoryName(pipelineStack, 'ECRImage', 'my-repo-name'), + ), + }, + }), + }); + + // THEN -- no exception (necessary for linter) + expect(true).toBeTruthy(); + }); +}); + +behavior('CodeBuild: Can specify additional policy statements', (suite) => { + suite.legacy(() => { + // WHEN + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], + resources: ['arn:my:arn'], + }), + ], + }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.CodeBuildStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + primaryOutputDirectory: '.', + commands: ['synth'], + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['codeartifact:*', 'sts:GetServiceBearerToken'], + resources: ['arn:my:arn'], + }), + ], + }), + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(deepObjectLike({ + Action: [ + 'codeartifact:*', + 'sts:GetServiceBearerToken', + ], + Resource: 'arn:my:arn', + })), + }, + }); + } +}); + +behavior('Multiple input sources in side-by-side directories', (suite) => { + // Legacy API does not support this + suite.doesNotApply.legacy(); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.ShellStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + commands: ['false'], + additionalInputs: { + '../sibling': cdkp.CodePipelineSource.gitHub('foo/bar', 'main'), + 'sub': new cdkp.ShellStep('Prebuild', { + input: cdkp.CodePipelineSource.gitHub('pre/build', 'main'), + commands: ['true'], + primaryOutputDirectory: 'built', + }), + }, + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith( + { + Name: 'Source', + Actions: [ + objectLike({ Configuration: objectLike({ Repo: 'bar' }) }), + objectLike({ Configuration: objectLike({ Repo: 'build' }) }), + objectLike({ Configuration: objectLike({ Repo: 'test' }) }), + ], + }, + { + Name: 'Build', + Actions: [ + objectLike({ Name: 'Prebuild', RunOrder: 1 }), + objectLike({ + Name: 'Synth', + RunOrder: 2, + InputArtifacts: [ + // 3 input artifacts + anything(), + anything(), + anything(), + ], + }), + ], + }, + ), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + install: { + commands: [ + 'ln -s "$CODEBUILD_SRC_DIR_foo_bar_Source" "../sibling"', + 'ln -s "$CODEBUILD_SRC_DIR_Prebuild_Output" "sub"', + ], + }, + build: { + commands: [ + 'false', + ], + }, + }, + })), + }, + }); + }); +}); + +behavior('Can easily switch on privileged mode for synth', (suite) => { + // Legacy API does not support this + suite.doesNotApply.legacy(); + + suite.modern(() => { + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + dockerEnabledForSynth: true, + commands: ['LookAtMe'], + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: objectLike({ + PrivilegedMode: true, + }), + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'LookAtMe', + ], + }, + }, + })), + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts b/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts new file mode 100644 index 0000000000000..447e22da59124 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts @@ -0,0 +1,799 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { anything, arrayWith, Capture, deepObjectLike, encodedJson, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { CodePipelineSource, ShellStep } from '../../lib'; +import { AppWithOutput, behavior, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, sortedByRunOrder, StageWithStackOutput, stringNoLongerThan, TestApp, TwoStackApp } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('can add manual approval after app', (suite) => { + // No need to be backwards compatible + suite.doesNotApply.legacy(); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoStackApp(app, 'MyApp'), { + post: [ + new cdkp.ManualApprovalStep('Approve'), + ], + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'MyApp', + Actions: sortedByRunOrder([ + objectLike({ Name: 'Stack1.Prepare' }), + objectLike({ Name: 'Stack1.Deploy' }), + objectLike({ Name: 'Stack2.Prepare' }), + objectLike({ Name: 'Stack2.Deploy' }), + objectLike({ Name: 'Approve' }), + ]), + }), + }); + }); +}); + +behavior('can add steps to wave', (suite) => { + // No need to be backwards compatible + suite.doesNotApply.legacy(); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const wave = pipeline.addWave('MyWave', { + post: [ + new cdkp.ManualApprovalStep('Approve'), + ], + }); + wave.addStage(new OneStackApp(pipelineStack, 'Stage1')); + wave.addStage(new OneStackApp(pipelineStack, 'Stage2')); + wave.addStage(new OneStackApp(pipelineStack, 'Stage3')); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'MyWave', + Actions: sortedByRunOrder([ + objectLike({ Name: 'Stage1.Stack.Prepare' }), + objectLike({ Name: 'Stage2.Stack.Prepare' }), + objectLike({ Name: 'Stage3.Stack.Prepare' }), + objectLike({ Name: 'Stage1.Stack.Deploy' }), + objectLike({ Name: 'Stage2.Stack.Deploy' }), + objectLike({ Name: 'Stage3.Stack.Deploy' }), + objectLike({ Name: 'Approve' }), + ]), + }), + }); + }); +}); + + +behavior('script validation steps can use stack outputs as environment variables', (suite) => { + suite.legacy(() => { + // GIVEN + const { pipeline } = legacySetup(); + const stage = new StageWithStackOutput(app, 'MyApp'); + + // WHEN + const pipeStage = pipeline.addApplicationStage(stage); + pipeStage.addActions(new cdkp.ShellScriptAction({ + actionName: 'TestOutput', + useOutputs: { + BUCKET_NAME: pipeline.stackOutput(stage.output), + }, + commands: ['echo $BUCKET_NAME'], + })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'MyApp', + Actions: arrayWith( + deepObjectLike({ + Name: 'Stack.Deploy', + OutputArtifacts: [{ Name: anything() }], + Configuration: { + OutputFileName: 'outputs.json', + }, + }), + deepObjectLike({ + ActionTypeId: { + Provider: 'CodeBuild', + }, + Configuration: { + ProjectName: anything(), + }, + InputArtifacts: [{ Name: anything() }], + Name: 'TestOutput', + }), + ), + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'set -eu', + 'export BUCKET_NAME="$(node -pe \'require(process.env.CODEBUILD_SRC_DIR + "/outputs.json")["BucketName"]\')"', + 'echo $BUCKET_NAME', + ], + }, + }, + })), + Type: 'CODEPIPELINE', + }, + }); + }); + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const myApp = new AppWithOutput(app, 'Alpha'); + pipeline.addStage(myApp, { + post: [ + new cdkp.ShellStep('Approve', { + commands: ['/bin/true'], + envFromCfnOutputs: { + THE_OUTPUT: myApp.theOutput, + }, + }), + ], + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Alpha', + Actions: arrayWith( + objectLike({ + Name: 'Stack.Deploy', + Namespace: 'AlphaStack6B3389FA', + }), + objectLike({ + Name: 'Approve', + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { name: 'THE_OUTPUT', value: '#{AlphaStack6B3389FA.MyOutput}', type: 'PLAINTEXT' }, + ]), + }), + }), + ), + }), + }); + }); +}); + +behavior('stackOutput generates names limited to 100 characters', (suite) => { + suite.legacy(() => { + const { pipeline } = legacySetup(); + const stage = new StageWithStackOutput(app, 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild'); + const pipeStage = pipeline.addApplicationStage(stage); + pipeStage.addActions(new cdkp.ShellScriptAction({ + actionName: 'TestOutput', + useOutputs: { + BUCKET_NAME: pipeline.stackOutput(stage.output), + }, + commands: ['echo $BUCKET_NAME'], + })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', + Actions: arrayWith( + deepObjectLike({ + Name: 'Stack.Deploy', + OutputArtifacts: [{ Name: stringNoLongerThan(100) }], + Configuration: { + OutputFileName: 'outputs.json', + }, + }), + deepObjectLike({ + ActionTypeId: { + Provider: 'CodeBuild', + }, + Configuration: { + ProjectName: anything(), + }, + InputArtifacts: [{ Name: stringNoLongerThan(100) }], + Name: 'TestOutput', + }), + ), + }), + }); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const stage = new StageWithStackOutput(app, 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild'); + pipeline.addStage(stage, { + post: [ + new cdkp.ShellStep('TestOutput', { + commands: ['echo $BUCKET_NAME'], + envFromCfnOutputs: { + BUCKET_NAME: stage.output, + }, + }), + ], + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', + Actions: arrayWith( + deepObjectLike({ + Name: 'Stack.Deploy', + Namespace: stringNoLongerThan(100), + }), + ), + }), + }); + }); +}); + +behavior('validation step can run from scripts in source', (suite) => { + suite.legacy(() => { + const { pipeline, sourceArtifact } = legacySetup(); + + // WHEN + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'UseSources', + additionalArtifacts: [sourceArtifact], + commands: ['true'], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [ + new cdkp.ShellStep('UseSources', { + input: pipeline.gitHubSource, + commands: ['set -eu', 'true'], + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + const sourceArtifact = Capture.aString(); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Source', + Actions: [ + deepObjectLike({ + OutputArtifacts: [{ Name: sourceArtifact.capture() }], + }), + ], + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: arrayWith( + deepObjectLike({ + Name: 'UseSources', + InputArtifacts: [{ Name: sourceArtifact.capturedValue }], + }), + ), + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'set -eu', + 'true', + ], + }, + }, + })), + }, + }); + } +}); + +behavior('can use additional output artifacts from build', (suite) => { + suite.legacy(() => { + // WHEN + const { pipeline, integTestArtifact } = legacySetup(); + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'UseBuildArtifact', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const synth = new ShellStep('Synth', { + input: CodePipelineSource.gitHub('test/test', 'main'), + commands: ['synth'], + }); + + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth, + }); + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [ + new cdkp.ShellStep('UseBuildArtifact', { + input: synth.addOutputDirectory('test'), + commands: ['set -eu', 'true'], + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + const integArtifact = Capture.aString(); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Build', + Actions: [ + deepObjectLike({ + Name: 'Synth', + OutputArtifacts: [ + { Name: anything() }, // It's not the first output + { Name: integArtifact.capture() }, + ], + }), + ], + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: arrayWith( + deepObjectLike({ + Name: 'UseBuildArtifact', + InputArtifacts: [{ Name: integArtifact.capturedValue }], + }), + ), + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'set -eu', + 'true', + ], + }, + }, + })), + }, + }); + } +}); + +behavior('can add policy statements to shell script action', (suite) => { + suite.legacy(() => { + // WHEN + const { pipeline, integTestArtifact } = legacySetup(); + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'Boop', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['s3:Banana'], + resources: ['*'], + }), + ], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [ + new cdkp.CodeBuildStep('Boop', { + commands: ['true'], + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['s3:Banana'], + resources: ['*'], + }), + ], + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(deepObjectLike({ + Action: 's3:Banana', + Resource: '*', + })), + }, + }); + } +}); + +behavior('can grant permissions to shell script action', (suite) => { + let bucket: s3.IBucket; + beforeEach(() => { + bucket = s3.Bucket.fromBucketArn(pipelineStack, 'Bucket', 'arn:aws:s3:::ThisParticularBucket'); + }); + + suite.legacy(() => { + const { pipeline, integTestArtifact } = legacySetup(); + const action = new cdkp.ShellScriptAction({ + actionName: 'Boop', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + }); + pipeline.addStage('Test').addActions(action); + + // WHEN + bucket.grantRead(action); + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + const codeBuildStep = new cdkp.CodeBuildStep('Boop', { + commands: ['true'], + }); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [codeBuildStep], + }); + + pipeline.buildPipeline(); + + // WHEN + bucket.grantRead(codeBuildStep.project); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(deepObjectLike({ + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Resource: ['arn:aws:s3:::ThisParticularBucket', 'arn:aws:s3:::ThisParticularBucket/*'], + })), + }, + }); + } +}); + +behavior('can run shell script actions in a VPC', (suite) => { + let vpc: ec2.Vpc; + beforeEach(() => { + vpc = new ec2.Vpc(pipelineStack, 'VPC'); + }); + + suite.legacy(() => { + const { pipeline, integTestArtifact } = legacySetup(); + + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + vpc, + actionName: 'VpcAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // All CodeBuild jobs automatically go into the VPC + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { vpc }, + }); + + pipeline.addStage(new TwoStackApp(app, 'MyApp'), { + post: [new cdkp.ShellStep('VpcAction', { + commands: ['set -eu', 'true'], + })], + }); + + THEN_codePipelineExpectation(); + }); + + suite.additional('modern, alternate API', () => { + // Can also explicitly specify a VPC when going to the "full config" class + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + pipeline.addStage(new TwoStackApp(app, 'MyApp'), { + post: [new cdkp.CodeBuildStep('VpcAction', { + commands: ['set -eu', 'true'], + vpc, + })], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + VpcConfig: { + Subnets: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + { + Ref: 'VPCPrivateSubnet3Subnet3EDCD457', + }, + ], + VpcId: { + Ref: 'VPCB9E5F0B4', + }, + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: [ + 'set -eu', + 'true', + ], + }, + }, + })), + }, + }); + } +}); + +behavior('can run shell script actions with a specific SecurityGroup', (suite) => { + let vpc: ec2.Vpc; + let sg: ec2.SecurityGroup; + beforeEach(() => { + vpc = new ec2.Vpc(pipelineStack, 'VPC'); + sg = new ec2.SecurityGroup(pipelineStack, 'SG', { vpc }); + }); + + suite.legacy(() => { + // WHEN + const { pipeline, integTestArtifact } = legacySetup(); + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + vpc, + securityGroups: [sg], + actionName: 'sgAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // All CodeBuild jobs automatically go into the VPC + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [new cdkp.CodeBuildStep('sgAction', { + commands: ['set -eu', 'true'], + vpc, + securityGroups: [sg], + })], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: arrayWith( + deepObjectLike({ + Name: 'sgAction', + }), + ), + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + VpcConfig: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'SGADB53937', + 'GroupId', + ], + }, + ], + VpcId: { + Ref: 'VPCB9E5F0B4', + }, + }, + }); + } +}); + +behavior('can run scripts with specified BuildEnvironment', (suite) => { + suite.legacy(() => { + let { pipeline, integTestArtifact } = legacySetup(); + + // WHEN + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'imageAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_2_0 }, + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // Run all Build jobs with the given image + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + codeBuildDefaults: { + buildEnvironment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_2_0, + }, + }, + }); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [new cdkp.ShellStep('imageAction', { + commands: ['true'], + })], + }); + + THEN_codePipelineExpectation(); + }); + + suite.additional('modern, alternative API', () => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [new cdkp.CodeBuildStep('imageAction', { + commands: ['true'], + buildEnvironment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_2_0, + }, + })], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:2.0', + }, + }); + } +}); + +behavior('can run scripts with magic environment variables', (suite) => { + suite.legacy(() => { + const { pipeline, integTestArtifact } = legacySetup(); + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'imageAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + environmentVariables: { + VERSION: { value: codepipeline.GlobalVariables.executionId }, + }, + })); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // Run all Build jobs with the given image + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + pipeline.addStage(new TwoStackApp(app, 'Test'), { + post: [new cdkp.ShellStep('imageAction', { + commands: ['true'], + env: { + VERSION: codepipeline.GlobalVariables.executionId, + }, + })], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: arrayWith( + objectLike({ + Name: 'imageAction', + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { + name: 'VERSION', + type: 'PLAINTEXT', + value: '#{codepipeline.PipelineExecutionId}', + }, + ]), + }), + }), + ), + }), + }); + } +}); + + +/** + * Some shared setup for legacy API tests + */ +function legacySetup() { + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); + const integTestArtifact = new codepipeline.Artifact('IntegTests'); + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + additionalArtifacts: [{ directory: 'test', artifact: integTestArtifact }], + }), + }); + + return { sourceArtifact, cloudAssemblyArtifact, integTestArtifact, pipeline }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/cross-environment-infra.test.ts b/packages/@aws-cdk/pipelines/test/cross-environment-infra.test.ts deleted file mode 100644 index e442e540b1ac9..0000000000000 --- a/packages/@aws-cdk/pipelines/test/cross-environment-infra.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { arrayWith, objectLike, stringLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import { Stack, Stage, StageProps } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('in a cross-account/cross-region setup, artifact bucket can be read by deploy role', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new TestApplication(app, 'MyApp', { - env: { account: '321elsewhere', region: 'us-elsewhere' }, - })); - - // THEN - app.synth(); - const supportStack = app.node.findAll().filter(Stack.isStack).find(s => s.stackName === 'PipelineStack-support-us-elsewhere'); - expect(supportStack).not.toBeUndefined(); - - expect(supportStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: arrayWith('s3:GetObject*', 's3:GetBucket*', 's3:List*'), - Principal: { - AWS: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - stringLike('*-deploy-role-*'), - ]], - }, - }, - })), - }, - }); - - // And the key to go along with it - expect(supportStack).toHaveResourceLike('AWS::KMS::Key', { - KeyPolicy: { - Statement: arrayWith(objectLike({ - Action: arrayWith('kms:Decrypt', 'kms:DescribeKey'), - Principal: { - AWS: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - stringLike('*-deploy-role-*'), - ]], - }, - }, - })), - }, - }); - }); -}); - -behavior('in a cross-account/same-region setup, artifact bucket can be read by deploy role', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new TestApplication(app, 'MyApp', { - env: { account: '321elsewhere', region: PIPELINE_ENV.region }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - Principal: { - AWS: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - stringLike('*-deploy-role-*'), - ]], - }, - }, - })), - }, - }); - }); -}); - -behavior('in an unspecified-account setup, artifact bucket can be read by deploy role', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new TestApplication(app, 'MyApp', {})); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - Principal: { - AWS: { - 'Fn::Join': ['', arrayWith( - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - stringLike('*-deploy-role-*'), - )], - }, - }, - })), - }, - }); - }); -}); - -behavior('in a same-account setup, artifact bucket can be read by deploy role', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new TestApplication(app, 'MyApp', { - env: PIPELINE_ENV, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { - PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - Principal: { - AWS: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - stringLike('*-deploy-role-*'), - ]], - }, - }, - })), - }, - }); - }); -}); - -/** - * Our application - */ -class TestApplication extends Stage { - constructor(scope: Construct, id: string, props: StageProps) { - super(scope, id, props); - new BucketStack(this, 'Stack'); - } -} diff --git a/packages/@aws-cdk/pipelines/test/existing-pipeline.test.ts b/packages/@aws-cdk/pipelines/test/existing-pipeline.test.ts deleted file mode 100644 index b5ad74c799b77..0000000000000 --- a/packages/@aws-cdk/pipelines/test/existing-pipeline.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as cp from '@aws-cdk/aws-codepipeline'; -import { Stack } from '@aws-cdk/core'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { PIPELINE_ENV, TestApp, TestGitHubAction } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let sourceArtifact: cp.Artifact; -let cloudAssemblyArtifact: cp.Artifact; -let codePipeline: cp.Pipeline; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - sourceArtifact = new cp.Artifact(); - cloudAssemblyArtifact = new cp.Artifact(); -}); - -afterEach(() => { - app.cleanup(); -}); - -describe('with empty existing CodePipeline', () => { - beforeEach(() => { - codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline'); - }); - - behavior('both actions are required', (suite) => { - suite.legacy(() => { - // WHEN - expect(() => { - new cdkp.CdkPipeline(pipelineStack, 'Cdk', { cloudAssemblyArtifact, codePipeline }); - }).toThrow(/You must pass a 'sourceAction'/); - }); - }); - - behavior('can give both actions', (suite) => { - suite.legacy(() => { - // WHEN - new cdkp.CdkPipeline(pipelineStack, 'Cdk', { - cloudAssemblyArtifact, - codePipeline, - sourceAction: new TestGitHubAction(sourceArtifact), - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'Source' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - ], - }); - }); - }); -}); - -describe('with custom Source stage in existing Pipeline', () => { - beforeEach(() => { - codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { - stages: [ - { - stageName: 'CustomSource', - actions: [new TestGitHubAction(sourceArtifact)], - }, - ], - }); - }); - - behavior('Work with synthAction', (suite) => { - suite.legacy(() => { - // WHEN - new cdkp.CdkPipeline(pipelineStack, 'Cdk', { - codePipeline, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact }), - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'CustomSource' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - ], - }); - }); - }); -}); - -describe('with Source and Build stages in existing Pipeline', () => { - beforeEach(() => { - codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { - stages: [ - { - stageName: 'CustomSource', - actions: [new TestGitHubAction(sourceArtifact)], - }, - { - stageName: 'CustomBuild', - actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], - }, - ], - }); - }); - - behavior('can supply no actions', (suite) => { - suite.legacy(() => { - // WHEN - new cdkp.CdkPipeline(pipelineStack, 'Cdk', { - codePipeline, - cloudAssemblyArtifact, - }); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'CustomSource' }), - objectLike({ Name: 'CustomBuild' }), - objectLike({ Name: 'UpdatePipeline' }), - ], - }); - }); - }); -}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json new file mode 100644 index 0000000000000..27f47b7a985a2 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json @@ -0,0 +1,2336 @@ +{ + "Resources": { + "PipelineArtifactsBucketAEA9A052": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucketPolicyF53CCC52": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleB27FAA37": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicy7BDC1ABB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicy7BDC1ABB", + "Roles": [ + { + "Ref": "PipelineRoleB27FAA37" + } + ] + } + }, + "Pipeline9850B417": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleB27FAA37", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "rix0rrr", + "Repo": "cdk-pipelines-demo", + "Branch": "main", + "OAuthToken": "{{resolve:secretsmanager:github-token:SecretString:::}}", + "PollForSourceChanges": false + }, + "Name": "rix0rrr_cdk-pipelines-demo", + "OutputArtifacts": [ + { + "Name": "rix0rrr_cdk-pipelines-demo_Source" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"00ebacfb32b1bde8d3638577308e7b7144dfa3b0a58a83bc6ff38a3b1f26951c\"}]" + }, + "InputArtifacts": [ + { + "Name": "rix0rrr_cdk-pipelines-demo_Source" + } + ], + "Name": "Synth", + "OutputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"9eda7f97d24aac861052bb47a41b80eecdd56096bf9a88a27c88d94c463785c8\"}]" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "SelfMutate", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "UpdatePipeline" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Beta/PipelineStackBetaStack2C79AD00A.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + } + ], + "Name": "Beta" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod1-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod1/PipelineStackProd1Stack14013D698.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod1.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod2-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod2/PipelineStackProd2Stack1FD464162.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod2.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod1-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod1.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod2-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod2.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod1-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod1/PipelineStackProd1Stack2F0681AFF.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod1.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod2-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod2/PipelineStackProd2Stack2176123EB.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod2.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod1-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod1.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod2-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod2.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + } + ], + "Name": "Wave1" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod3-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod3/PipelineStackProd3Stack1795F3D43.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod3.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod4-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod4/PipelineStackProd4Stack118F74ADB.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod4.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod5-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod5/PipelineStackProd5Stack1E7E4E4C6.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod5.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod6-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod6/PipelineStackProd6Stack1E7C34314.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod6.Stack1.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod3-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod3.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod4-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod4.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod5-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod5.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod6-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod6.Stack1.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod3-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod3/PipelineStackProd3Stack2DFBBA0B2.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod3.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod4-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod4/PipelineStackProd4Stack2E2CB4ED3.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod4.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod5-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod5/PipelineStackProd5Stack2C39BEE5B.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod5.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod6-Stack2", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Prod6/PipelineStackProd6Stack2BED1BBCE.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prod6.Stack2.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod3-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod3.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod4-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod4.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod5-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod5.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Prod6-Stack2", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Prod6.Stack2.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 4 + } + ], + "Name": "Wave2" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "Type": "S3" + }, + "RestartExecutionOnUpdate": true + }, + "DependsOn": [ + "PipelineRoleDefaultPolicy7BDC1ABB", + "PipelineRoleB27FAA37" + ] + }, + "PipelineSourcerix0rrrcdkpipelinesdemoWebhookResourceDB0C1BCA": { + "Type": "AWS::CodePipeline::Webhook", + "Properties": { + "Authentication": "GITHUB_HMAC", + "AuthenticationConfiguration": { + "SecretToken": "{{resolve:secretsmanager:github-token:SecretString:::}}" + }, + "Filters": [ + { + "JsonPath": "$.ref", + "MatchEquals": "refs/heads/{Branch}" + } + ], + "TargetAction": "rix0rrr_cdk-pipelines-demo", + "TargetPipeline": { + "Ref": "Pipeline9850B417" + }, + "TargetPipelineVersion": 1, + "RegisterWithThirdParty": true + } + }, + "PipelineBuildSynthCdkBuildProjectRole231EEA2A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C", + "Roles": [ + { + "Ref": "PipelineBuildSynthCdkBuildProjectRole231EEA2A" + } + ] + } + }, + "PipelineBuildSynthCdkBuildProject6BEFA8E6": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectRole231EEA2A", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm ci\",\n \"npm run build\",\n \"npx cdk synth\"\n ]\n }\n },\n \"artifacts\": {\n \"base-directory\": \"cdk.out\",\n \"files\": \"**/*\"\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3" + } + }, + "PipelineBuildSynthCodePipelineActionRole4E7A6C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProject6BEFA8E6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290", + "Roles": [ + { + "Ref": "PipelineBuildSynthCodePipelineActionRole4E7A6C97" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleDefaultPolicyE626265B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationDAA41400", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleDefaultPolicyE626265B", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutationRole57E559E8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Condition": { + "ForAnyValue:StringEquals": { + "iam:ResourceTag/aws-cdk:bootstrap-role": [ + "image-publishing", + "file-publishing", + "deploy" + ] + } + }, + "Effect": "Allow", + "Resource": "arn:*:iam::12345678:role/*" + }, + { + "Action": "cloudformation:DescribeStacks", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:ListBucket", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutationRole57E559E8" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutationDAA41400": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationRole57E559E8", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline.ts b/packages/@aws-cdk/pipelines/test/integ.newpipeline.ts new file mode 100644 index 0000000000000..b777b61a23e09 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline.ts @@ -0,0 +1,62 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +/// !cdk-integ PipelineStack +import * as sqs from '@aws-cdk/aws-sqs'; +import { App, Stack, StackProps, Stage, StageProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as pipelines from '../lib'; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub('rix0rrr/cdk-pipelines-demo', 'main'), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), + }); + + pipeline.addStage(new AppStage(this, 'Beta')); + + const group = pipeline.addWave('Wave1'); + group.addStage(new AppStage(this, 'Prod1')); + group.addStage(new AppStage(this, 'Prod2')); + + const group2 = pipeline.addWave('Wave2'); + group2.addStage(new AppStage(this, 'Prod3')); + group2.addStage(new AppStage(this, 'Prod4')); + group2.addStage(new AppStage(this, 'Prod5')); + group2.addStage(new AppStage(this, 'Prod6')); + } +} + +class AppStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack1 = new Stack(this, 'Stack1'); + const queue1 = new sqs.Queue(stack1, 'Queue'); + + const stack2 = new Stack(this, 'Stack2'); + new sqs.Queue(stack2, 'OtherQueue', { + deadLetterQueue: { + queue: queue1, + maxReceiveCount: 5, + }, + }); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, +}); +new PipelineStack(app, 'PipelineStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +}); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts index a4f35010a10b7..e5461ebe6efe1 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets-single-upload.ts @@ -14,10 +14,10 @@ class MyStage extends Stage { const stack = new Stack(this, 'Stack', props); new s3_assets.Asset(stack, 'Asset', { - path: path.join(__dirname, 'test-file-asset.txt'), + path: path.join(__dirname, 'testhelpers/assets/test-file-asset.txt'), }); new s3_assets.Asset(stack, 'Asset2', { - path: path.join(__dirname, 'test-file-asset-two.txt'), + path: path.join(__dirname, 'testhelpers/assets/test-file-asset-two.txt'), }); new CfnResource(stack, 'Resource', { diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts index ae9f5046137d4..41b2e6ae0cdc2 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.ts @@ -14,10 +14,10 @@ class MyStage extends Stage { const stack = new Stack(this, 'Stack', props); new s3_assets.Asset(stack, 'Asset', { - path: path.join(__dirname, 'test-file-asset.txt'), + path: path.join(__dirname, 'testhelpers/assets/test-file-asset.txt'), }); new s3_assets.Asset(stack, 'Asset2', { - path: path.join(__dirname, 'test-file-asset-two.txt'), + path: path.join(__dirname, 'testhelpers/assets/test-file-asset-two.txt'), }); new CfnResource(stack, 'Resource', { diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline.ts index b79dd24841472..f263e65a7f09c 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline.ts +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline.ts @@ -80,4 +80,4 @@ const app = new App({ new CdkpipelinesDemoPipelineStack(app, 'PipelineStack', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, }); -app.synth(); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/fs.test.ts b/packages/@aws-cdk/pipelines/test/legacy/fs.test.ts similarity index 85% rename from packages/@aws-cdk/pipelines/test/fs.test.ts rename to packages/@aws-cdk/pipelines/test/legacy/fs.test.ts index 49cbe2458e64a..da49fa9cf2986 100644 --- a/packages/@aws-cdk/pipelines/test/fs.test.ts +++ b/packages/@aws-cdk/pipelines/test/legacy/fs.test.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { toPosixPath } from '../lib/private/fs'; +import { toPosixPath } from '../../lib/private/fs'; test('translate path.sep', () => { expect(toPosixPath(`a${path.sep}b${path.sep}c`)).toEqual('a/b/c'); diff --git a/packages/@aws-cdk/pipelines/test/pipeline.test.ts b/packages/@aws-cdk/pipelines/test/pipeline.test.ts deleted file mode 100644 index fdb20d19ae396..0000000000000 --- a/packages/@aws-cdk/pipelines/test/pipeline.test.ts +++ /dev/null @@ -1,563 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { - anything, - arrayWith, - Capture, - deepObjectLike, - encodedJson, - notMatching, - objectLike, - stringLike, -} from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as cp from '@aws-cdk/aws-codepipeline'; -import * as cpa from '@aws-cdk/aws-codepipeline-actions'; -import { Stack, Stage, StageProps, SecretValue, Tags } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { behavior } from './helpers/compliance'; -import { BucketStack, PIPELINE_ENV, stackTemplate, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('references stack template in subassembly', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'App')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'App', - Actions: arrayWith( - objectLike({ - Name: 'Stack.Prepare', - InputArtifacts: [objectLike({})], - Configuration: objectLike({ - StackName: 'App-Stack', - TemplatePath: stringLike('*::assembly-App/*.template.json'), - }), - }), - ), - }), - }); - }); - -}); - -behavior('obvious error is thrown when stage contains no stacks', (suite) => { - suite.legacy(() => { - // WHEN - expect(() => { - pipeline.addApplicationStage(new Stage(app, 'EmptyStage')); - }).toThrow(/should contain at least one Stack/); - }); -}); - -behavior('action has right settings for same-env deployment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'Same')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Same', - Actions: [ - objectLike({ - Name: 'Stack.Prepare', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-deploy-role-', - { Ref: 'AWS::AccountId' }, - '-', - { Ref: 'AWS::Region' }, - ]], - }, - Configuration: objectLike({ - StackName: 'Same-Stack', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-cfn-exec-role-', - { Ref: 'AWS::AccountId' }, - '-', - { Ref: 'AWS::Region' }, - ]], - }, - }), - }), - objectLike({ - Name: 'Stack.Deploy', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-deploy-role-', - { Ref: 'AWS::AccountId' }, - '-', - { Ref: 'AWS::Region' }, - ]], - }, - Configuration: objectLike({ - StackName: 'Same-Stack', - }), - }), - ], - }), - }); - }); -}); - -behavior('action has right settings for cross-account deployment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'CrossAccount', { env: { account: 'you' } })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'CrossAccount', - Actions: [ - objectLike({ - Name: 'Stack.Prepare', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-deploy-role-you-', - { Ref: 'AWS::Region' }, - ]], - }, - Configuration: objectLike({ - StackName: 'CrossAccount-Stack', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-cfn-exec-role-you-', - { Ref: 'AWS::Region' }, - ]], - }, - }), - }), - objectLike({ - Name: 'Stack.Deploy', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-deploy-role-you-', - { Ref: 'AWS::Region' }, - ]], - }, - Configuration: objectLike({ - StackName: 'CrossAccount-Stack', - }), - }), - ], - }), - }); - }); -}); - -behavior('action has right settings for cross-region deployment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'CrossRegion', { env: { region: 'elsewhere' } })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'CrossRegion', - Actions: [ - objectLike({ - Name: 'Stack.Prepare', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-deploy-role-', - { Ref: 'AWS::AccountId' }, - '-elsewhere', - ]], - }, - Region: 'elsewhere', - Configuration: objectLike({ - StackName: 'CrossRegion-Stack', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-cfn-exec-role-', - { Ref: 'AWS::AccountId' }, - '-elsewhere', - ]], - }, - }), - }), - objectLike({ - Name: 'Stack.Deploy', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::', - { Ref: 'AWS::AccountId' }, - ':role/cdk-hnb659fds-deploy-role-', - { Ref: 'AWS::AccountId' }, - '-elsewhere', - ]], - }, - Region: 'elsewhere', - Configuration: objectLike({ - StackName: 'CrossRegion-Stack', - }), - }), - ], - }), - }); - }); -}); - -behavior('action has right settings for cross-account/cross-region deployment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackApp(app, 'CrossBoth', { - env: { - account: 'you', - region: 'elsewhere', - }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'CrossBoth', - Actions: [ - objectLike({ - Name: 'Stack.Prepare', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-deploy-role-you-elsewhere', - ]], - }, - Region: 'elsewhere', - Configuration: objectLike({ - StackName: 'CrossBoth-Stack', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-cfn-exec-role-you-elsewhere', - ]], - }, - }), - }), - objectLike({ - Name: 'Stack.Deploy', - RoleArn: { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':iam::you:role/cdk-hnb659fds-deploy-role-you-elsewhere', - ]], - }, - Region: 'elsewhere', - Configuration: objectLike({ - StackName: 'CrossBoth-Stack', - }), - }), - ], - }), - }); - }); -}); - -behavior('pipeline has self-mutation stage', (suite) => { - suite.legacy(() => { - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'UpdatePipeline', - Actions: [ - objectLike({ - Name: 'SelfMutate', - Configuration: objectLike({ - ProjectName: { Ref: anything() }, - }), - }), - ], - }), - }); - - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - PrivilegedMode: false, - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: ['npm install -g aws-cdk'], - }, - build: { - commands: arrayWith('cdk -a . deploy PipelineStack --require-approval=never --verbose'), - }, - }, - })), - Type: 'CODEPIPELINE', - }, - }); - }); -}); - -behavior('selfmutation stage correctly identifies nested assembly of pipeline stack', (suite) => { - suite.legacy(() => { - const pipelineStage = new Stage(app, 'PipelineStage'); - const nestedPipelineStack = new Stack(pipelineStage, 'PipelineStack', { env: PIPELINE_ENV }); - new TestGitHubNpmPipeline(nestedPipelineStack, 'Cdk'); - - // THEN - expect(stackTemplate(nestedPipelineStack)).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: arrayWith('cdk -a assembly-PipelineStage deploy PipelineStage/PipelineStack --require-approval=never --verbose'), - }, - }, - })), - }, - }); - }); -}); - -behavior('selfmutation feature can be turned off', (suite) => { - suite.legacy(() => { - const stack = new Stack(); - const cloudAssemblyArtifact = new cp.Artifact(); - // WHEN - new TestGitHubNpmPipeline(stack, 'Cdk', { - cloudAssemblyArtifact, - selfMutating: false, - }); - // THEN - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: notMatching(arrayWith({ - Name: 'UpdatePipeline', - Actions: anything(), - })), - }); - }); -}); - -behavior('generates CodeBuild project in privileged mode', (suite) => { - suite.legacy(() => { - // WHEN - const stack = new Stack(app, 'PrivilegedPipelineStack', { env: PIPELINE_ENV }); - new TestGitHubNpmPipeline(stack, 'PrivilegedPipeline', { - supportDockerAssets: true, - }); - - // THEN - expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - PrivilegedMode: true, - }, - }); - }); -}); - -behavior('overridden stack names are respected', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addApplicationStage(new OneStackAppWithCustomName(app, 'App1')); - pipeline.addApplicationStage(new OneStackAppWithCustomName(app, 'App2')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith( - { - Name: 'App1', - Actions: arrayWith(objectLike({ - Name: 'MyFancyStack.Prepare', - Configuration: objectLike({ - StackName: 'MyFancyStack', - }), - })), - }, - { - Name: 'App2', - Actions: arrayWith(objectLike({ - Name: 'MyFancyStack.Prepare', - Configuration: objectLike({ - StackName: 'MyFancyStack', - }), - })), - }, - ), - }); - }); -}); - -behavior('can control fix/CLI version used in pipeline selfupdate', (suite) => { - suite.legacy(() => { - // WHEN - const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); - new TestGitHubNpmPipeline(stack2, 'Cdk2', { - pipelineName: 'vpipe', - cdkCliVersion: '1.2.3', - }); - - // THEN - expect(stack2).toHaveResourceLike('AWS::CodeBuild::Project', { - Name: 'vpipe-selfupdate', - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: ['npm install -g aws-cdk@1.2.3'], - }, - }, - })), - }, - }); - }); -}); - -behavior('changing CLI version leads to a different pipeline structure (restarting it)', (suite) => { - suite.legacy(() => { - // GIVEN - const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); - const stack3 = new Stack(app, 'Stack3', { env: PIPELINE_ENV }); - const structure2 = Capture.anyType(); - const structure3 = Capture.anyType(); - - // WHEN - new TestGitHubNpmPipeline(stack2, 'Cdk', { - cdkCliVersion: '1.2.3', - }); - new TestGitHubNpmPipeline(stack3, 'Cdk', { - cdkCliVersion: '4.5.6', - }); - - // THEN - expect(stack2).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: structure2.capture(), - }); - expect(stack3).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: structure3.capture(), - }); - - expect(JSON.stringify(structure2.capturedValue)).not.toEqual(JSON.stringify(structure3.capturedValue)); - }); -}); - -behavior('add another action to an existing stage', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.stage('Source').addAction(new cpa.GitHubSourceAction({ - actionName: 'GitHub2', - oauthToken: SecretValue.plainText('oops'), - output: new cp.Artifact(), - owner: 'OWNER', - repo: 'REPO', - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Source', - Actions: [ - objectLike({ Name: 'GitHub' }), - objectLike({ Name: 'GitHub2' }), - ], - }), - }); - }); -}); - -behavior('tags get reflected in pipeline', (suite) => { - suite.legacy(() => { - // WHEN - const stage = new OneStackApp(app, 'App'); - Tags.of(stage).add('CostCenter', 'F00B4R'); - pipeline.addApplicationStage(stage); - - // THEN - const templateConfig = Capture.aString(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'App', - Actions: arrayWith( - objectLike({ - Name: 'Stack.Prepare', - InputArtifacts: [objectLike({})], - Configuration: objectLike({ - StackName: 'App-Stack', - TemplateConfiguration: templateConfig.capture(stringLike('*::assembly-App/*.template.*json')), - }), - }), - ), - }), - }); - - const [, relConfigFile] = templateConfig.capturedValue.split('::'); - const absConfigFile = path.join(app.outdir, relConfigFile); - const configFile = JSON.parse(fs.readFileSync(absConfigFile, { encoding: 'utf-8' })); - expect(configFile).toEqual(expect.objectContaining({ - Tags: { - CostCenter: 'F00B4R', - }, - })); - }); -}); - -class OneStackApp extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - new BucketStack(this, 'Stack'); - } -} - -class OneStackAppWithCustomName extends Stage { - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - new BucketStack(this, 'Stack', { - stackName: 'MyFancyStack', - }); - } -} diff --git a/packages/@aws-cdk/pipelines/test/test-docker-asset/Dockerfile b/packages/@aws-cdk/pipelines/test/testhelpers/assets/test-docker-asset/Dockerfile similarity index 100% rename from packages/@aws-cdk/pipelines/test/test-docker-asset/Dockerfile rename to packages/@aws-cdk/pipelines/test/testhelpers/assets/test-docker-asset/Dockerfile diff --git a/packages/@aws-cdk/pipelines/test/test-file-asset-two.txt b/packages/@aws-cdk/pipelines/test/testhelpers/assets/test-file-asset-two.txt similarity index 100% rename from packages/@aws-cdk/pipelines/test/test-file-asset-two.txt rename to packages/@aws-cdk/pipelines/test/testhelpers/assets/test-file-asset-two.txt diff --git a/packages/@aws-cdk/pipelines/test/test-file-asset.txt b/packages/@aws-cdk/pipelines/test/testhelpers/assets/test-file-asset.txt similarity index 100% rename from packages/@aws-cdk/pipelines/test/test-file-asset.txt rename to packages/@aws-cdk/pipelines/test/testhelpers/assets/test-file-asset.txt diff --git a/packages/@aws-cdk/pipelines/test/helpers/compliance.ts b/packages/@aws-cdk/pipelines/test/testhelpers/compliance.ts similarity index 62% rename from packages/@aws-cdk/pipelines/test/helpers/compliance.ts rename to packages/@aws-cdk/pipelines/test/testhelpers/compliance.ts index a152c1ef87b10..bf6603d4753cb 100644 --- a/packages/@aws-cdk/pipelines/test/helpers/compliance.ts +++ b/packages/@aws-cdk/pipelines/test/testhelpers/compliance.ts @@ -4,54 +4,43 @@ interface SkippedSuite { modern(reason?: string): void; } -interface ParameterizedSuite { - legacy(fn: (arg: any) => void): void; - - modern(fn: (arg: any) => void): void; -} - interface Suite { readonly doesNotApply: SkippedSuite; - each(cases: any[]): ParameterizedSuite; - legacy(fn: () => void): void; modern(fn: () => void): void; + + additional(description: string, fn: () => void): void; } // eslint-disable-next-line jest/no-export export function behavior(name: string, cb: (suite: Suite) => void) { // 'describe()' adds a nice grouping in Jest describe(name, () => { - const unwritten = new Set(['modern', 'legacy']); + + function scratchOff(flavor: string) { + if (!unwritten.has(flavor)) { + throw new Error(`Already had test for ${flavor}. Use .additional() to add more tests.`); + } + unwritten.delete(flavor); + } + + cb({ - each: (cases: any[]) => { - return { - legacy: (testFn) => { - unwritten.delete('legacy'); - describe('legacy', () => { - test.each(cases)(name, testFn); - }); - }, - modern: (testFn) => { - unwritten.delete('modern'); - test.each(cases)('modern', testFn); - }, - }; - }, legacy: (testFn) => { - unwritten.delete('legacy'); + scratchOff('legacy'); test('legacy', testFn); }, modern: (testFn) => { - unwritten.delete('modern'); + scratchOff('modern'); test('modern', testFn); }, + additional: test, doesNotApply: { modern: (reason?: string) => { - unwritten.delete('modern'); + scratchOff('modern'); if (reason != null) { // eslint-disable-next-line jest/no-disabled-tests @@ -60,7 +49,7 @@ export function behavior(name: string, cb: (suite: Suite) => void) { }, legacy: (reason?: string) => { - unwritten.delete('legacy'); + scratchOff('legacy'); if (reason != null) { // eslint-disable-next-line jest/no-disabled-tests diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/index.ts b/packages/@aws-cdk/pipelines/test/testhelpers/index.ts new file mode 100644 index 0000000000000..21ca108240f27 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/index.ts @@ -0,0 +1,5 @@ +export * from './compliance'; +export * from './legacy-pipeline'; +export * from './modern-pipeline'; +export * from './test-app'; +export * from './testmatchers'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/legacy-pipeline.ts b/packages/@aws-cdk/pipelines/test/testhelpers/legacy-pipeline.ts new file mode 100644 index 0000000000000..63ffec75b7188 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/legacy-pipeline.ts @@ -0,0 +1,48 @@ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import { SecretValue } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cdkp from '../../lib'; + +export interface LegacyTestGitHubNpmPipelineExtraProps { + readonly sourceArtifact?: codepipeline.Artifact; + readonly npmSynthOptions?: Partial; +} + +export class LegacyTestGitHubNpmPipeline extends cdkp.CdkPipeline { + public readonly sourceArtifact: codepipeline.Artifact; + public readonly cloudAssemblyArtifact: codepipeline.Artifact; + + constructor(scope: Construct, id: string, props?: Partial & LegacyTestGitHubNpmPipelineExtraProps) { + const sourceArtifact = props?.sourceArtifact ?? new codepipeline.Artifact(); + const cloudAssemblyArtifact = props?.cloudAssemblyArtifact ?? new codepipeline.Artifact(); + + super(scope, id, { + sourceAction: new TestGitHubAction(sourceArtifact), + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + sourceArtifact, + cloudAssemblyArtifact, + ...props?.npmSynthOptions, + }), + cloudAssemblyArtifact, + ...props, + }); + + this.sourceArtifact = sourceArtifact; + this.cloudAssemblyArtifact = cloudAssemblyArtifact; + } +} + + +export class TestGitHubAction extends codepipeline_actions.GitHubSourceAction { + constructor(sourceArtifact: codepipeline.Artifact) { + super({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.plainText('$3kr1t'), + owner: 'test', + repo: 'test', + trigger: codepipeline_actions.GitHubTrigger.POLL, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/modern-pipeline.ts b/packages/@aws-cdk/pipelines/test/testhelpers/modern-pipeline.ts new file mode 100644 index 0000000000000..b3e783ea8f569 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/modern-pipeline.ts @@ -0,0 +1,26 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { Construct } from 'constructs'; +import * as cdkp from '../../lib'; + +export type ModernTestGitHubNpmPipelineProps = Partial & Partial; + +export class ModernTestGitHubNpmPipeline extends cdkp.CodePipeline { + public readonly gitHubSource: cdkp.CodePipelineSource; + + constructor(scope: Construct, id: string, props?: ModernTestGitHubNpmPipelineProps) { + const source = cdkp.CodePipelineSource.gitHub('test/test', 'main'); + const synth = props?.synth ?? new cdkp.ShellStep('Synth', { + input: source, + installCommands: ['npm ci'], + commands: ['npx cdk synth'], + ...props, + }); + + super(scope, id, { + synth: synth, + ...props, + }); + + this.gitHubSource = source; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/test-app.ts b/packages/@aws-cdk/pipelines/test/testhelpers/test-app.ts new file mode 100644 index 0000000000000..1f554b75e2623 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/test-app.ts @@ -0,0 +1,214 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import '@aws-cdk/assert-internal/jest'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import { App, AppProps, Environment, CfnOutput, Stage, StageProps, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { assemblyBuilderOf } from '../../lib/private/construct-internals'; + +export const PIPELINE_ENV: Environment = { + account: '123pipeline', + region: 'us-pipeline', +}; + +export class TestApp extends App { + constructor(props?: Partial) { + super({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, + stackTraces: false, + autoSynth: false, + treeMetadata: false, + ...props, + }); + } + + public stackArtifact(stackName: string | Stack) { + if (typeof stackName !== 'string') { + stackName = stackName.stackName; + } + + this.synth(); + const supportStack = this.node.findAll().filter(Stack.isStack).find(s => s.stackName === stackName); + expect(supportStack).not.toBeUndefined(); + return supportStack; + } + + public cleanup() { + rimraf(assemblyBuilderOf(this).outdir); + } +} + + +export class OneStackApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + new BucketStack(this, 'Stack'); + } +} + +export class AppWithOutput extends Stage { + public readonly theOutput: CfnOutput; + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack = new BucketStack(this, 'Stack'); + this.theOutput = new CfnOutput(stack, 'MyOutput', { value: stack.bucket.bucketName }); + } +} + +export class TwoStackApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack2 = new BucketStack(this, 'Stack2'); + const stack1 = new BucketStack(this, 'Stack1'); + + stack2.addDependency(stack1); + } +} + +/** + * Three stacks where the last one depends on the earlier 2 + */ +export class ThreeStackApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack1 = new BucketStack(this, 'Stack1'); + const stack2 = new BucketStack(this, 'Stack2'); + const stack3 = new BucketStack(this, 'Stack3'); + + stack3.addDependency(stack1); + stack3.addDependency(stack2); + } +} + +/** + * A test stack + * + * It contains a single Bucket. Such robust. Much uptime. + */ +export class BucketStack extends Stack { + public readonly bucket: s3.IBucket; + + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + this.bucket = new s3.Bucket(this, 'Bucket'); + } +} + + +/** + * rm -rf reimplementation, don't want to depend on an NPM package for this + */ +export function rimraf(fsPath: string) { + try { + const isDir = fs.lstatSync(fsPath).isDirectory(); + + if (isDir) { + for (const file of fs.readdirSync(fsPath)) { + rimraf(path.join(fsPath, file)); + } + fs.rmdirSync(fsPath); + } else { + fs.unlinkSync(fsPath); + } + } catch (e) { + // We will survive ENOENT + if (e.code !== 'ENOENT') { throw e; } + } +} + +export function stackTemplate(stack: Stack) { + const stage = Stage.of(stack); + if (!stage) { throw new Error('stack not in a Stage'); } + return stage.synth().getStackArtifact(stack.artifactId); +} + +export class StageWithStackOutput extends Stage { + public readonly output: CfnOutput; + + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new BucketStack(this, 'Stack'); + + this.output = new CfnOutput(stack, 'BucketName', { + value: stack.bucket.bucketName, + }); + } +} + +export class FileAssetApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + new s3_assets.Asset(stack, 'Asset', { + path: path.join(__dirname, 'assets', 'test-file-asset.txt'), + }); + } +} + +export class TwoFileAssetsApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + new s3_assets.Asset(stack, 'Asset1', { + path: path.join(__dirname, 'assets', 'test-file-asset.txt'), + }); + new s3_assets.Asset(stack, 'Asset2', { + path: path.join(__dirname, 'assets', 'test-file-asset-two.txt'), + }); + } +} + +export class DockerAssetApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + new ecr_assets.DockerImageAsset(stack, 'Asset', { + directory: path.join(__dirname, 'assets', 'test-docker-asset'), + }); + } +} + +export interface MegaAssetsAppProps extends StageProps { + readonly numAssets: number; +} + +// Creates a mix of file and image assets, up to a specified count +export class MegaAssetsApp extends Stage { + constructor(scope: Construct, id: string, props: MegaAssetsAppProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + + let assetCount = 0; + for (; assetCount < props.numAssets / 2; assetCount++) { + new s3_assets.Asset(stack, `Asset${assetCount}`, { + path: path.join(__dirname, 'assets', 'test-file-asset.txt'), + assetHash: `FileAsset${assetCount}`, + }); + } + for (; assetCount < props.numAssets; assetCount++) { + new ecr_assets.DockerImageAsset(stack, `Asset${assetCount}`, { + directory: path.join(__dirname, 'assets', 'test-docker-asset'), + extraHash: `FileAsset${assetCount}`, + }); + } + } +} + +export class PlainStackApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + new BucketStack(this, 'Stack'); + } +} + + diff --git a/packages/@aws-cdk/pipelines/test/testmatchers.ts b/packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts similarity index 61% rename from packages/@aws-cdk/pipelines/test/testmatchers.ts rename to packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts index 90b31fb133fd4..8faa855b71abf 100644 --- a/packages/@aws-cdk/pipelines/test/testmatchers.ts +++ b/packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { annotateMatcher, InspectionFailure, matcherFrom, PropertyMatcher } from '@aws-cdk/assert-internal'; /** @@ -22,4 +23,20 @@ export function sortedByRunOrder(matcher: any): PropertyMatcher { return matcherFrom(matcher)(value, failure); }); +} + +export function stringNoLongerThan(length: number): PropertyMatcher { + return annotateMatcher({ $stringIsNoLongerThan: length }, (value: any, failure: InspectionFailure) => { + if (typeof value !== 'string') { + failure.failureReason = `Expected a string, but got '${typeof value}'`; + return false; + } + + if (value.length > length) { + failure.failureReason = `String is ${value.length} characters long. Expected at most ${length} characters`; + return false; + } + + return true; + }); } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testutil.ts b/packages/@aws-cdk/pipelines/test/testutil.ts deleted file mode 100644 index e654299d85182..0000000000000 --- a/packages/@aws-cdk/pipelines/test/testutil.ts +++ /dev/null @@ -1,136 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { annotateMatcher, InspectionFailure, PropertyMatcher } from '@aws-cdk/assert-internal'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; -import * as s3 from '@aws-cdk/aws-s3'; -import { App, AppProps, Environment, SecretValue, Stack, StackProps, Stage } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { assemblyBuilderOf } from '../lib/private/construct-internals'; - -export const PIPELINE_ENV: Environment = { - account: '123pipeline', - region: 'us-pipeline', -}; - -export class TestApp extends App { - constructor(props?: Partial) { - super({ - context: { - '@aws-cdk/core:newStyleStackSynthesis': '1', - }, - stackTraces: false, - autoSynth: false, - treeMetadata: false, - ...props, - }); - } - - public cleanup() { - rimraf(assemblyBuilderOf(this).outdir); - } -} - -export interface TestGitHubNpmPipelineExtraProps { - readonly sourceArtifact?: codepipeline.Artifact; - readonly npmSynthOptions?: Partial; -} - -export class TestGitHubNpmPipeline extends cdkp.CdkPipeline { - public readonly sourceArtifact: codepipeline.Artifact; - public readonly cloudAssemblyArtifact: codepipeline.Artifact; - - constructor(scope: Construct, id: string, props?: Partial & TestGitHubNpmPipelineExtraProps ) { - const sourceArtifact = props?.sourceArtifact ?? new codepipeline.Artifact(); - const cloudAssemblyArtifact = props?.cloudAssemblyArtifact ?? new codepipeline.Artifact(); - - super(scope, id, { - sourceAction: new TestGitHubAction(sourceArtifact), - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - ...props?.npmSynthOptions, - }), - cloudAssemblyArtifact, - ...props, - }); - - this.sourceArtifact = sourceArtifact; - this.cloudAssemblyArtifact = cloudAssemblyArtifact; - } -} - - -export class TestGitHubAction extends codepipeline_actions.GitHubSourceAction { - constructor(sourceArtifact: codepipeline.Artifact) { - super({ - actionName: 'GitHub', - output: sourceArtifact, - oauthToken: SecretValue.plainText('$3kr1t'), - owner: 'test', - repo: 'test', - trigger: codepipeline_actions.GitHubTrigger.POLL, - }); - } -} - -/** - * A test stack - * - * It contains a single Bucket. Such robust. Much uptime. - */ -export class BucketStack extends Stack { - public readonly bucket: s3.IBucket; - - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); - this.bucket = new s3.Bucket(this, 'Bucket'); - } -} - -/** - * rm -rf reimplementation, don't want to depend on an NPM package for this - */ -export function rimraf(fsPath: string) { - try { - const isDir = fs.lstatSync(fsPath).isDirectory(); - - if (isDir) { - for (const file of fs.readdirSync(fsPath)) { - rimraf(path.join(fsPath, file)); - } - fs.rmdirSync(fsPath); - } else { - fs.unlinkSync(fsPath); - } - } catch (e) { - // We will survive ENOENT - if (e.code !== 'ENOENT') { throw e; } - } -} - -/** - * Because 'expect(stack)' doesn't work correctly for stacks in nested assemblies - */ -export function stackTemplate(stack: Stack) { - const stage = Stage.of(stack); - if (!stage) { throw new Error('stack not in a Stage'); } - return stage.synth().getStackArtifact(stack.artifactId); -} - -export function stringNoLongerThan(length: number): PropertyMatcher { - return annotateMatcher({ $stringIsNoLongerThan: length }, (value: any, failure: InspectionFailure) => { - if (typeof value !== 'string') { - failure.failureReason = `Expected a string, but got '${typeof value}'`; - return false; - } - - if (value.length > length) { - failure.failureReason = `String is ${value.length} characters long. Expected at most ${length} characters`; - return false; - } - - return true; - }); -} diff --git a/packages/@aws-cdk/pipelines/test/validation.test.ts b/packages/@aws-cdk/pipelines/test/validation.test.ts deleted file mode 100644 index a75431e63266a..0000000000000 --- a/packages/@aws-cdk/pipelines/test/validation.test.ts +++ /dev/null @@ -1,497 +0,0 @@ -import { anything, arrayWith, deepObjectLike, encodedJson, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as codebuild from '@aws-cdk/aws-codebuild'; -import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as iam from '@aws-cdk/aws-iam'; -import * as s3 from '@aws-cdk/aws-s3'; -import { CfnOutput, Stack, Stage, StageProps } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import * as cdkp from '../lib'; -import { } from './testmatchers'; -import { behavior } from './helpers/compliance'; -import { BucketStack, PIPELINE_ENV, stringNoLongerThan, TestApp, TestGitHubNpmPipeline } from './testutil'; - -let app: TestApp; -let pipelineStack: Stack; -let pipeline: cdkp.CdkPipeline; -let sourceArtifact: codepipeline.Artifact; -let cloudAssemblyArtifact: codepipeline.Artifact; -let integTestArtifact: codepipeline.Artifact; - -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - sourceArtifact = new codepipeline.Artifact(); - cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); - integTestArtifact = new codepipeline.Artifact('IntegTests'); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { - sourceArtifact, - cloudAssemblyArtifact, - synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ - sourceArtifact, - cloudAssemblyArtifact, - additionalArtifacts: [{ directory: 'test', artifact: integTestArtifact }], - }), - }); -}); - -afterEach(() => { - app.cleanup(); -}); - -behavior('stackOutput generates names limited to 100 characters', (suite) => { - suite.legacy(() => { - const stage = new AppWithStackOutput(app, 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild'); - const pipeStage = pipeline.addApplicationStage(stage); - pipeStage.addActions(new cdkp.ShellScriptAction({ - actionName: 'TestOutput', - useOutputs: { - BUCKET_NAME: pipeline.stackOutput(stage.output), - }, - commands: ['echo $BUCKET_NAME'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', - Actions: arrayWith( - deepObjectLike({ - Name: 'Stack.Deploy', - OutputArtifacts: [{ Name: stringNoLongerThan(100) }], - Configuration: { - OutputFileName: 'outputs.json', - }, - }), - deepObjectLike({ - ActionTypeId: { - Provider: 'CodeBuild', - }, - Configuration: { - ProjectName: anything(), - }, - InputArtifacts: [{ Name: stringNoLongerThan(100) }], - Name: 'TestOutput', - }), - ), - }), - }); - }); -}); - -behavior('can use stack outputs as validation inputs', (suite) => { - suite.legacy(() => { - // GIVEN - const stage = new AppWithStackOutput(app, 'MyApp'); - - // WHEN - const pipeStage = pipeline.addApplicationStage(stage); - pipeStage.addActions(new cdkp.ShellScriptAction({ - actionName: 'TestOutput', - useOutputs: { - BUCKET_NAME: pipeline.stackOutput(stage.output), - }, - commands: ['echo $BUCKET_NAME'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'MyApp', - Actions: arrayWith( - deepObjectLike({ - Name: 'Stack.Deploy', - OutputArtifacts: [{ Name: anything() }], - Configuration: { - OutputFileName: 'outputs.json', - }, - }), - deepObjectLike({ - ActionTypeId: { - Provider: 'CodeBuild', - }, - Configuration: { - ProjectName: anything(), - }, - InputArtifacts: [{ Name: anything() }], - Name: 'TestOutput', - }), - ), - }), - }); - - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: [ - 'set -eu', - 'export BUCKET_NAME="$(node -pe \'require(process.env.CODEBUILD_SRC_DIR + "/outputs.json")["BucketName"]\')"', - 'echo $BUCKET_NAME', - ], - }, - }, - })), - Type: 'CODEPIPELINE', - }, - }); - }); -}); - -behavior('can use additional files from source', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'UseSources', - additionalArtifacts: [sourceArtifact], - commands: ['true'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'UseSources', - InputArtifacts: [{ Name: 'Artifact_Source_GitHub' }], - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: [ - 'set -eu', - 'true', - ], - }, - }, - })), - }, - }); - }); -}); - -behavior('can use additional files from build', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'UseBuildArtifact', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'UseBuildArtifact', - InputArtifacts: [{ Name: 'IntegTests' }], - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: [ - 'set -eu', - 'true', - ], - }, - }, - })), - }, - }); - }); -}); - -behavior('add policy statements to ShellScriptAction', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'Boop', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - rolePolicyStatements: [ - new iam.PolicyStatement({ - actions: ['s3:Banana'], - resources: ['*'], - }), - ], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith(deepObjectLike({ - Action: 's3:Banana', - Resource: '*', - })), - }, - }); - }); -}); - -behavior('ShellScriptAction is IGrantable', (suite) => { - suite.legacy(() => { - // GIVEN - const action = new cdkp.ShellScriptAction({ - actionName: 'Boop', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - }); - pipeline.addStage('Test').addActions(action); - const bucket = new s3.Bucket(pipelineStack, 'Bucket'); - - // WHEN - bucket.grantRead(action); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith(deepObjectLike({ - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - })), - }, - }); - }); -}); - -behavior('run ShellScriptAction in a VPC', (suite) => { - suite.legacy(() => { - // WHEN - const vpc = new ec2.Vpc(pipelineStack, 'VPC'); - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - vpc, - actionName: 'VpcAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'VpcAction', - InputArtifacts: [{ Name: 'IntegTests' }], - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:5.0', - }, - VpcConfig: { - SecurityGroupIds: [ - { - 'Fn::GetAtt': [ - 'CdkPipelineTestVpcActionProjectSecurityGroupBA94D315', - 'GroupId', - ], - }, - ], - Subnets: [ - { - Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', - }, - { - Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', - }, - { - Ref: 'VPCPrivateSubnet3Subnet3EDCD457', - }, - ], - VpcId: { - Ref: 'VPCB9E5F0B4', - }, - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: [ - 'set -eu', - 'true', - ], - }, - }, - })), - }, - }); - }); -}); - -behavior('run ShellScriptAction with Security Group', (suite) => { - suite.legacy(() => { - // WHEN - const vpc = new ec2.Vpc(pipelineStack, 'VPC'); - const sg = new ec2.SecurityGroup(pipelineStack, 'SG', { vpc }); - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - vpc, - securityGroups: [sg], - actionName: 'sgAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'sgAction', - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - VpcConfig: { - SecurityGroupIds: [ - { - 'Fn::GetAtt': [ - 'SGADB53937', - 'GroupId', - ], - }, - ], - VpcId: { - Ref: 'VPCB9E5F0B4', - }, - }, - }); - }); -}); - -behavior('run ShellScriptAction with specified codebuild image', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'imageAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_2_0 }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - deepObjectLike({ - Name: 'imageAction', - }), - ], - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:2.0', - }, - }); - }); -}); - -behavior('run ShellScriptAction with specified BuildEnvironment', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'imageAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - environment: { - buildImage: codebuild.LinuxBuildImage.STANDARD_2_0, - computeType: codebuild.ComputeType.LARGE, - environmentVariables: { FOO: { value: 'BAR', type: codebuild.BuildEnvironmentVariableType.PLAINTEXT } }, - privileged: true, - }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:2.0', - PrivilegedMode: true, - ComputeType: 'BUILD_GENERAL1_LARGE', - EnvironmentVariables: [ - { - Type: 'PLAINTEXT', - Value: 'BAR', - Name: 'FOO', - }, - ], - }, - }); - }); -}); - -behavior('run ShellScriptAction with specified environment variables', (suite) => { - suite.legacy(() => { - // WHEN - pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ - actionName: 'imageAction', - additionalArtifacts: [integTestArtifact], - commands: ['true'], - environmentVariables: { - VERSION: { value: codepipeline.GlobalVariables.executionId }, - }, - })); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Test', - Actions: [ - objectLike({ - Name: 'imageAction', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ - { - name: 'VERSION', - type: 'PLAINTEXT', - value: '#{codepipeline.PipelineExecutionId}', - }, - ]), - }), - }), - ], - }), - }); - }); -}); - -class AppWithStackOutput extends Stage { - public readonly output: CfnOutput; - - constructor(scope: Construct, id: string, props?: StageProps) { - super(scope, id, props); - const stack = new BucketStack(this, 'Stack'); - - this.output = new CfnOutput(stack, 'BucketName', { - value: stack.bucket.bucketName, - }); - } -} \ No newline at end of file diff --git a/scripts/best b/scripts/best new file mode 100755 index 0000000000000..e0d540a96a28f --- /dev/null +++ b/scripts/best @@ -0,0 +1,4 @@ +#!/bin/bash +# Run jest with the fail-fast plugin +scriptdir=$(cd $(dirname $0) && pwd) +exec $scriptdir/../tools/cdk-build-tools/node_modules/.bin/jest --setupFilesAfterEnv $scriptdir/jest-fail-fast-setup.js -- "$@" \ No newline at end of file diff --git a/scripts/jest-fail-fast-setup.js b/scripts/jest-fail-fast-setup.js new file mode 100644 index 0000000000000..85a77dd84ff74 --- /dev/null +++ b/scripts/jest-fail-fast-setup.js @@ -0,0 +1,4 @@ +// Run `jest --setupFilesAfterEnv path/to/jest-fail-fast-setup.js --` to stop after the first failing test +// Use the `best` script in this directory for convenience. +const failFast = require('jasmine-fail-fast'); +jasmine.getEnv().addReporter(failFast.init()); \ No newline at end of file diff --git a/scripts/print-construct-tree.py b/scripts/print-construct-tree.py new file mode 100755 index 0000000000000..447c3e66f60e9 --- /dev/null +++ b/scripts/print-construct-tree.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +"""Print the construct tree from a cdk.out directory.""" +import sys +import argparse +from os import path +import json + + +def main(): + dirname = sys.argv[1] + parser = argparse.ArgumentParser(description='Print the construct tree from a cdk.out directory') + parser.add_argument('dir', metavar='DIR', type=str, nargs=1, default='cdk.out', + help='cdk.out directory') + + args = parser.parse_args() + print_tree_file(path.join(args.dir[0], 'tree.json')) + + +def print_tree_file(tree_file_name): + with open(tree_file_name, 'r') as f: + contents = json.load(f) + print_tree(contents) + + +def print_tree(tree_file): + print_node(tree_file['tree']) + + +def print_node(node, prefix_here='', prefix_children=''): + info = [] + cfn_type = node.get('attributes', {}).get('aws:cdk:cloudformation:type') + if cfn_type: + info.append(cfn_type) + + print(prefix_here + node['id'] + ((' (' + ', '.join(info) + ')') if info else '')) + children = list(node.get('children', {}).values()) + for i, child in enumerate(children): + if i < len(children) - 1: + print_node(child, prefix_children + ' ├─ ', prefix_children + ' │ ') + else: + print_node(child, prefix_children + ' └─ ', prefix_children + ' ') + + +if __name__ == '__main__': + main() From 53a7298f241632a13b12601a3d348c05d9e0e5a2 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Thu, 15 Jul 2021 10:13:19 +0000 Subject: [PATCH 055/105] chore(release): 1.114.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdc62b8761765..8c2c236e6eb4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.114.0](https://github.com/aws/aws-cdk/compare/v1.113.0...v1.114.0) (2021-07-15) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **appmesh:** `prefixPath` property in `HttpGatewayRouteMatch` has been renamed to `path`, and its type changed from `string` to `HttpGatewayRoutePathMatch` +* **servicecatalog:** `AcceptLanguage` enum has been renamed to `MessageLanguage`, and fields that accepted this enum have been updated to reflect this change. +* **servicecatalog:** property `acceptLanguage` in `PortfolioShareOptions` has been renamed to `messageLanguage`. +* **servicecatalog:** property `acceptLanguage` in `PortfolioProps` has been renamed to `messageLanguage`. +* **servicecatalog:** property `acceptLanguage` in `CloudFormationProductProps` has been renamed `messageLanguage`. +* **appmesh:** `prefixPath` property in `HttpRouteMatch` has been renamed to `path`, and its type changed from `string` to `HttpRoutePathMatch` + +### Features + +* **appmesh:** add Route matching on path, query parameters, metadata, and method name ([#15470](https://github.com/aws/aws-cdk/issues/15470)) ([eeeec5d](https://github.com/aws/aws-cdk/commit/eeeec5d14aa03dbaeeb08fc664c26e82a447f7da)) +* **appmesh:** add support for Gateway Route request matching and path rewriting ([#15527](https://github.com/aws/aws-cdk/issues/15527)) ([1589ff8](https://github.com/aws/aws-cdk/commit/1589ff859e3816e1326b25e4fc855be86f76ffc8)), closes [#15305](https://github.com/aws/aws-cdk/issues/15305) +* **appmesh:** the App Mesh Construct Library is now Generally Available (stable) ([#15560](https://github.com/aws/aws-cdk/issues/15560)) ([718d143](https://github.com/aws/aws-cdk/commit/718d143a376893fb168121b0ff9b57f8a057281e)), closes [#9489](https://github.com/aws/aws-cdk/issues/9489) +* **aws-ecs:** New CDK constructs for ECS Anywhere task and service definitions ([#14931](https://github.com/aws/aws-cdk/issues/14931)) ([3592b26](https://github.com/aws/aws-cdk/commit/3592b26c5806cc31cd6ad0ebba32cbf4d09b9abf)) +* **bootstrap:** widen lookup role permissions for future extension ([#15423](https://github.com/aws/aws-cdk/issues/15423)) ([cafdd3c](https://github.com/aws/aws-cdk/commit/cafdd3c0a619be69c9b6af08664af8e641d4c69b)) +* **cfnspec:** cloudformation spec v39.5.0 ([#15536](https://github.com/aws/aws-cdk/issues/15536)) ([c98e40e](https://github.com/aws/aws-cdk/commit/c98e40e963964ae01b6ad15898a6809687d6a5e3)) +* **pipelines:** revised version of the API ([#12326](https://github.com/aws/aws-cdk/issues/12326)) ([165ee3a](https://github.com/aws/aws-cdk/commit/165ee3aa89bda7c18fcb4820c0bf2f6905adc4ed)), closes [#10872](https://github.com/aws/aws-cdk/issues/10872) +* **servicecatalog:** Add portfolio-product association and tag update constraint ([#15452](https://github.com/aws/aws-cdk/issues/15452)) ([b06f7bf](https://github.com/aws/aws-cdk/commit/b06f7bf8ee59379a3478e4200b941635174c777e)) + + +### Bug Fixes + +* **ecr-assets:** There is already a Construct with name 'Staging' when using tarball image ([#15540](https://github.com/aws/aws-cdk/issues/15540)) ([594d7c6](https://github.com/aws/aws-cdk/commit/594d7c664abed631163ec6b5cfede0a61acb0602)) + ## [1.113.0](https://github.com/aws/aws-cdk/compare/v1.112.0...v1.113.0) (2021-07-12) diff --git a/version.v1.json b/version.v1.json index edaba2d69702e..b2aee66739e48 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.113.0" + "version": "1.114.0" } From 09591c6268d0e03937741e4f7cad9b97e21b131b Mon Sep 17 00:00:00 2001 From: Alban Esc Date: Thu, 15 Jul 2021 08:17:14 -0700 Subject: [PATCH 056/105] feat(aws-efs): grant support on FileSystem (#14999) `FileSystem` now have the `fileSystemArn` attribute available. It is also possible to import an existing `FileSystem` by `arn` or `id` using the `fromFileSystemAttributes` method. You can also grant permission to an existing grantee using the grant method. See the example below giving an IAM Role permission to write to an imported file system: ```ts const arn = stack.formatArn({ service: 'elasticfilesystem', resource: 'file-system', resourceName: 'fs-12912923', }); const importedFileSystem = efs.FileSystem.fromFileSystemAttributes(this, 'existingFS', { fileSystemArn: arn, // You can also use fileSystemArn instead of fileSystemId. securityGroup: ec2.SecurityGroup.fromSecurityGroupId(this, 'SG', 'sg-123456789', { allowAllOutbound: false, }), }); const role = new iam.Role(this, 'Access Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com') }); importedFileSystem.grant(role, 'elasticfilesystem:ClientWrite'); ``` Closes #14998. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-efs/README.md | 30 +++++- .../@aws-cdk/aws-efs/lib/efs-file-system.ts | 95 ++++++++++++++++++- packages/@aws-cdk/aws-efs/package.json | 6 +- .../aws-efs/test/efs-file-system.test.ts | 94 +++++++++++++++++- 4 files changed, 216 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index bccae05b20547..0323d8e516199 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -37,12 +37,40 @@ const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, // files are not transitioned to infrequent access (IA) storage by default performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, // default }); - ``` ⚠️ An Amazon EFS file system's performance mode can't be changed after the file system has been created. Updating this property will replace the file system. +Any file system that has been created outside the stack can be imported into your CDK app. + +Use the `fromFileSystemAttributes()` API to import an existing file system. +Here is an example of giving a role write permissions on a file system. + +```ts +import * as iam from '@aws-cdk/aws-iam'; + +const importedFileSystem = efs.FileSystem.fromFileSystemAttributes(this, 'existingFS', { + fileSystemId: 'fs-12345678', // You can also use fileSystemArn instead of fileSystemId. + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(this, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), +}); +``` + +### Permissions + +If you need to grant file system permissions to another resource, you can use the `.grant()` API. +As an example, the following code gives `elasticfilesystem:ClientWrite` permissions to an IAM role. + +```ts fixture=with-filesystem-instance +const role = new iam.Role(this, 'Role', { + assumedBy: new iam.AnyPrincipal(), +}); + +fileSystem.grant(role, 'elasticfilesystem:ClientWrite'); +``` + ### Access Point An access point is an application-specific view into an EFS file system that applies an operating diff --git a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts index 99e390a89257d..aac4636ed9fa9 100644 --- a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts +++ b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts @@ -1,6 +1,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { ConcreteDependable, IDependable, IResource, RemovalPolicy, Resource, Size, Tags } from '@aws-cdk/core'; +import { ConcreteDependable, IDependable, IResource, RemovalPolicy, Resource, Size, Stack, Tags } from '@aws-cdk/core'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports import { FeatureFlags } from '@aws-cdk/core'; @@ -92,11 +93,23 @@ export interface IFileSystem extends ec2.IConnectable, IResource { */ readonly fileSystemId: string; + /** + * The ARN of the file system. + * + * @attribute + */ + readonly fileSystemArn: string; + /** * Dependable that can be depended upon to ensure the mount targets of the filesystem are ready */ readonly mountTargetsAvailable: IDependable; + /** + * Grant the actions defined in actions to the given grantee + * on this File System resource. + */ + grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; } /** @@ -203,8 +216,53 @@ export interface FileSystemAttributes { /** * The File System's ID. + * + * @default - determined based on fileSystemArn */ - readonly fileSystemId: string; + readonly fileSystemId?: string; + + /** + * The File System's Arn. + * + * @default - determined based on fileSystemId + */ + readonly fileSystemArn?: string; +} + +abstract class FileSystemBase extends Resource implements IFileSystem { + /** + * The security groups/rules used to allow network connections to the file system. + */ + public abstract readonly connections: ec2.Connections; + + /** + * @attribute + */ + public abstract readonly fileSystemId: string; + /** + * @attribute + */ + public abstract readonly fileSystemArn: string; + + /** + * Dependable that can be depended upon to ensure the mount targets of the filesystem are ready + */ + public abstract readonly mountTargetsAvailable: IDependable; + + /** + * Grant the actions defined in actions to the given grantee + * on this File System resource. + * + * @param grantee Principal to grant right to + * @param actions The actions to grant + */ + public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee: grantee, + actions: actions, + resourceArns: [this.fileSystemArn], + }); + } } /** @@ -217,7 +275,7 @@ export interface FileSystemAttributes { * * @resource AWS::EFS::FileSystem */ -export class FileSystem extends Resource implements IFileSystem { +export class FileSystem extends FileSystemBase { /** * The default port File System listens on. */ @@ -239,6 +297,10 @@ export class FileSystem extends Resource implements IFileSystem { * @attribute */ public readonly fileSystemId: string; + /** + * @attribute + */ + public readonly fileSystemArn: string; public readonly mountTargetsAvailable: IDependable; @@ -271,6 +333,8 @@ export class FileSystem extends Resource implements IFileSystem { filesystem.applyRemovalPolicy(props.removalPolicy); this.fileSystemId = filesystem.ref; + this.fileSystemArn = filesystem.attrArn; + Tags.of(this).add('Name', props.fileSystemName || this.node.path); const securityGroup = (props.securityGroup || new ec2.SecurityGroup(this, 'EfsSecurityGroup', { @@ -311,7 +375,7 @@ export class FileSystem extends Resource implements IFileSystem { } } -class ImportedFileSystem extends Resource implements IFileSystem { +class ImportedFileSystem extends FileSystemBase { /** * The security groups/rules used to allow network connections to the file system. */ @@ -322,6 +386,11 @@ class ImportedFileSystem extends Resource implements IFileSystem { */ public readonly fileSystemId: string; + /** + * @attribute + */ + public readonly fileSystemArn: string; + /** * Dependable that can be depended upon to ensure the mount targets of the filesystem are ready */ @@ -330,7 +399,23 @@ class ImportedFileSystem extends Resource implements IFileSystem { constructor(scope: Construct, id: string, attrs: FileSystemAttributes) { super(scope, id); - this.fileSystemId = attrs.fileSystemId; + if (!!attrs.fileSystemId === !!attrs.fileSystemArn) { + throw new Error('One of fileSystemId or fileSystemArn, but not both, must be provided.'); + } + + this.fileSystemArn = attrs.fileSystemArn ?? Stack.of(scope).formatArn({ + service: 'elasticfilesystem', + resource: 'file-system', + resourceName: attrs.fileSystemId, + }); + + const parsedArn = Stack.of(scope).parseArn(this.fileSystemArn); + + if (!parsedArn.resourceName) { + throw new Error(`Invalid FileSystem Arn ${this.fileSystemArn}`); + } + + this.fileSystemId = attrs.fileSystemId ?? parsedArn.resourceName; this.connections = new ec2.Connections({ securityGroups: [attrs.securityGroup], diff --git a/packages/@aws-cdk/aws-efs/package.json b/packages/@aws-cdk/aws-efs/package.json index 7a68d2200311a..d6dbf8bce203b 100644 --- a/packages/@aws-cdk/aws-efs/package.json +++ b/packages/@aws-cdk/aws-efs/package.json @@ -81,17 +81,19 @@ "@aws-cdk/assertions": "0.0.0" }, "dependencies": { - "@aws-cdk/core": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", - "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/core": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts b/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts index 0c822e368b932..8db4c262c1aac 100644 --- a/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts +++ b/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts @@ -1,5 +1,6 @@ import { TemplateAssertions, Match } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { App, RemovalPolicy, Size, Stack, Tags } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -192,7 +193,7 @@ test('file system is created correctly with provisioned throughput mode', () => }); }); -test('existing file system is imported correctly', () => { +test('existing file system is imported correctly using id', () => { // WHEN const fs = FileSystem.fromFileSystemAttributes(stack, 'existingFS', { fileSystemId: 'fs123', @@ -209,6 +210,97 @@ test('existing file system is imported correctly', () => { }); }); +test('existing file system is imported correctly using arn', () => { + // WHEN + const arn = stack.formatArn({ + service: 'elasticfilesystem', + resource: 'file-system', + resourceName: 'fs-12912923', + }); + const fs = FileSystem.fromFileSystemAttributes(stack, 'existingFS', { + fileSystemArn: arn, + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + + fs.connections.allowToAnyIpv4(ec2.Port.tcp(443)); + + // THEN + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { + GroupId: 'sg-123456789', + }); + + expect(fs.fileSystemArn).toEqual(arn); + expect(fs.fileSystemId).toEqual('fs-12912923'); +}); + +test('must throw an error when trying to import a fileSystem without specifying id or arn', () => { + // WHEN + expect(() => { + FileSystem.fromFileSystemAttributes(stack, 'existingFS', { + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + }).toThrow(/One of fileSystemId or fileSystemArn, but not both, must be provided./); +}); + +test('must throw an error when trying to import a fileSystem specifying both id and arn', () => { + // WHEN + const arn = stack.formatArn({ + service: 'elasticfilesystem', + resource: 'file-system', + resourceName: 'fs-12912923', + }); + + expect(() => { + FileSystem.fromFileSystemAttributes(stack, 'existingFS', { + fileSystemArn: arn, + fileSystemId: 'fs-12343435', + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + }).toThrow(/One of fileSystemId or fileSystemArn, but not both, must be provided./); +}); + +test('support granting permissions', () => { + const fileSystem = new FileSystem(stack, 'EfsFileSystem', { + vpc, + }); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + + fileSystem.grant(role, 'elasticfilesystem:ClientWrite'); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'elasticfilesystem:ClientWrite', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'EfsFileSystem37910666', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'RoleDefaultPolicy5FFB7DAB', + Roles: [ + { + Ref: 'Role1ABCC5F0', + }, + ], + }); +}); + test('support tags', () => { // WHEN const fileSystem = new FileSystem(stack, 'EfsFileSystem', { From dd38eff318c31bf2c5308f6b3daae0fd433b4370 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 15 Jul 2021 09:23:38 -0700 Subject: [PATCH 057/105] fix(cfnspec): make EndpointConfiguration of AWS::Serverless::Api a union type (#15526) A recent update to the SAM spec (https://github.com/aws/aws-cdk/pull/15311) changed the EndpointConfiguration property of AWS::Serverless::Api to have a complex type. However, that is a breaking change compared to the previous, string, type. I consulted with the SAM team, and it turns out the property accepts both a string and the complex type. Given that, patch our SAM spec to make EndpointConfiguration a union type. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-sam/test/api.test.ts | 44 +++++++++++++++++++ ...1_SAM_Api_EndpointConfiguration_patch.json | 29 ++++++++++++ .../test/serverless-transform.test.ts | 24 ++++++++++ .../sam/api-endpoint-config-object.yaml | 11 +++++ .../sam/api-endpoint-config-string-empty.yaml | 10 +++++ .../sam/api-endpoint-config-string.yaml | 10 +++++ tools/cfn2ts/lib/codegen.ts | 12 ++++- 7 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-sam/test/api.test.ts create mode 100644 packages/@aws-cdk/cfnspec/spec-source/901_SAM_Api_EndpointConfiguration_patch.json create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-object.yaml create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-string-empty.yaml create mode 100644 packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-string.yaml diff --git a/packages/@aws-cdk/aws-sam/test/api.test.ts b/packages/@aws-cdk/aws-sam/test/api.test.ts new file mode 100644 index 0000000000000..bed8a90ae4e57 --- /dev/null +++ b/packages/@aws-cdk/aws-sam/test/api.test.ts @@ -0,0 +1,44 @@ +import '@aws-cdk/assert-internal/jest'; +import * as cdk from '@aws-cdk/core'; +import * as sam from '../lib'; + +describe('AWS::Serverless::Api', () => { + let stack: cdk.Stack; + beforeEach(() => { + stack = new cdk.Stack(); + }); + + test('can be created by passing a complex type to EndpointConfiguration', () => { + new sam.CfnApi(stack, 'Api', { + stageName: 'prod', + definitionBody: { + body: 'definitionBody', + }, + endpointConfiguration: { + type: 'GLOBAL', + }, + }); + + expect(stack).toHaveResourceLike('AWS::Serverless::Api', { + StageName: 'prod', + EndpointConfiguration: { + Type: 'GLOBAL', + }, + }); + }); + + test('can be created by passing a string to EndpointConfiguration', () => { + new sam.CfnApi(stack, 'Api', { + stageName: 'prod', + definitionBody: { + body: 'definitionBody', + }, + endpointConfiguration: 'GLOBAL', + }); + + expect(stack).toHaveResourceLike('AWS::Serverless::Api', { + StageName: 'prod', + EndpointConfiguration: 'GLOBAL', + }); + }); +}); diff --git a/packages/@aws-cdk/cfnspec/spec-source/901_SAM_Api_EndpointConfiguration_patch.json b/packages/@aws-cdk/cfnspec/spec-source/901_SAM_Api_EndpointConfiguration_patch.json new file mode 100644 index 0000000000000..906dbbb88466c --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/901_SAM_Api_EndpointConfiguration_patch.json @@ -0,0 +1,29 @@ +{ + "ResourceTypes": { + "AWS::Serverless::Api": { + "Properties": { + "EndpointConfiguration": { + "patch": { + "description": "Make the EndpointConfiguration property of AWS::Serverless::Api have a union type", + "operations": [ + { + "op": "add", + "path": "/PrimitiveTypes", + "value": ["String"] + }, + { + "op": "add", + "path": "/Types", + "value": ["EndpointConfiguration"] + }, + { + "op": "remove", + "path": "/Type" + } + ] + } + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts b/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts index 853869ec1dd24..ce0d044bb4ed3 100644 --- a/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/serverless-transform.test.ts @@ -54,6 +54,30 @@ describe('CDK Include for templates with SAM transform', () => { loadTestFileToJsObject('only-sam-function-policies-array-ddb-crud-if.yaml'), ); }); + + test('can ingest a template with a a union-type property provided as an object, and output it unchanged', () => { + includeTestTemplate(stack, 'api-endpoint-config-object.yaml'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('api-endpoint-config-object.yaml'), + ); + }); + + test('can ingest a template with a a union-type property provided as a string, and output it unchanged', () => { + includeTestTemplate(stack, 'api-endpoint-config-string.yaml'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('api-endpoint-config-string.yaml'), + ); + }); + + test('can ingest a template with a a union-type property provided as an empty string, and output it unchanged', () => { + includeTestTemplate(stack, 'api-endpoint-config-string-empty.yaml'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('api-endpoint-config-string-empty.yaml'), + ); + }); }); function includeTestTemplate(scope: constructs.Construct, testTemplate: string): inc.CfnInclude { diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-object.yaml b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-object.yaml new file mode 100644 index 0000000000000..77be57c23823c --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-object.yaml @@ -0,0 +1,11 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Resources: + Api: + Type: AWS::Serverless::Api + Properties: + StageName: prod + DefinitionBody: + Body: DefinitionBody + EndpointConfiguration: + Type: GLOBAL diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-string-empty.yaml b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-string-empty.yaml new file mode 100644 index 0000000000000..1898e5105a864 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-string-empty.yaml @@ -0,0 +1,10 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Resources: + Api: + Type: AWS::Serverless::Api + Properties: + StageName: prod + DefinitionBody: + Body: DefinitionBody + EndpointConfiguration: '' # empty string diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-string.yaml b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-string.yaml new file mode 100644 index 0000000000000..a12949dabd226 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/sam/api-endpoint-config-string.yaml @@ -0,0 +1,10 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Resources: + Api: + Type: AWS::Serverless::Api + Properties: + StageName: prod + DefinitionBody: + Body: DefinitionBody + EndpointConfiguration: GLOBAL diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index f952bb34a832e..6a02f08b1e253 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -585,9 +585,17 @@ export default class CodeGenerator { this.code.closeBlock(); } - this.code.line('properties = properties || {};'); - this.code.line(`const ret = new ${CFN_PARSE}.FromCloudFormationPropertyObject<${typeName.fqn}>();`); + this.code.line('properties = properties == null ? {} : properties;'); + // if the passed value is not an object, immediately return it, + // and let a validator report an error - + // otherwise, we'll just return an empty object for this case, + // which a validator might not catch + // (if the interface we're emitting this function for has no required properties, for example) + this.code.openBlock("if (typeof properties !== 'object')"); + this.code.line(`return new ${CFN_PARSE}.FromCloudFormationResult(properties);`); + this.code.closeBlock(); + this.code.line(`const ret = new ${CFN_PARSE}.FromCloudFormationPropertyObject<${typeName.fqn}>();`); const self = this; // class used for the visitor class FromCloudFormationFactoryVisitor implements genspec.PropertyVisitor { From ebfc0e092addbcf21562ce63328457285f7b2d77 Mon Sep 17 00:00:00 2001 From: Otavio Macedo Date: Thu, 15 Jul 2021 18:02:21 +0100 Subject: [PATCH 058/105] chore(cli): tests that apply only to v1 are failing when building for v2 (#15565) In v1 we are allowing selection by the ID of the stack, with a warning that it will be stop working in v2 and asking the user to use the same names shown in the `cdk ls` result. Since the tests are written with the ID as input, they work when building for v1, but fail when building for v2. This change makes it work in both cases. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/test/api/cloud-assembly.test.ts | 2 +- packages/aws-cdk/test/cdk-toolkit.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/api/cloud-assembly.test.ts b/packages/aws-cdk/test/api/cloud-assembly.test.ts index 8ca66c9bad894..54d3a18f682ef 100644 --- a/packages/aws-cdk/test/api/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/api/cloud-assembly.test.ts @@ -135,7 +135,7 @@ test('select behavior with nested assemblies: repeat', async() => { const cxasm = await testNestedCloudAssembly(); // WHEN - const x = await cxasm.selectStacks({ patterns: ['withouterrors', 'withouterrors', 'nested'] }, { + const x = await cxasm.selectStacks({ patterns: ['deeply/hidden/withouterrors', 'nested'] }, { defaultBehavior: DefaultSelection.AllStacks, }); diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index c2bd6c48206ca..8f53405b749e4 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -221,7 +221,7 @@ describe('synth', () => { const toolkit = defaultToolkitSetup(); - await expect(toolkit.synth(['witherrors'], false, true)).rejects.toBeDefined(); + await expect(toolkit.synth(['Test-Stack-A/witherrors'], false, true)).rejects.toBeDefined(); }); test('stack has error, is not flagged for validation and was not explicitly selected', async () => { From a0f00e84e4e6f3d1fe5a9a0d8ae3c1a75c6e7baf Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 16 Jul 2021 10:53:29 +0200 Subject: [PATCH 059/105] chore(kms): cross-stack usage detection depends on NPM tree (#15580) KMS keys try to be smart about not generating impossible dependencies between multiple stacks, which CodePipeline takes advantage of for its support stacks. However, because the logic that tests for this case has an `instanceof Construct` in its code path, if there are ever multiple copies of the `constructs` library in the NPM tree the test will fail, and the resulting error will be very confusing. This situation can arise when people flip back and forth between CDK v1 and v2, because `package-lock.json` will contain half-baked dependency trees; people will be looking at their code but the issue will be in invisible state. Be more liberal in detecting that a construct is, in fact, a construct to get around this. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-kms/lib/key.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index 03097d9d49073..aae71efb460a7 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -201,7 +201,7 @@ abstract class KeyBase extends Resource implements IKey { */ private granteeStackDependsOnKeyStack(grantee: iam.IGrantable): string | undefined { const grantPrincipal = grantee.grantPrincipal; - if (!(grantPrincipal instanceof Construct)) { + if (!isConstruct(grantPrincipal)) { return undefined; } // this logic should only apply to newly created @@ -229,7 +229,7 @@ abstract class KeyBase extends Resource implements IKey { } private isGranteeFromAnotherRegion(grantee: iam.IGrantable): boolean { - if (!(grantee instanceof Construct)) { + if (!isConstruct(grantee)) { return false; } const bucketStack = Stack.of(this); @@ -238,7 +238,7 @@ abstract class KeyBase extends Resource implements IKey { } private isGranteeFromAnotherAccount(grantee: iam.IGrantable): boolean { - if (!(grantee instanceof Construct)) { + if (!isConstruct(grantee)) { return false; } const bucketStack = Stack.of(this); @@ -675,3 +675,20 @@ export class Key extends KeyBase { })); } } + +/** + * Whether the given object is a Construct + * + * Normally we'd do `x instanceof Construct`, but that is not robust against + * multiple copies of the `constructs` library on disk. This can happen + * when upgrading and downgrading between v2 and v1, and in the use of CDK + * Pipelines is going to an error that says "Can't use Pipeline/Pipeline/Role in + * a cross-environment fashion", which is very confusing. + */ +function isConstruct(x: any): x is Construct { + const sym = Symbol.for('constructs.Construct.node'); + return (typeof x === 'object' && x && + (x instanceof Construct // happy fast case + || !!(x as any).node // constructs v10 + || !!(x as any)[sym])); // constructs v3 +} \ No newline at end of file From 4b7116d8a252a6768ae50d736d5cab0f0cef22f4 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 16 Jul 2021 11:47:37 +0200 Subject: [PATCH 060/105] fix(pipelines): unresolved source names aren't handled properly (#15600) Fixes #15592 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-codepipeline/lib/private/stage.ts | 16 ++++- .../@aws-cdk/pipelines/lib/blueprint/step.ts | 5 ++ .../lib/codepipeline/codepipeline-source.ts | 36 ++++++---- .../codepipeline/codepipeline-sources.test.ts | 67 +++++++++++++++++++ 4 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts diff --git a/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts index b5f5aa86dc1c4..214e3c7086dd2 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/private/stage.ts @@ -1,5 +1,6 @@ import * as events from '@aws-cdk/aws-events'; import * as cdk from '@aws-cdk/core'; +import { Token } from '@aws-cdk/core'; import { Construct, Node } from 'constructs'; import { IAction, IPipeline, IStage } from '../action'; import { Artifact } from '../artifact'; @@ -145,7 +146,11 @@ export class Stage implements IStage { // If so, we simply reuse it. let actionScope = Node.of(this.scope).tryFindChild(action.actionProperties.actionName) as Construct | undefined; if (!actionScope) { - actionScope = new cdk.Construct(this.scope, action.actionProperties.actionName); + let id = action.actionProperties.actionName; + if (Token.isUnresolved(id)) { + id = findUniqueConstructId(this.scope, action.actionProperties.provider); + } + actionScope = new cdk.Construct(this.scope, id); } return this._pipeline._attachActionToPipeline(this, action, actionScope); } @@ -183,3 +188,12 @@ function sanitizeArtifactName(artifactName: string): string { // but not in Artifact names return artifactName.replace(/[@.]/g, ''); } + +function findUniqueConstructId(scope: Construct, prefix: string) { + let current = prefix; + let ctr = 1; + while (Node.of(scope).tryFindChild(current) !== undefined) { + current = `${prefix}${++ctr}`; + } + return current; +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts index e252765efd04e..e04b79bdcd848 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts @@ -1,3 +1,4 @@ +import { Token } from '@aws-cdk/core'; import { FileSet, IFileSetProducer } from './file-set'; /** @@ -27,6 +28,10 @@ export abstract class Step implements IFileSetProducer { constructor( /** Identifier for this step */ public readonly id: string) { + + if (Token.isUnresolved(id)) { + throw new Error(`Step id cannot be unresolved, got '${id}'`); + } } /** diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts index d97b4c5f925de..d9075b45f8334 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -5,7 +5,8 @@ import * as cp_actions from '@aws-cdk/aws-codepipeline-actions'; import { Action, CodeCommitTrigger, GitHubTrigger, S3Trigger } from '@aws-cdk/aws-codepipeline-actions'; import * as iam from '@aws-cdk/aws-iam'; import { IBucket } from '@aws-cdk/aws-s3'; -import { SecretValue } from '@aws-cdk/core'; +import { SecretValue, Token } from '@aws-cdk/core'; +import { Node } from 'constructs'; import { FileSet, Step } from '../blueprint'; import { CodePipelineActionFactoryResult, ProduceActionOptions, ICodePipelineActionFactory } from './codepipeline-action-factory'; @@ -172,8 +173,8 @@ class GitHubSource extends CodePipelineSource { super(repoString); const parts = repoString.split('/'); - if (parts.length !== 2) { - throw new Error(`GitHub repository name should look like '/', got '${repoString}'`); + if (Token.isUnresolved(repoString) || parts.length !== 2) { + throw new Error(`GitHub repository name should be a resolved string like '/', got '${repoString}'`); } this.owner = parts[0]; this.repo = parts[1]; @@ -208,19 +209,27 @@ export interface S3SourceOptions { * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/log-s3-data-events.html */ readonly trigger?: S3Trigger; + + /** + * The action name used for this source in the CodePipeline + * + * @default - The bucket name + */ + readonly actionName?: string; } class S3Source extends CodePipelineSource { constructor(readonly bucket: IBucket, private readonly objectKey: string, readonly props: S3SourceOptions) { - super(bucket.bucketName); + super(Node.of(bucket).addr); this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, actionName: string, runOrder: number) { + protected getAction(output: Artifact, _actionName: string, runOrder: number) { return new cp_actions.S3SourceAction({ output, - actionName, + // Bucket names are guaranteed to conform to ActionName restrictions + actionName: this.props.actionName ?? this.bucket.bucketName, runOrder, bucketKey: this.objectKey, trigger: this.props.trigger, @@ -276,8 +285,8 @@ class CodeStarConnectionSource extends CodePipelineSource { super(repoString); const parts = repoString.split('/'); - if (parts.length !== 2) { - throw new Error(`CodeStar repository name should look like '/', got '${repoString}'`); + if (Token.isUnresolved(repoString) || parts.length !== 2) { + throw new Error(`CodeStar repository name should be a resolved string like '/', got '${repoString}'`); } this.owner = parts[0]; this.repo = parts[1]; @@ -333,16 +342,19 @@ export interface CodeCommitSourceOptions { } class CodeCommitSource extends CodePipelineSource { - constructor(readonly repository: codecommit.IRepository, readonly branch: string, readonly props: CodeCommitSourceOptions) { - super(repository.repositoryName); + constructor(private readonly repository: codecommit.IRepository, private readonly branch: string, private readonly props: CodeCommitSourceOptions) { + super(Token.isUnresolved(repository.repositoryName) + ? Node.of(repository).addr + : repository.repositoryName); this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, actionName: string, runOrder: number) { + protected getAction(output: Artifact, _actionName: string, runOrder: number) { return new cp_actions.CodeCommitSourceAction({ output, - actionName, + // Guaranteed to be okay as action name + actionName: this.repository.repositoryName, runOrder, branch: this.branch, trigger: this.props.trigger, diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts new file mode 100644 index 0000000000000..f1991792bfd86 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts @@ -0,0 +1,67 @@ +import { anything, arrayWith, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as ccommit from '@aws-cdk/aws-codecommit'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +test('CodeCommit source handles tokenized names correctly', () => { + const repo = new ccommit.Repository(pipelineStack, 'Repo', { + repositoryName: 'MyRepo', + }); + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: cdkp.CodePipelineSource.codeCommit(repo, 'main'), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Source', + Actions: [ + objectLike({ + Configuration: objectLike({ + RepositoryName: { 'Fn::GetAtt': [anything(), 'Name'] }, + }), + Name: { 'Fn::GetAtt': [anything(), 'Name'] }, + }), + ], + }), + }); +}); + +test('S3 source handles tokenized names correctly', () => { + const buckit = new s3.Bucket(pipelineStack, 'Buckit'); + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: cdkp.CodePipelineSource.s3(buckit, 'thefile.zip'), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Source', + Actions: [ + objectLike({ + Configuration: objectLike({ + S3Bucket: { Ref: anything() }, + S3ObjectKey: 'thefile.zip', + }), + Name: { Ref: anything() }, + }), + ], + }), + }); +}); + + +// a-z0-9.@-_ \ No newline at end of file From 27c176a181868f4af3fbc9941061bcd071fe0ae2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 16 Jul 2021 12:26:47 +0200 Subject: [PATCH 061/105] chore: add license notice, remove Docker instructions (#15599) From the CONTRIBUTING guide. - License notice was requested by our lawyers - Docker instructions haven't worked in forever: our build uses Docker itself so needs privileged mode, and the `--privileged` flag does not available for `docker build`. Closes #10438. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- CONTRIBUTING.md | 19 ++++--------------- Dockerfile | 11 ----------- 2 files changed, 4 insertions(+), 26 deletions(-) delete mode 100644 Dockerfile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70bdbcd8b42a7..97bc876d86a0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,10 @@ coming from the community. We want to recognize all your hard work by getting your code merged as quickly as we can, so please read the guidance here carefully to make sure the review process goes smoothly. -This document describes how to set up a development environment and submit your changes. Please +The CDK is released under the [Apache license](http://aws.amazon.com/apache2.0/). +Any code you submit will be released under that license. + +This document describes how to set up a development environment and submit your changes. Please let us know if it's not up-to-date (even better, submit a PR with your corrections ;-)). - [Getting Started](#getting-started) @@ -142,20 +145,6 @@ docker$ exit The `dist/` folder within each module contains the packaged up language artifacts. -## Docker Build (Alternative) - -Build the docker image: - -```console -$ docker build -t aws-cdk . -``` - -This allows you to run the CDK in a CDK-compatible directory with a command like: - -```console -$ docker run -v $(pwd):/app -w /app aws-cdk -``` - ## Gitpod (Alternative) You may also set up your local development environment using [Gitpod](http://gitpod.io) - diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ac1830af4ec59..0000000000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM jsii/superchain - -WORKDIR /app - -ARG BUILD_ARGS - -COPY . . - -RUN ./build.sh ${BUILD_ARGS} && ./link-all.sh - -ENTRYPOINT ["/app/node_modules/.bin/cdk"] From 22f2499508bccd3f44733705bbfa3c4e2b0b0d63 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 16 Jul 2021 14:38:51 +0200 Subject: [PATCH 062/105] fix(cli): `cdk deploy` is listing deprecated ids (#15603) The new recommended way of selecting stacks is by their construct path, however when prompted the CLI is printing the deprecated identifiers. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 2 +- packages/aws-cdk/test/api/cloud-assembly.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 690d626f8e8dd..f42b0ee640fc2 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -156,7 +156,7 @@ export class CloudAssembly { return new StackCollection(this, topLevelStacks); } else { throw new Error('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' + - `Stacks: ${stacks.map(x => x.id).join(' ')}`); + `Stacks: ${stacks.map(x => x.hierarchicalId).join(' · ')}`); } default: throw new Error(`invalid default behavior: ${defaultSelection}`); diff --git a/packages/aws-cdk/test/api/cloud-assembly.test.ts b/packages/aws-cdk/test/api/cloud-assembly.test.ts index 54d3a18f682ef..c17f108142a1e 100644 --- a/packages/aws-cdk/test/api/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/api/cloud-assembly.test.ts @@ -86,6 +86,15 @@ test('select behavior: single', async () => { .rejects.toThrow('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`'); }); +test('stack list error contains node paths', async () => { + // GIVEN + const cxasm = await testCloudAssembly(); + + // WHEN + await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.OnlySingle })) + .rejects.toThrow('withouterrorsNODEPATH'); +}); + test('select behavior: repeat', async () => { // GIVEN const cxasm = await testCloudAssembly(); @@ -147,6 +156,7 @@ async function testCloudAssembly({ env }: { env?: string, versionReporting?: boo const cloudExec = new MockCloudExecutable({ stacks: [{ stackName: 'withouterrors', + displayName: 'withouterrorsNODEPATH', env, template: { resource: 'noerrorresource' }, }, From ebba61830ea7ee73e168099d1cd8e8f4003d595c Mon Sep 17 00:00:00 2001 From: Bryan Pan Date: Fri, 16 Jul 2021 06:56:29 -0700 Subject: [PATCH 063/105] feat(pipelines): confirm IAM changes before starting the deployment (#15441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an option under `addApplication` for a given stage to create a CodeBuild that checks if there are any security changes within the stage's assembly. * If the changes exist: **manual approval is required** * else: a lambda function will automatically approve the manual approval action Adding a security check to an application creates two actions that precede the prepare and deploy actions of an application: 1. A CodeBuild Project that runs a security diff on the stage 2. A Manual Approval Action that can be approved via a shared Lambda function. ```txt Pipeline ├── Stage: Build │   └── ... ├── Stage: Synth │   └── ... ├── Stage: UpdatePipeline │   └── ... ├── Stage: MyApplicationStage │   └── Actions │      ├── MyApplicationSecurityCheck // Security Diff Action │      ├── MyApplicationManualApproval // Manual Approval Action │      ├── Stack.Prepare │      └── Stack.Deploy └── ... ```
Example Usage You can enable the security check in one of two ways: 1. Enable security check across the entire `CdkStage` ```ts const pipeline = new CdkPipeline(app, 'Pipeline', { // ...source and build information here (see above) }); const stage = pipeline.addApplicationStage(new MyApplication(this, 'Testing'), { securityCheck: true, }); // The 'PreProd' application is also run against a security diff because we configured // the stage to enable security checks stage.addApplication(new MyApplication(this, 'PreProd')); ``` 2. Enable security check for a single application ```ts const pipeline = new CdkPipeline(app, 'Pipeline', { // ...source and build information here (see above) }); const stage = pipeline.addApplicationStage(new MyApplication(this, 'NoCheck')); stage.addApplication(new MyApplication(this, 'RunSecurityDiff'), { securityCheck: true, }); ```
Fixes: #12748 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- package.json | 2 + packages/@aws-cdk/pipelines/ORIGINAL_API.md | 58 +- packages/@aws-cdk/pipelines/README.md | 64 +- .../codepipeline-action-factory.ts | 5 +- .../confirm-permissions-broadening.ts | 88 + .../pipelines/lib/codepipeline/index.ts | 1 + .../@aws-cdk/pipelines/lib/legacy/pipeline.ts | 27 +- .../@aws-cdk/pipelines/lib/legacy/stage.ts | 151 +- .../pipelines/lib/main/pipeline-base.ts | 16 +- .../lib/private/application-security-check.ts | 183 ++ .../lib/private/approve-lambda/index.ts | 48 + packages/@aws-cdk/pipelines/package.json | 48 +- .../test/compliance/security-check.test.ts | 362 +++ .../integ.pipeline-security.expected.json | 2399 +++++++++++++++++ .../pipelines/test/integ.pipeline-security.ts | 110 + 15 files changed, 3526 insertions(+), 36 deletions(-) create mode 100644 packages/@aws-cdk/pipelines/lib/codepipeline/confirm-permissions-broadening.ts create mode 100644 packages/@aws-cdk/pipelines/lib/private/application-security-check.ts create mode 100644 packages/@aws-cdk/pipelines/lib/private/approve-lambda/index.ts create mode 100644 packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts create mode 100644 packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json create mode 100644 packages/@aws-cdk/pipelines/test/integ.pipeline-security.ts diff --git a/package.json b/package.json index 73af824089036..96281e2f0dd36 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,8 @@ "@aws-cdk/core/minimatch/**", "@aws-cdk/cx-api/semver", "@aws-cdk/cx-api/semver/**", + "@aws-cdk/pipelines/aws-sdk", + "@aws-cdk/pipelines/aws-sdk/**", "@aws-cdk/yaml-cfn/yaml", "@aws-cdk/yaml-cfn/yaml/**", "aws-cdk-lib/@balena/dockerignore", diff --git a/packages/@aws-cdk/pipelines/ORIGINAL_API.md b/packages/@aws-cdk/pipelines/ORIGINAL_API.md index 3f1bd5920bcd2..d46acb44af989 100644 --- a/packages/@aws-cdk/pipelines/ORIGINAL_API.md +++ b/packages/@aws-cdk/pipelines/ORIGINAL_API.md @@ -495,4 +495,60 @@ const validationAction = new ShellScriptAction({ // 'test.js' was produced from 'test/test.ts' during the synth step commands: ['node ./test.js'], }); -``` \ No newline at end of file +``` + +### Confirm permissions broadening + +To keep tabs on the security impact of changes going out through your pipeline, +you can insert a security check before any stage deployment. This security check +will check if the upcoming deployment would add any new IAM permissions or +security group rules, and if so pause the pipeline and require you to confirm +the changes. + +The security check will appear as two distinct actions in your pipeline: first +a CodeBuild project that runs `cdk diff` on the stage that's about to be deployed, +followed by a Manual Approval action that pauses the pipeline. If it so happens +that there no new IAM permissions or security group rules will be added by the deployment, +the manual approval step is automatically satisfied. The pipeline will look like this: + +```txt +Pipeline +├── ... +├── MyApplicationStage +│   ├── MyApplicationSecurityCheck // Security Diff Action +│   ├── MyApplicationManualApproval // Manual Approval Action +│   ├── Stack.Prepare +│   └── Stack.Deploy +└── ... +``` + +You can enable the security check by passing `confirmBroadeningPermissions` to +`addApplicationStage`: + +```ts +const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), { + confirmBroadeningPermissions: true, +}); +``` + +To get notified when there is a change that needs your manual approval, +create an SNS Topic, subscribe your own email address, and pass it in via +`securityNotificationTopic`: + +```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 stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), { + confirmBroadeningPermissions: true, + securityNotificationTopic: topic, +}); +``` + +**Note**: Manual Approvals notifications only apply when an application has security +check enabled. \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 76a8c3a84f8fb..711a73f7e64e1 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -840,7 +840,7 @@ and orphan the old bucket. You should manually delete the orphaned bucket after you are sure you have redeployed all CDK applications and there are no more references to the old asset bucket. -## Security Tips +## Security Considerations It's important to stay safe while employing Continuous Delivery. The CDK Pipelines library comes with secure defaults to the best of our ability, but by its @@ -862,6 +862,68 @@ We therefore expect you to mind the following: changes can be deployed through git. Avoid the chances of credentials leaking by not having them in the first place! +### Confirm permissions broadening + +To keep tabs on the security impact of changes going out through your pipeline, +you can insert a security check before any stage deployment. This security check +will check if the upcoming deployment would add any new IAM permissions or +security group rules, and if so pause the pipeline and require you to confirm +the changes. + +The security check will appear as two distinct actions in your pipeline: first +a CodeBuild project that runs `cdk diff` on the stage that's about to be deployed, +followed by a Manual Approval action that pauses the pipeline. If it so happens +that there no new IAM permissions or security group rules will be added by the deployment, +the manual approval step is automatically satisfied. The pipeline will look like this: + +```txt +Pipeline +├── ... +├── MyApplicationStage +│   ├── MyApplicationSecurityCheck // Security Diff Action +│   ├── MyApplicationManualApproval // Manual Approval Action +│   ├── Stack.Prepare +│   └── Stack.Deploy +└── ... +``` + +You can insert the security check by using a `ConfirmPermissionsBroadening` step: + +```ts +const stage = new MyApplicationStage(this, 'MyApplication'); +pipeline.addStage(stage, { + pre: [ + new ConfirmPermissionsBroadening('Check', { stage }), + ], +}); +``` + +To get notified when there is a change that needs your manual approval, +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'; + +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', { + stage, + notificationTopic: topic, + }), + ], +}); +``` + +**Note**: Manual Approvals notifications only apply when an application has security +check enabled. + ## Troubleshooting Here are some common errors you may encounter while using this library. diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts index 89d419b56223d..62c9fa86d025b 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts @@ -1,9 +1,8 @@ import * as cb from '@aws-cdk/aws-codebuild'; import * as cp from '@aws-cdk/aws-codepipeline'; import { Construct } from 'constructs'; -import { PipelineBase } from '../main'; import { ArtifactMap } from './artifact-map'; -import { CodeBuildOptions } from './codepipeline'; +import { CodeBuildOptions, CodePipeline } from './codepipeline'; /** * Options for the `CodePipelineActionFactory.produce()` method. @@ -43,7 +42,7 @@ export interface ProduceActionOptions { /** * The pipeline the action is being generated for */ - readonly pipeline: PipelineBase; + readonly pipeline: CodePipeline; /** * If this action factory creates a CodeBuild step, default options to inherit diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/confirm-permissions-broadening.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/confirm-permissions-broadening.ts new file mode 100644 index 0000000000000..95b66267be25c --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/confirm-permissions-broadening.ts @@ -0,0 +1,88 @@ +import { IStage } from '@aws-cdk/aws-codepipeline'; +import * as cpa from '@aws-cdk/aws-codepipeline-actions'; +import * as sns from '@aws-cdk/aws-sns'; +import { Stage } from '@aws-cdk/core'; +import { Node } from 'constructs'; +import { Step } from '../blueprint'; +import { ApplicationSecurityCheck } from '../private/application-security-check'; +import { CodePipeline } from './codepipeline'; +import { CodePipelineActionFactoryResult, ICodePipelineActionFactory, ProduceActionOptions } from './codepipeline-action-factory'; + +/** + * Properties for a `PermissionsBroadeningCheck` + */ +export interface PermissionsBroadeningCheckProps { + /** + * The CDK Stage object to check the stacks of + * + * This should be the same Stage object you are passing to `addStage()`. + */ + readonly stage: Stage; + + /** + * Topic to send notifications when a human needs to give manual confirmation + * + * @default - no notification + */ + readonly notificationTopic?: sns.ITopic +} + +/** + * Pause the pipeline if a deployment would add IAM permissions or Security Group rules + * + * This step is only supported in CodePipeline pipelines. + */ +export class ConfirmPermissionsBroadening extends Step implements ICodePipelineActionFactory { + constructor(id: string, private readonly props: PermissionsBroadeningCheckProps) { + super(id); + } + + public produceAction(stage: IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { + const sec = this.getOrCreateSecCheck(options.pipeline); + this.props.notificationTopic?.grantPublish(sec.cdkDiffProject); + + const variablesNamespace = Node.of(this.props.stage).addr; + + const approveActionName = `${options.actionName}.Confirm`; + stage.addAction(new cpa.CodeBuildAction({ + runOrder: options.runOrder, + actionName: `${options.actionName}.Check`, + input: options.artifacts.toCodePipeline(options.pipeline.cloudAssemblyFileSet), + project: sec.cdkDiffProject, + variablesNamespace, + environmentVariables: { + STAGE_PATH: { value: Node.of(this.props.stage).path }, + STAGE_NAME: { value: stage.stageName }, + ACTION_NAME: { value: approveActionName }, + ...this.props.notificationTopic ? { + NOTIFICATION_ARN: { value: this.props.notificationTopic.topicArn }, + NOTIFICATION_SUBJECT: { value: `Confirm permission broadening in ${this.props.stage.stageName}` }, + } : {}, + }, + })); + + stage.addAction(new cpa.ManualApprovalAction({ + actionName: approveActionName, + runOrder: options.runOrder + 1, + additionalInformation: `#{${variablesNamespace}.MESSAGE}`, + externalEntityLink: `#{${variablesNamespace}.LINK}`, + })); + + return { runOrdersConsumed: 2 }; + } + + private getOrCreateSecCheck(pipeline: CodePipeline): ApplicationSecurityCheck { + const id = 'PipelinesSecurityCheck'; + const existing = Node.of(pipeline).tryFindChild(id); + if (existing) { + if (!(existing instanceof ApplicationSecurityCheck)) { + throw new Error(`Expected '${Node.of(existing).path}' to be 'ApplicationSecurityCheck' but was '${existing}'`); + } + return existing; + } + + return new ApplicationSecurityCheck(pipeline, id, { + codePipeline: pipeline.pipeline, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts index 00e10509bb0df..4b2a86d61fc4d 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts @@ -1,5 +1,6 @@ export * from './artifact-map'; export * from './codebuild-step'; +export * from './confirm-permissions-broadening'; export * from './codepipeline'; export * from './codepipeline-action-factory'; export * from './codepipeline-source'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts index 95a828b981ea1..ec09a80dccb0e 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts @@ -6,14 +6,15 @@ import { Annotations, App, Aws, CfnOutput, Fn, Lazy, PhysicalName, Stack, Stage import { Construct } from 'constructs'; import { AssetType } from '../blueprint/asset-type'; import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; +import { ApplicationSecurityCheck } from '../private/application-security-check'; import { appOf, assemblyBuilderOf } from '../private/construct-internals'; import { DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; -import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from './stage'; +import { AddStageOptions, AssetPublishingCommand, BaseStageOptions, CdkStage, StackOutput } from './stage'; +import { SimpleSynthAction } from './synths'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; -import { SimpleSynthAction } from './synths'; const CODE_BUILD_LENGTH_LIMIT = 100; /** @@ -184,6 +185,7 @@ export class CdkPipeline extends CoreConstruct { private readonly _outputArtifacts: Record = {}; private readonly _cloudAssemblyArtifact: codepipeline.Artifact; private readonly _dockerCredentials: DockerCredential[]; + private _applicationSecurityCheck?: ApplicationSecurityCheck; constructor(scope: Construct, id: string, props: CdkPipelineProps) { super(scope, id); @@ -288,6 +290,22 @@ export class CdkPipeline extends CoreConstruct { return this._pipeline.stage(stageName); } + /** + * Get a cached version of an Application Security Check, which consists of: + * - CodeBuild Project to check for security changes in a stage + * - Lambda Function that approves the manual approval if no security changes are detected + * + * @internal + */ + public _getApplicationSecurityCheck(): ApplicationSecurityCheck { + if (!this._applicationSecurityCheck) { + this._applicationSecurityCheck = new ApplicationSecurityCheck(this, 'PipelineApplicationSecurityCheck', { + codePipeline: this._pipeline, + }); + } + return this._applicationSecurityCheck; + } + /** * Add pipeline stage that will deploy the given application stage * @@ -300,7 +318,7 @@ export class CdkPipeline extends CoreConstruct { * publishing stage. */ public addApplicationStage(appStage: Stage, options: AddStageOptions = {}): CdkStage { - const stage = this.addStage(appStage.stageName); + const stage = this.addStage(appStage.stageName, options); stage.addApplication(appStage, options); return stage; } @@ -312,7 +330,7 @@ export class CdkPipeline extends CoreConstruct { * application, but you can use this method if you want to add other kinds of * Actions to a pipeline. */ - public addStage(stageName: string) { + public addStage(stageName: string, options?: BaseStageOptions) { const pipelineStage = this._pipeline.addStage({ stageName, }); @@ -325,6 +343,7 @@ export class CdkPipeline extends CoreConstruct { publishAsset: this._assets.addPublishAssetAction.bind(this._assets), stackOutputArtifact: (artifactId) => this._outputArtifacts[artifactId], }, + ...options, }); this._stages.push(stage); return stage; diff --git a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts index 55c847d984a58..ee4b860848503 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts @@ -1,12 +1,17 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as cpactions from '@aws-cdk/aws-codepipeline-actions'; +import { CodeBuildAction } from '@aws-cdk/aws-codepipeline-actions'; +import * as sns from '@aws-cdk/aws-sns'; import { Stage, Aspects } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { AssetType } from '../blueprint/asset-type'; +import { ApplicationSecurityCheck } from '../private/application-security-check'; import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; import { topologicalSort } from '../private/toposort'; import { DeployCdkStackAction } from './actions'; +import { CdkPipeline } from './pipeline'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line @@ -35,8 +40,30 @@ export interface CdkStageProps { * Features the Stage needs from its environment */ readonly host: IStageHost; + + /** + * Run a security check before every application prepare/deploy actions. + * + * Note: Stage level security check can be overriden per application as follows: + * `stage.addApplication(app, { confirmBroadeningPermissions: false })` + * + * @default false + */ + readonly confirmBroadeningPermissions?: boolean; + + /** + * Optional SNS topic to send notifications to when any security check registers + * changes within a application. + * + * Note: The Stage Notification Topic can be overriden per application as follows: + * `stage.addApplication(app, { securityNotificationTopic: newTopic })` + * + * @default undefined no stage level notification topic + */ + readonly securityNotificationTopic?: sns.ITopic; } + /** * Stage in a CdkPipeline * @@ -51,15 +78,25 @@ export class CdkStage extends CoreConstruct { private readonly stacksToDeploy = new Array(); private readonly stageName: string; private readonly host: IStageHost; + private readonly confirmBroadeningPermissions: boolean; + private readonly pipeline?: CdkPipeline; + private readonly securityNotificationTopic?: sns.ITopic; + private _applicationSecurityCheck?: ApplicationSecurityCheck; private _prepared = false; constructor(scope: Construct, id: string, props: CdkStageProps) { super(scope, id); + if (scope instanceof CdkPipeline) { + this.pipeline = scope; + } + this.stageName = props.stageName; this.pipelineStage = props.pipelineStage; this.cloudAssemblyArtifact = props.cloudAssemblyArtifact; this.host = props.host; + this.confirmBroadeningPermissions = props.confirmBroadeningPermissions ?? false; + this.securityNotificationTopic = props.securityNotificationTopic; Aspects.of(this).add({ visit: () => this.prepareStage() }); } @@ -79,6 +116,10 @@ export class CdkStage extends CoreConstruct { const asm = appStage.synth({ validateOnSynthesis: true }); const extraRunOrderSpace = options.extraRunOrderSpace ?? 0; + if (options.confirmBroadeningPermissions ?? this.confirmBroadeningPermissions) { + this.addSecurityCheck(appStage, options); + } + if (asm.stacks.length === 0) { // If we don't check here, a more puzzling "stage contains no actions" // error will be thrown come deployment time. @@ -108,6 +149,30 @@ export class CdkStage extends CoreConstruct { } } + /** + * Get a cached version of an ApplicationSecurityCheck, which consists of: + * - CodeBuild Project to check for security changes in a stage + * - Lambda Function that approves the manual approval if no security changes are detected + * + * The ApplicationSecurityCheck is cached from the pipeline **if** this stage is scoped + * to a CDK Pipeline. If this stage **is not** scoped to a pipeline, create an ApplicationSecurityCheck + * scoped to the stage itself. + * + * @internal + */ + private getApplicationSecurityCheck(): ApplicationSecurityCheck { + if (this._applicationSecurityCheck) { + return this._applicationSecurityCheck; + } + + this._applicationSecurityCheck = this.pipeline + ? this.pipeline._getApplicationSecurityCheck() + : new ApplicationSecurityCheck(this, 'StageApplicationSecurityCheck', { + codePipeline: this.pipelineStage.pipeline as codepipeline.Pipeline, + }); + return this._applicationSecurityCheck; + } + /** * Add a deployment action based on a stack artifact */ @@ -225,6 +290,61 @@ export class CdkStage extends CoreConstruct { return stripPrefix(s, `${this.stageName}-`); } + /** + * Add a security check before the prepare/deploy actions of an CDK stage. + * The security check consists of two actions: + * - CodeBuild Action to check for security changes in a stage + * - Manual Approval Action that is auto approved via a Lambda if no security changes detected + */ + private addSecurityCheck(appStage: Stage, options?: BaseStageOptions) { + const { cdkDiffProject } = this.getApplicationSecurityCheck(); + const notificationTopic: sns.ITopic | undefined = options?.securityNotificationTopic ?? this.securityNotificationTopic; + notificationTopic?.grantPublish(cdkDiffProject); + + const appStageName = appStage.stageName; + const approveActionName = `${appStageName}ManualApproval`; + const diffAction = new CodeBuildAction({ + runOrder: this.nextSequentialRunOrder(), + actionName: `${appStageName}SecurityCheck`, + input: this.cloudAssemblyArtifact, + project: cdkDiffProject, + variablesNamespace: `${appStageName}SecurityCheck`, + environmentVariables: { + STAGE_PATH: { + value: this.pipelineStage.pipeline.stack.stackName, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + STAGE_NAME: { + value: this.stageName, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + ACTION_NAME: { + value: approveActionName, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + ...notificationTopic ? { + NOTIFICATION_ARN: { + value: notificationTopic.topicArn, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + NOTIFICATION_SUBJECT: { + value: `Confirm permission broadening in ${appStageName}`, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + } : {}, + }, + }); + + const approve = new cpactions.ManualApprovalAction({ + actionName: approveActionName, + runOrder: this.nextSequentialRunOrder(), + additionalInformation: `#{${appStageName}SecurityCheck.MESSAGE}`, + externalEntityLink: `#{${appStageName}SecurityCheck.LINK}`, + }); + + this.addActions(diffAction, approve); + } + /** * Make sure all assets depended on by this stack are published in this pipeline * @@ -370,10 +490,39 @@ export interface AssetPublishingCommand { readonly assetPublishingRoleArn: string; } +/** + * Base options for a pipelines stage + */ +export interface BaseStageOptions { + /** + * Runs a `cdk diff --security-only --fail` to pause the pipeline if there + * are any security changes. + * + * If the stage is configured with `confirmBroadeningPermissions` enabled, you can use this + * property to override the stage configuration. For example, Pipeline Stage + * "Prod" has confirmBroadeningPermissions enabled, with applications "A", "B", "C". All three + * applications will run a security check, but if we want to disable the one for "C", + * we run `stage.addApplication(C, { confirmBroadeningPermissions: false })` to override the pipeline + * stage behavior. + * + * Adds 1 to the run order space. + * + * @default false + */ + readonly confirmBroadeningPermissions?: boolean; + /** + * Optional SNS topic to send notifications to when the security check registers + * changes within the application. + * + * @default undefined no notification topic for security check manual approval action + */ + readonly securityNotificationTopic?: sns.ITopic; +} + /** * Options for adding an application stage to a pipeline */ -export interface AddStageOptions { +export interface AddStageOptions extends BaseStageOptions { /** * Add manual approvals before executing change sets * diff --git a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts index 563697746a8cf..6ff5a1be60853 100644 --- a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts +++ b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts @@ -1,6 +1,6 @@ import { Aspects, Stage } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AddStageOpts as StageOptions, WaveOptions, Wave, IFileSetProducer, ShellStep } from '../blueprint'; +import { AddStageOpts as StageOptions, WaveOptions, Wave, IFileSetProducer, ShellStep, FileSet } from '../blueprint'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line @@ -46,6 +46,13 @@ export abstract class PipelineBase extends CoreConstruct { */ public readonly waves: Wave[]; + /** + * The FileSet tha contains the cloud assembly + * + * This is the primary output of the synth step. + */ + public readonly cloudAssemblyFileSet: FileSet; + private built = false; constructor(scope: Construct, id: string, props: PipelineBaseProps) { @@ -55,13 +62,14 @@ export abstract class PipelineBase extends CoreConstruct { props.synth.primaryOutputDirectory('cdk.out'); } - this.synth = props.synth; - this.waves = []; - if (!props.synth.primaryOutput) { throw new Error(`synthStep ${props.synth} must produce a primary output, but is not producing anything. Configure the Step differently or use a different Step type.`); } + this.synth = props.synth; + this.waves = []; + this.cloudAssemblyFileSet = props.synth.primaryOutput; + Aspects.of(this).add({ visit: () => this.buildJustInTime() }); } diff --git a/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts b/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts new file mode 100644 index 0000000000000..152404db70a30 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts @@ -0,0 +1,183 @@ +import * as path from 'path'; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Duration, Tags } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properteis for an ApplicationSecurityCheck + */ +export interface ApplicationSecurityCheckProps { + /** + * The pipeline that will be automatically approved + * + * Will have a tag added to it. + */ + readonly codePipeline: cp.Pipeline; +} + +/** + * A construct containing both the Lambda and CodeBuild Project + * needed to conduct a security check on any given application stage. + * + * The Lambda acts as an auto approving mechanism that should only be + * triggered when the CodeBuild Project registers no security changes. + * + * The CodeBuild Project runs a security diff on the application stage, + * and exports the link to the console of the project. + */ +export class ApplicationSecurityCheck extends CoreConstruct { + /** + * A lambda function that approves a Manual Approval Action, given + * the following payload: + * + * { + * "PipelineName": [CodePipelineName], + * "StageName": [CodePipelineStageName], + * "ActionName": [ManualApprovalActionName] + * } + */ + public readonly preApproveLambda: lambda.Function; + /** + * A CodeBuild Project that runs a security diff on the application stage. + * + * - If the diff registers no security changes, CodeBuild will invoke the + * pre-approval lambda and approve the ManualApprovalAction. + * - If changes are detected, CodeBuild will exit into a ManualApprovalAction + */ + public readonly cdkDiffProject: codebuild.Project; + + constructor(scope: Construct, id: string, props: ApplicationSecurityCheckProps) { + super(scope, id); + + Tags.of(props.codePipeline).add('SECURITY_CHECK', 'ALLOW_APPROVE', { + includeResourceTypes: ['AWS::CodePipeline::Pipeline'], + }); + + this.preApproveLambda = new lambda.Function(this, 'CDKPipelinesAutoApprove', { + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + code: lambda.Code.fromAsset(path.resolve(__dirname, 'approve-lambda')), + timeout: Duration.minutes(5), + }); + + this.preApproveLambda.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codepipeline:GetPipelineState', 'codepipeline:PutApprovalResult'], + conditions: { + StringEquals: { + 'aws:ResourceTag/SECURITY_CHECK': 'ALLOW_APPROVE', + }, + }, + resources: ['*'], + })); + + const invokeLambda = + 'aws lambda invoke' + + ` --function-name ${this.preApproveLambda.functionName}` + + ' --invocation-type Event' + + ' --payload "$payload"' + + ' lambda.out'; + + const message = [ + 'An upcoming change would broaden security changes in $PIPELINE_NAME.', + 'Review and approve the changes in CodePipeline to proceed with the deployment.', + '', + 'Review the changes in CodeBuild:', + '', + '$LINK', + '', + 'Approve the changes in CodePipeline (stage $STAGE_NAME, action $ACTION_NAME):', + '', + '$PIPELINE_LINK', + ]; + const publishNotification = + 'aws sns publish' + + ' --topic-arn $NOTIFICATION_ARN' + + ' --subject "$NOTIFICATION_SUBJECT"' + + ` --message "${message.join('\n')}"`; + + this.cdkDiffProject = new codebuild.Project(this, 'CDKSecurityCheck', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: 0.2, + phases: { + build: { + commands: [ + 'npm install -g aws-cdk', + // $CODEBUILD_INITIATOR will always be Code Pipeline and in the form of: + // "codepipeline/example-pipeline-name-Xxx" + 'export PIPELINE_NAME="$(node -pe \'`${process.env.CODEBUILD_INITIATOR}`.split("/")[1]\')"', + 'payload="$(node -pe \'JSON.stringify({ "PipelineName": process.env.PIPELINE_NAME, "StageName": process.env.STAGE_NAME, "ActionName": process.env.ACTION_NAME })\' )"', + // ARN: "arn:aws:codebuild:$region:$account_id:build/$project_name:$project_execution_id$" + 'ARN=$CODEBUILD_BUILD_ARN', + 'REGION="$(node -pe \'`${process.env.ARN}`.split(":")[3]\')"', + 'ACCOUNT_ID="$(node -pe \'`${process.env.ARN}`.split(":")[4]\')"', + 'PROJECT_NAME="$(node -pe \'`${process.env.ARN}`.split(":")[5].split("/")[1]\')"', + 'PROJECT_ID="$(node -pe \'`${process.env.ARN}`.split(":")[6]\')"', + // Manual Approval adds 'http/https' to the resolved link + 'export LINK="https://$REGION.console.aws.amazon.com/codesuite/codebuild/$ACCOUNT_ID/projects/$PROJECT_NAME/build/$PROJECT_NAME:$PROJECT_ID/?region=$REGION"', + 'export PIPELINE_LINK="https://$REGION.console.aws.amazon.com/codesuite/codepipeline/pipelines/$PIPELINE_NAME/view?region=$REGION"', + // Run invoke only if cdk diff passes (returns exit code 0) + // 0 -> true, 1 -> false + ifElse({ + condition: 'cdk diff -a . --security-only --fail $STAGE_PATH/\\*', + thenStatements: [ + invokeLambda, + 'export MESSAGE="No security-impacting changes detected."', + ], + elseStatements: [ + `[ -z "\${NOTIFICATION_ARN}" ] || ${publishNotification}`, + 'export MESSAGE="Deployment would make security-impacting changes. Click the link below to inspect them, then click Approve if all changes are expected."', + ], + }), + ], + }, + }, + env: { + 'exported-variables': [ + 'LINK', + 'MESSAGE', + ], + }, + }), + }); + + // this is needed to check the status the stacks when doing `cdk diff` + this.cdkDiffProject.addToRolePolicy(new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: ['*'], + conditions: { + 'ForAnyValue:StringEquals': { + 'iam:ResourceTag/aws-cdk:bootstrap-role': ['deploy'], + }, + }, + })); + + this.preApproveLambda.grantInvoke(this.cdkDiffProject); + } +} + +interface ifElseOptions { + readonly condition: string, + readonly thenStatements: string[], + readonly elseStatements?: string[] +} + +const ifElse = ({ condition, thenStatements, elseStatements }: ifElseOptions): string => { + let statement = thenStatements.reduce((acc, ifTrue) => { + return `${acc} ${ifTrue};`; + }, `if ${condition}; then`); + + if (elseStatements) { + statement = elseStatements.reduce((acc, ifFalse) => { + return `${acc} ${ifFalse};`; + }, `${statement} else`); + } + + return `${statement} fi`; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/approve-lambda/index.ts b/packages/@aws-cdk/pipelines/lib/private/approve-lambda/index.ts new file mode 100644 index 0000000000000..0eadb9d9871e3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/approve-lambda/index.ts @@ -0,0 +1,48 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import * as AWS from 'aws-sdk'; + +const client = new AWS.CodePipeline({ apiVersion: '2015-07-09' }); +const TIMEOUT_IN_MINUTES = 5; + +const sleep = (seconds: number) => { + return new Promise(resolve => setTimeout(resolve, seconds * 1000)); +}; + +export async function handler(event: any, _context: any) { + const { + PipelineName: pipelineName, + StageName: stageName, + ActionName: actionName, + } = event; + + function parseState(response: any): string | undefined { + const stages = response.stageStates; + const validStages = stages?.filter((s: any) => s.stageName === stageName); + const manualApproval = validStages.length && + validStages[0].actionStates.filter((state: any) => state.actionName === actionName); + const latest = manualApproval && manualApproval.length && + manualApproval[0].latestExecution; + + return latest ? latest.token : undefined; + } + + const deadline = Date.now() + TIMEOUT_IN_MINUTES * 60000; + while (Date.now() < deadline) { + const response = await client.getPipelineState({ name: pipelineName }).promise(); + const token = parseState(response); + if (token) { + await client.putApprovalResult({ + pipelineName, + actionName, + stageName, + result: { + summary: 'No security changes detected. Automatically approved by Lambda.', + status: 'Approved', + }, + token, + }).promise(); + return; + } + await sleep(5); + } +} diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 1db513581a96e..45252fe965ac7 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -32,52 +32,56 @@ "organization": true }, "devDependencies": { - "@types/jest": "^26.0.24", + "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-ecr-assets": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", + "@aws-cdk/aws-sns-subscriptions": "0.0.0", + "@types/jest": "^26.0.23", + "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "pkglint": "0.0.0", - "@aws-cdk/aws-s3": "0.0.0", - "@aws-cdk/aws-sqs": "0.0.0", - "@aws-cdk/aws-ecr": "0.0.0", - "@aws-cdk/aws-ecr-assets": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "pkglint": "0.0.0" }, "peerDependencies": { - "constructs": "^3.3.69", - "@aws-cdk/core": "0.0.0", - "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", - "@aws-cdk/aws-ec2": "0.0.0", - "@aws-cdk/aws-secretsmanager": "0.0.0", - "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", - "@aws-cdk/cx-api": "0.0.0" + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", + "constructs": "^3.3.69" }, "dependencies": { - "constructs": "^3.3.69", - "@aws-cdk/core": "0.0.0", - "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", - "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", - "@aws-cdk/aws-ec2": "0.0.0", - "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", - "@aws-cdk/cx-api": "0.0.0" + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", + "constructs": "^3.3.69" }, - "bundledDependencies": [], "keywords": [ "aws", "cdk", diff --git a/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts b/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts new file mode 100644 index 0000000000000..e698f355b8609 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts @@ -0,0 +1,362 @@ +import { arrayWith, objectLike, stringLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { Topic } from '@aws-cdk/aws-sns'; +import { Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, TestApp } from '../testhelpers'; +import { behavior } from '../testhelpers/compliance'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineSecurityStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('security check option generates lambda/codebuild at pipeline scope', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'App'), { confirmBroadeningPermissions: true }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const stage = new OneStackApp(app, 'App'); + pipeline.addStage(stage, { + pre: [ + new cdkp.ConfirmPermissionsBroadening('Check', { + stage, + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toCountResources('AWS::Lambda::Function', 1); + expect(pipelineStack).toHaveResourceLike('AWS::Lambda::Function', { + Role: { + 'Fn::GetAtt': [ + stringLike('CdkPipeline*SecurityCheckCDKPipelinesAutoApproveServiceRole*'), + 'Arn', + ], + }, + }); + // 1 for github build, 1 for synth stage, and 1 for the application security check + expect(pipelineStack).toCountResources('AWS::CodeBuild::Project', 3); + } +}); + +behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid permissions', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'App'), { confirmBroadeningPermissions: true }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const stage = new OneStackApp(app, 'App'); + pipeline.addStage(stage, { + pre: [ + new cdkp.ConfirmPermissionsBroadening('Check', { + stage, + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // CodePipeline must be tagged as SECURITY_CHECK=ALLOW_APPROVE + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', { + Tags: [ + { + Key: 'SECURITY_CHECK', + Value: 'ALLOW_APPROVE', + }, + ], + }); + // Lambda Function only has access to pipelines tagged SECURITY_CHECK=ALLOW_APPROVE + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['codepipeline:GetPipelineState', 'codepipeline:PutApprovalResult'], + Condition: { + StringEquals: { 'aws:ResourceTag/SECURITY_CHECK': 'ALLOW_APPROVE' }, + }, + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + // CodeBuild must have access to the stacks and invoking the lambda function + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith( + { + Action: 'sts:AssumeRole', + Condition: { + 'ForAnyValue:StringEquals': { + 'iam:ResourceTag/aws-cdk:bootstrap-role': [ + 'deploy', + ], + }, + }, + Effect: 'Allow', + Resource: '*', + }, + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + stringLike('*AutoApprove*'), + 'Arn', + ], + }, + }, + ), + }, + }); + } +}); + +behavior('confirmBroadeningPermissions option at addApplicationStage runs security check on all apps unless overriden', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const securityStage = pipeline.addApplicationStage(new OneStackApp(app, 'StageSecurityCheckStack'), { confirmBroadeningPermissions: true }); + securityStage.addApplication(new OneStackApp(app, 'AnotherStack')); + securityStage.addApplication(new OneStackApp(app, 'SkipCheckStack'), { confirmBroadeningPermissions: false }); + + THEN_codePipelineExpectation(); + }); + + // For the modern API, there is no inheritance + suite.doesNotApply.modern(); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + { + Actions: [{ Name: 'GitHub', RunOrder: 1 }], + Name: 'Source', + }, + { + Actions: [{ Name: 'Synth', RunOrder: 1 }], + Name: 'Build', + }, + { + Actions: [{ Name: 'SelfMutate', RunOrder: 1 }], + Name: 'UpdatePipeline', + }, + { + Actions: [ + { Name: 'StageSecurityCheckStackSecurityCheck', RunOrder: 1 }, + { Name: 'StageSecurityCheckStackManualApproval', RunOrder: 2 }, + { Name: 'AnotherStackSecurityCheck', RunOrder: 5 }, + { Name: 'AnotherStackManualApproval', RunOrder: 6 }, + { Name: 'Stack.Prepare', RunOrder: 3 }, + { Name: 'Stack.Deploy', RunOrder: 4 }, + { Name: 'AnotherStack-Stack.Prepare', RunOrder: 7 }, + { Name: 'AnotherStack-Stack.Deploy', RunOrder: 8 }, + { Name: 'SkipCheckStack-Stack.Prepare', RunOrder: 9 }, + { Name: 'SkipCheckStack-Stack.Deploy', RunOrder: 10 }, + ], + Name: 'StageSecurityCheckStack', + }, + ], + }); + } +}); + +behavior('confirmBroadeningPermissions option at addApplication runs security check only on selected application', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const noSecurityStage = pipeline.addApplicationStage(new OneStackApp(app, 'NoSecurityCheckStack')); + noSecurityStage.addApplication(new OneStackApp(app, 'EnableCheckStack'), { confirmBroadeningPermissions: true }); + + THEN_codePipelineExpectation(); + }); + + // For the modern API, there is no inheritance + suite.doesNotApply.modern(); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + { + Actions: [{ Name: 'GitHub', RunOrder: 1 }], + Name: 'Source', + }, + { + Actions: [{ Name: 'Synth', RunOrder: 1 }], + Name: 'Build', + }, + { + Actions: [{ Name: 'SelfMutate', RunOrder: 1 }], + Name: 'UpdatePipeline', + }, + { + Actions: [ + { Name: 'EnableCheckStackSecurityCheck', RunOrder: 3 }, + { Name: 'EnableCheckStackManualApproval', RunOrder: 4 }, + { Name: 'Stack.Prepare', RunOrder: 1 }, + { Name: 'Stack.Deploy', RunOrder: 2 }, + { Name: 'EnableCheckStack-Stack.Prepare', RunOrder: 5 }, + { Name: 'EnableCheckStack-Stack.Deploy', RunOrder: 6 }, + ], + Name: 'NoSecurityCheckStack', + }, + ], + }); + } +}); + +behavior('confirmBroadeningPermissions and notification topic options generates the right resources', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const topic = new Topic(pipelineStack, 'NotificationTopic'); + pipeline.addApplicationStage(new OneStackApp(app, 'MyStack'), { + confirmBroadeningPermissions: true, + securityNotificationTopic: topic, + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const topic = new Topic(pipelineStack, 'NotificationTopic'); + const stage = new OneStackApp(app, 'MyStack'); + pipeline.addStage(stage, { + pre: [ + new cdkp.ConfirmPermissionsBroadening('Approve', { + stage, + notificationTopic: topic, + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toCountResources('AWS::SNS::Topic', 1); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith( + { + Name: 'MyStack', + Actions: [ + objectLike({ + Configuration: { + ProjectName: { Ref: stringLike('*SecurityCheck*') }, + EnvironmentVariables: { + 'Fn::Join': ['', [ + stringLike('*'), + { Ref: 'NotificationTopicEB7A0DF1' }, + stringLike('*'), + ]], + }, + }, + Name: stringLike('*Check'), + Namespace: stringLike('*'), + RunOrder: 1, + }), + objectLike({ + Configuration: { + CustomData: stringLike('#{*.MESSAGE}'), + ExternalEntityLink: stringLike('#{*.LINK}'), + }, + Name: stringLike('*Approv*'), + RunOrder: 2, + }), + objectLike({ Name: 'Stack.Prepare', RunOrder: 3 }), + objectLike({ Name: 'Stack.Deploy', RunOrder: 4 }), + ], + }, + ), + }); + } +}); + +behavior('Stages declared outside the pipeline create their own ApplicationSecurityCheck', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const pipelineStage = pipeline.codePipeline.addStage({ + stageName: 'UnattachedStage', + }); + + const unattachedStage = new cdkp.CdkStage(pipelineStack, 'UnattachedStage', { + stageName: 'UnattachedStage', + pipelineStage, + cloudAssemblyArtifact: pipeline.cloudAssemblyArtifact, + host: { + publishAsset: () => undefined, + stackOutputArtifact: () => undefined, + }, + }); + + unattachedStage.addApplication(new OneStackApp(app, 'UnattachedStage'), { + confirmBroadeningPermissions: true, + }); + + THEN_codePipelineExpectation(); + }); + + // Not a valid use of the modern API + suite.doesNotApply.modern(); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toCountResources('AWS::Lambda::Function', 1); + // 1 for github build, 1 for synth stage, and 1 for the application security check + expect(pipelineStack).toCountResources('AWS::CodeBuild::Project', 3); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Tags: [ + { + Key: 'SECURITY_CHECK', + Value: 'ALLOW_APPROVE', + }, + ], + Stages: [ + { Name: 'Source' }, + { Name: 'Build' }, + { Name: 'UpdatePipeline' }, + { + Actions: [ + { + Configuration: { + ProjectName: { Ref: 'UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B' }, + }, + Name: 'UnattachedStageSecurityCheck', + RunOrder: 1, + }, + { + Configuration: { + CustomData: '#{UnattachedStageSecurityCheck.MESSAGE}', + ExternalEntityLink: '#{UnattachedStageSecurityCheck.LINK}', + }, + Name: 'UnattachedStageManualApproval', + RunOrder: 2, + }, + { Name: 'Stack.Prepare', RunOrder: 3 }, + { Name: 'Stack.Deploy', RunOrder: 4 }, + ], + Name: 'UnattachedStage', + }, + ], + }); + } +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json new file mode 100644 index 0000000000000..3495d780cecdd --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json @@ -0,0 +1,2399 @@ +{ + "Resources": { + "TestPipelineArtifactsBucketEncryptionKey13258842": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TestPipelineArtifactsBucketEncryptionKeyAliasE8D86DD3": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-pipelinesecuritystacktestpipelinef7060861", + "TargetKeyId": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TestPipelineArtifactsBucket026AF2F9": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TestPipelineArtifactsBucketPolicyDF75C611": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "TestPipelineArtifactsBucket026AF2F9" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineRole63C35BBD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineRoleDefaultPolicyFA69BF2D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineBuildSynthCodePipelineActionRoleF7BF5926", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleFF6E43E2", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineUnattachedStageSingleStageManualApprovalCodePipelineActionRoleF7A614C8", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRole4E54C194", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePreProductionPreProductionManualApprovalCodePipelineActionRole81B9C4F9", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRole399C68A6", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePreProductionSafeProductionManualApprovalCodePipelineActionRole4F30C0D9", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRole8D10AA6D", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineNoSecurityCheckEnableSecurityCheckManualApprovalCodePipelineActionRole27FC4015", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineRoleDefaultPolicyFA69BF2D", + "Roles": [ + { + "Ref": "TestPipelineRole63C35BBD" + } + ] + } + }, + "TestPipeline34ACDBF9": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineRole63C35BBD", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "OWNER", + "Repo": "REPO", + "Branch": "master", + "OAuthToken": "not-a-secret", + "PollForSourceChanges": true + }, + "Name": "GitHub", + "OutputArtifacts": [ + { + "Name": "Artifact_Source_GitHub" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "TestPipelineBuildSynthCdkBuildProject755D4B01" + }, + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"fade37e243023bb2c0d6730c10a2c61567fbe168675a7c5e26a8810aadc7e513\"}]" + }, + "InputArtifacts": [ + { + "Name": "Artifact_Source_GitHub" + } + ], + "Name": "Synth", + "OutputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineBuildSynthCodePipelineActionRoleF7BF5926", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B" + }, + "EnvironmentVariables": { + "Fn::Join": [ + "", + [ + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"UnattachedStage\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"SingleStageManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + { + "Ref": "SecurityChangesTopic9762A9B3" + }, + "\"},{\"name\":\"NOTIFICATION_SUBJECT\",\"type\":\"PLAINTEXT\",\"value\":\"Confirm permission broadening in SingleStage\"}]" + ] + ] + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SingleStageSecurityCheck", + "Namespace": "SingleStageSecurityCheck", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleFF6E43E2", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "CustomData": "#{SingleStageSecurityCheck.MESSAGE}", + "ExternalEntityLink": "#{SingleStageSecurityCheck.LINK}" + }, + "Name": "SingleStageManualApproval", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineUnattachedStageSingleStageManualApprovalCodePipelineActionRoleF7A614C8", + "Arn" + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SingleStage-MyStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-SingleStage/PipelineSecurityStackSingleStageMyStack29962269.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SingleStage-MyStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SingleStage-MyStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "SingleStage-MyStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 4 + } + ], + "Name": "UnattachedStage" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + "EnvironmentVariables": { + "Fn::Join": [ + "", + [ + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProduction\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProductionManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + { + "Ref": "SecurityChangesTopic9762A9B3" + }, + "\"},{\"name\":\"NOTIFICATION_SUBJECT\",\"type\":\"PLAINTEXT\",\"value\":\"Confirm permission broadening in PreProduction\"}]" + ] + ] + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "PreProductionSecurityCheck", + "Namespace": "PreProductionSecurityCheck", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRole4E54C194", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "CustomData": "#{PreProductionSecurityCheck.MESSAGE}", + "ExternalEntityLink": "#{PreProductionSecurityCheck.LINK}" + }, + "Name": "PreProductionManualApproval", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelinePreProductionPreProductionManualApprovalCodePipelineActionRole81B9C4F9", + "Arn" + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + "EnvironmentVariables": { + "Fn::Join": [ + "", + [ + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProduction\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"SafeProductionManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + { + "Ref": "SecurityChangesTopic9762A9B3" + }, + "\"},{\"name\":\"NOTIFICATION_SUBJECT\",\"type\":\"PLAINTEXT\",\"value\":\"Confirm permission broadening in SafeProduction\"}]" + ] + ] + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SafeProductionSecurityCheck", + "Namespace": "SafeProductionSecurityCheck", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRole399C68A6", + "Arn" + ] + }, + "RunOrder": 5 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "CustomData": "#{SafeProductionSecurityCheck.MESSAGE}", + "ExternalEntityLink": "#{SafeProductionSecurityCheck.LINK}" + }, + "Name": "SafeProductionManualApproval", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelinePreProductionSafeProductionManualApprovalCodePipelineActionRole4F30C0D9", + "Arn" + ] + }, + "RunOrder": 6 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "PreProduction-MyStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-PreProduction/PipelineSecurityStackPreProductionMyStackDCCBB4EA.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "MyStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "PreProduction-MyStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "MyStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SafeProduction-MySafeStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-SafeProduction/PipelineSecurityStackSafeProductionMySafeStackC0D87904.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SafeProduction-MySafeStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 7 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SafeProduction-MySafeStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "SafeProduction-MySafeStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 8 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "DisableSecurityCheck-MySafeStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-DisableSecurityCheck/PipelineSecurityStackDisableSecurityCheckMySafeStack7A4F8E95.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "DisableSecurityCheck-MySafeStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 9 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "DisableSecurityCheck-MySafeStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "DisableSecurityCheck-MySafeStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 10 + } + ], + "Name": "PreProduction" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + "EnvironmentVariables": "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"NoSecurityCheck\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"EnableSecurityCheckManualApproval\"}]" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "EnableSecurityCheckSecurityCheck", + "Namespace": "EnableSecurityCheckSecurityCheck", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRole8D10AA6D", + "Arn" + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "CustomData": "#{EnableSecurityCheckSecurityCheck.MESSAGE}", + "ExternalEntityLink": "#{EnableSecurityCheckSecurityCheck.LINK}" + }, + "Name": "EnableSecurityCheckManualApproval", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineNoSecurityCheckEnableSecurityCheckManualApprovalCodePipelineActionRole27FC4015", + "Arn" + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "NoSecurityCheck-MyStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-NoSecurityCheck/PipelineSecurityStackNoSecurityCheckMyStack3484019E.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "MyStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "NoSecurityCheck-MyStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "MyStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "EnableSecurityCheck-MyStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-EnableSecurityCheck/PipelineSecurityStackEnableSecurityCheckMyStack0B9FE272.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "EnableSecurityCheck-MyStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 5 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "EnableSecurityCheck-MyStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "EnableSecurityCheck-MyStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 6 + } + ], + "Name": "NoSecurityCheck" + } + ], + "ArtifactStore": { + "EncryptionKey": { + "Id": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + }, + "Type": "KMS" + }, + "Location": { + "Ref": "TestPipelineArtifactsBucket026AF2F9" + }, + "Type": "S3" + }, + "Name": "TestPipeline", + "RestartExecutionOnUpdate": true, + "Tags": [ + { + "Key": "SECURITY_CHECK", + "Value": "ALLOW_APPROVE" + } + ] + }, + "DependsOn": [ + "TestPipelineRoleDefaultPolicyFA69BF2D", + "TestPipelineRole63C35BBD" + ] + }, + "TestPipelineBuildSynthCodePipelineActionRoleF7BF5926": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineBuildSynthCodePipelineActionRoleDefaultPolicy65DF5C76": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineBuildSynthCdkBuildProject755D4B01", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineBuildSynthCodePipelineActionRoleDefaultPolicy65DF5C76", + "Roles": [ + { + "Ref": "TestPipelineBuildSynthCodePipelineActionRoleF7BF5926" + } + ] + } + }, + "TestPipelineBuildSynthCdkBuildProjectRole4C6E5729": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineBuildSynthCdkBuildProjectRoleDefaultPolicy73DC4481": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "TestPipelineBuildSynthCdkBuildProject755D4B01" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "TestPipelineBuildSynthCdkBuildProject755D4B01" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "TestPipelineBuildSynthCdkBuildProject755D4B01" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineBuildSynthCdkBuildProjectRoleDefaultPolicy73DC4481", + "Roles": [ + { + "Ref": "TestPipelineBuildSynthCdkBuildProjectRole4C6E5729" + } + ] + } + }, + "TestPipelineBuildSynthCdkBuildProject755D4B01": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "EnvironmentVariables": [ + { + "Name": "NPM_CONFIG_UNSAFE_PERM", + "Type": "PLAINTEXT", + "Value": "true" + } + ], + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "TestPipelineBuildSynthCdkBuildProjectRole4C6E5729", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"pre_build\": {\n \"commands\": [\n \"yarn install --frozen-lockfile\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"yarn build\",\n \"npx cdk synth\"\n ]\n }\n },\n \"artifacts\": {\n \"base-directory\": \"cdk.out\",\n \"files\": \"**/*\"\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + }, + "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleFF6E43E2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleDefaultPolicyFC737D71": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleDefaultPolicyFC737D71", + "Roles": [ + { + "Ref": "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleFF6E43E2" + } + ] + } + }, + "TestPipelineUnattachedStageSingleStageManualApprovalCodePipelineActionRoleF7A614C8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRole4E54C194": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRoleDefaultPolicy10D0864F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRoleDefaultPolicy10D0864F", + "Roles": [ + { + "Ref": "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRole4E54C194" + } + ] + } + }, + "TestPipelinePreProductionPreProductionManualApprovalCodePipelineActionRole81B9C4F9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRole399C68A6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRoleDefaultPolicyB836B566": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRoleDefaultPolicyB836B566", + "Roles": [ + { + "Ref": "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRole399C68A6" + } + ] + } + }, + "TestPipelinePreProductionSafeProductionManualApprovalCodePipelineActionRole4F30C0D9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRole8D10AA6D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRoleDefaultPolicyE83A2CA1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRoleDefaultPolicyE83A2CA1", + "Roles": [ + { + "Ref": "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRole8D10AA6D" + } + ] + } + }, + "TestPipelineNoSecurityCheckEnableSecurityCheckManualApprovalCodePipelineActionRole27FC4015": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole7594919D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicyE47AE90F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codepipeline:GetPipelineState", + "codepipeline:PutApprovalResult" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/SECURITY_CHECK": "ALLOW_APPROVE" + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicyE47AE90F", + "Roles": [ + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole7594919D" + } + ] + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApprove1EE0AA81": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", + "S3Key": "6c6c8f170c2cc5c6e35d90fe172fbc17cae75777b84707d58332dee79f444404.zip" + }, + "Role": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole7594919D", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "Timeout": 300 + }, + "DependsOn": [ + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicyE47AE90F", + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole7594919D" + ] + }, + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleA54CF050": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleDefaultPolicyF2137052": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Condition": { + "ForAnyValue:StringEquals": { + "iam:ResourceTag/aws-cdk:bootstrap-role": [ + "deploy" + ] + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApprove1EE0AA81", + "Arn" + ] + } + }, + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "SecurityChangesTopic9762A9B3" + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleDefaultPolicyF2137052", + "Roles": [ + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleA54CF050" + } + ] + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleA54CF050", + "Arn" + ] + }, + "Source": { + "BuildSpec": { + "Fn::Join": [ + "", + [ + "{\n \"version\": 0.2,\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm install -g aws-cdk\",\n \"export PIPELINE_NAME=\\\"$(node -pe '`${process.env.CODEBUILD_INITIATOR}`.split(\\\"/\\\")[1]')\\\"\",\n \"payload=\\\"$(node -pe 'JSON.stringify({ \\\"PipelineName\\\": process.env.PIPELINE_NAME, \\\"StageName\\\": process.env.STAGE_NAME, \\\"ActionName\\\": process.env.ACTION_NAME })' )\\\"\",\n \"ARN=$CODEBUILD_BUILD_ARN\",\n \"REGION=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[3]')\\\"\",\n \"ACCOUNT_ID=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[4]')\\\"\",\n \"PROJECT_NAME=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[5].split(\\\"/\\\")[1]')\\\"\",\n \"PROJECT_ID=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[6]')\\\"\",\n \"export LINK=\\\"https://$REGION.console.aws.amazon.com/codesuite/codebuild/$ACCOUNT_ID/projects/$PROJECT_NAME/build/$PROJECT_NAME:$PROJECT_ID/?region=$REGION\\\"\",\n \"export PIPELINE_LINK=\\\"https://$REGION.console.aws.amazon.com/codesuite/codepipeline/pipelines/$PIPELINE_NAME/view?region=$REGION\\\"\",\n \"if cdk diff -a . --security-only --fail $STAGE_PATH/\\\\*; then aws lambda invoke --function-name ", + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApprove1EE0AA81" + }, + " --invocation-type Event --payload \\\"$payload\\\" lambda.out; export MESSAGE=\\\"No security-impacting changes detected.\\\"; else [ -z \\\"${NOTIFICATION_ARN}\\\" ] || aws sns publish --topic-arn $NOTIFICATION_ARN --subject \\\"$NOTIFICATION_SUBJECT\\\" --message \\\"An upcoming change would broaden security changes in $PIPELINE_NAME.\\nReview and approve the changes in CodePipeline to proceed with the deployment.\\n\\nReview the changes in CodeBuild:\\n\\n$LINK\\n\\nApprove the changes in CodePipeline (stage $STAGE_NAME, action $ACTION_NAME):\\n\\n$PIPELINE_LINK\\\"; export MESSAGE=\\\"Deployment would make security-impacting changes. Click the link below to inspect them, then click Approve if all changes are expected.\\\"; fi\"\n ]\n }\n },\n \"env\": {\n \"exported-variables\": [\n \"LINK\",\n \"MESSAGE\"\n ]\n }\n}" + ] + ] + }, + "Type": "NO_SOURCE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole1358574A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicy5AF69BD3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codepipeline:GetPipelineState", + "codepipeline:PutApprovalResult" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/SECURITY_CHECK": "ALLOW_APPROVE" + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicy5AF69BD3", + "Roles": [ + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole1358574A" + } + ] + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApprove249F82F9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", + "S3Key": "6c6c8f170c2cc5c6e35d90fe172fbc17cae75777b84707d58332dee79f444404.zip" + }, + "Role": { + "Fn::GetAtt": [ + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole1358574A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "Timeout": 300 + }, + "DependsOn": [ + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicy5AF69BD3", + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole1358574A" + ] + }, + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleD3505CF0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleDefaultPolicy6F6EA2A6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Condition": { + "ForAnyValue:StringEquals": { + "iam:ResourceTag/aws-cdk:bootstrap-role": [ + "deploy" + ] + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApprove249F82F9", + "Arn" + ] + } + }, + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "SecurityChangesTopic9762A9B3" + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleDefaultPolicy6F6EA2A6", + "Roles": [ + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleD3505CF0" + } + ] + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleD3505CF0", + "Arn" + ] + }, + "Source": { + "BuildSpec": { + "Fn::Join": [ + "", + [ + "{\n \"version\": 0.2,\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm install -g aws-cdk\",\n \"export PIPELINE_NAME=\\\"$(node -pe '`${process.env.CODEBUILD_INITIATOR}`.split(\\\"/\\\")[1]')\\\"\",\n \"payload=\\\"$(node -pe 'JSON.stringify({ \\\"PipelineName\\\": process.env.PIPELINE_NAME, \\\"StageName\\\": process.env.STAGE_NAME, \\\"ActionName\\\": process.env.ACTION_NAME })' )\\\"\",\n \"ARN=$CODEBUILD_BUILD_ARN\",\n \"REGION=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[3]')\\\"\",\n \"ACCOUNT_ID=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[4]')\\\"\",\n \"PROJECT_NAME=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[5].split(\\\"/\\\")[1]')\\\"\",\n \"PROJECT_ID=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[6]')\\\"\",\n \"export LINK=\\\"https://$REGION.console.aws.amazon.com/codesuite/codebuild/$ACCOUNT_ID/projects/$PROJECT_NAME/build/$PROJECT_NAME:$PROJECT_ID/?region=$REGION\\\"\",\n \"export PIPELINE_LINK=\\\"https://$REGION.console.aws.amazon.com/codesuite/codepipeline/pipelines/$PIPELINE_NAME/view?region=$REGION\\\"\",\n \"if cdk diff -a . --security-only --fail $STAGE_PATH/\\\\*; then aws lambda invoke --function-name ", + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApprove249F82F9" + }, + " --invocation-type Event --payload \\\"$payload\\\" lambda.out; export MESSAGE=\\\"No security-impacting changes detected.\\\"; else [ -z \\\"${NOTIFICATION_ARN}\\\" ] || aws sns publish --topic-arn $NOTIFICATION_ARN --subject \\\"$NOTIFICATION_SUBJECT\\\" --message \\\"An upcoming change would broaden security changes in $PIPELINE_NAME.\\nReview and approve the changes in CodePipeline to proceed with the deployment.\\n\\nReview the changes in CodeBuild:\\n\\n$LINK\\n\\nApprove the changes in CodePipeline (stage $STAGE_NAME, action $ACTION_NAME):\\n\\n$PIPELINE_LINK\\\"; export MESSAGE=\\\"Deployment would make security-impacting changes. Click the link below to inspect them, then click Approve if all changes are expected.\\\"; fi\"\n ]\n }\n },\n \"env\": {\n \"exported-variables\": [\n \"LINK\",\n \"MESSAGE\"\n ]\n }\n}" + ] + ] + }, + "Type": "NO_SOURCE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + }, + "SecurityChangesTopic9762A9B3": { + "Type": "AWS::SNS::Topic" + }, + "SecurityChangesTopictestemailcom7C32D452": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "email", + "TopicArn": { + "Ref": "SecurityChangesTopic9762A9B3" + }, + "Endpoint": "test@email.com" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-security.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.ts new file mode 100644 index 0000000000000..8b43db6087a1c --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.ts @@ -0,0 +1,110 @@ +/// !cdk-integ PipelineSecurityStack +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; +import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; +import { App, SecretValue, Stack, StackProps, Stage, StageProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cdkp from '../lib'; + +class MyStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'MyStack', { + env: props?.env, + }); + const topic = new sns.Topic(stack, 'Topic'); + topic.grantPublish(new iam.AccountPrincipal(stack.account)); + } +} + +class MySafeStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'MySafeStack', { + env: props?.env, + }); + new sns.Topic(stack, 'MySafeTopic'); + } +} + +export class TestCdkStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + // The code that defines your stack goes here + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); + + const pipeline = new cdkp.CdkPipeline(this, 'TestPipeline', { + selfMutating: false, + pipelineName: 'TestPipeline', + cloudAssemblyArtifact, + sourceAction: new codepipeline_actions.GitHubSourceAction({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.plainText('not-a-secret'), + owner: 'OWNER', + repo: 'REPO', + trigger: codepipeline_actions.GitHubTrigger.POLL, + }), + synthAction: cdkp.SimpleSynthAction.standardYarnSynth({ + sourceArtifact, + cloudAssemblyArtifact, + buildCommand: 'yarn build', + }), + }); + + const pipelineStage = pipeline.codePipeline.addStage({ + stageName: 'UnattachedStage', + }); + + const unattachedStage = new cdkp.CdkStage(this, 'UnattachedStage', { + stageName: 'UnattachedStage', + pipelineStage, + cloudAssemblyArtifact, + host: { + publishAsset: () => undefined, + stackOutputArtifact: () => undefined, + }, + }); + + const topic = new sns.Topic(this, 'SecurityChangesTopic'); + topic.addSubscription(new subscriptions.EmailSubscription('test@email.com')); + + unattachedStage.addApplication(new MyStage(this, 'SingleStage', { + env: { account: this.account, region: this.region }, + }), { confirmBroadeningPermissions: true, securityNotificationTopic: topic }); + + const stage1 = pipeline.addApplicationStage(new MyStage(this, 'PreProduction', { + env: { account: this.account, region: this.region }, + }), { confirmBroadeningPermissions: true, securityNotificationTopic: topic }); + + stage1.addApplication(new MySafeStage(this, 'SafeProduction', { + env: { account: this.account, region: this.region }, + })); + + stage1.addApplication(new MySafeStage(this, 'DisableSecurityCheck', { + env: { account: this.account, region: this.region }, + }), { confirmBroadeningPermissions: false }); + + const stage2 = pipeline.addApplicationStage(new MyStage(this, 'NoSecurityCheck', { + env: { account: this.account, region: this.region }, + })); + + stage2.addApplication(new MyStage(this, 'EnableSecurityCheck', { + env: { account: this.account, region: this.region }, + }), { confirmBroadeningPermissions: true }); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': 'true', + }, +}); +new TestCdkStack(app, 'PipelineSecurityStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +}); +app.synth(); From 349de7c2abff97e10a6e76dd6b7856ecbfd0c441 Mon Sep 17 00:00:00 2001 From: Ayato Tokubi Date: Fri, 16 Jul 2021 14:37:51 +0000 Subject: [PATCH 064/105] feat(apigatewayv2): websocket - callback url (#15227) I added property to extract websocket callback endpoint url to WebSocketStage. I added it simply like existing `url` property. It's my first time to submit PR. Please point out any mistakes. closes #14836 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-apigatewayv2/README.md | 9 ++++++++ .../aws-apigatewayv2/lib/websocket/stage.ts | 23 ++++++++++++++++++- .../test/websocket/stage.test.ts | 18 +++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 0d4d7849fd8df..944e328fc8532 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -311,6 +311,15 @@ new WebSocketStage(stack, 'mystage', { }); ``` +To retrieve a websocket URL and a callback URL: + +```ts +const webSocketURL = webSocketStage.url; +// wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath} +const callbackURL = webSocketURL.callbackUrl; +// https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath} +``` + To add any other route: ```ts diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts index a50353a79ca2d..f6bc91909dcba 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -13,6 +13,14 @@ export interface IWebSocketStage extends IStage { * The API this stage is associated to. */ readonly api: IWebSocketApi; + + /** + * The callback URL to this stage. + * You can use the callback URL to send messages to the client from the backend system. + * https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-basic-concept.html + * https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html + */ + readonly callbackUrl: string; } /** @@ -57,6 +65,10 @@ export class WebSocketStage extends StageBase implements IWebSocketStage { get url(): string { throw new Error('url is not available for imported stages.'); } + + get callbackUrl(): string { + throw new Error('callback url is not available for imported stages.'); + } } return new Import(scope, id); } @@ -86,11 +98,20 @@ export class WebSocketStage extends StageBase implements IWebSocketStage { } /** - * The URL to this stage. + * The websocket URL to this stage. */ public get url(): string { const s = Stack.of(this); const urlPath = this.stageName; return `wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } + + /** + * The callback URL to this stage. + */ + public get callbackUrl(): string { + const s = Stack.of(this); + const urlPath = this.stageName; + return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; + } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts index 4ff13cd6bb8d0..bec3e34e5d4fb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -40,5 +40,23 @@ describe('WebSocketStage', () => { // THEN expect(imported.stageName).toEqual(stage.stageName); + expect(() => imported.url).toThrow(); + expect(() => imported.callbackUrl).toThrow(); + }); + + test('callback URL', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const defaultStage = new WebSocketStage(stack, 'Stage', { + webSocketApi: api, + stageName: 'dev', + }); + + // THEN + expect(defaultStage.callbackUrl.endsWith('/dev')).toBe(true); + expect(defaultStage.callbackUrl.startsWith('https://')).toBe(true); }); }); From 2647cf300ae0f9053104e3a545e2fd94dd7249e1 Mon Sep 17 00:00:00 2001 From: Cory Hall <43035978+corymhall@users.noreply.github.com> Date: Fri, 16 Jul 2021 11:18:20 -0400 Subject: [PATCH 065/105] feat(cli): add ability to specify an external id for the deploy-role (#15604) added the ability to specify an external id for the deploy-role as part of the synthesizer config. this is similar to the existing functionality that allows specifying the external id for the image and file publishing roles In order to take advantage of this functionality you would need to customize the bootstrap template. To test this feature I customized the DeploymentActionRole in the bootstrap template to have the `AssumeRolePolicyDocument` as: ```yaml DeploymentActionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Condition: StringEquals: sts:ExternalId: "my-external-id" Effect: Allow Principal: AWS: - Fn::Join: - "" - - "arn:" - Ref: AWS::Partition - :iam::1111111111111:root Version: "2012-10-17" ... RoleName: Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region} ``` I then created a test stack that specified that external id: ```ts new CdkExternalIdTestStack(app, 'CdkExternalIdTestStack', { env: { account: '111111111111', region: 'us-east-2', }, synthesizer: new cdk.DefaultStackSynthesizer({ deployRoleExternalId: 'my-external-id', }), }); ``` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/cloud-assembly/artifact-schema.ts | 9 ++++++++- .../schema/cloud-assembly.schema.json | 4 ++++ .../schema/cloud-assembly.version.json | 2 +- .../stack-synthesizers/default-synthesizer.ts | 8 ++++++++ .../stack-synthesizers/stack-synthesizer.ts | 9 ++++++++- .../new-style-synthesis.test.ts | 20 +++++++++++++++++++ .../lib/artifacts/cloudformation-artifact.ts | 10 +++++++++- .../lib/api/cloudformation-deployments.ts | 1 + 8 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts index c4facf9fd4f19..9bf124c31c71d 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts @@ -42,6 +42,13 @@ export interface AwsCloudFormationStackProperties { */ readonly assumeRoleArn?: string; + /** + * External ID to use when assuming role for cloudformation deployments + * + * @default - No external ID + */ + readonly assumeRoleExternalId?: string; + /** * The role that is passed to CloudFormation to execute the change set * @@ -149,4 +156,4 @@ export interface NestedCloudAssemblyProperties { export type ArtifactProperties = AwsCloudFormationStackProperties | AssetManifestProperties | TreeArtifactProperties -| NestedCloudAssemblyProperties; \ No newline at end of file +| NestedCloudAssemblyProperties; diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 3c0f38f598570..171c57d064b93 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -299,6 +299,10 @@ "description": "The role that needs to be assumed to deploy the stack (Default - No role is assumed (current credentials are used))", "type": "string" }, + "assumeRoleExternalId": { + "description": "External ID to use when assuming role for cloudformation deployments (Default - No external ID)", + "type": "string" + }, "cloudFormationExecutionRoleArn": { "description": "The role that is passed to CloudFormation to execute the change set (Default - No role is passed (currently assumed role/credentials are used))", "type": "string" diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 42c883f995fd4..72764c536bcd9 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"12.0.0"} \ No newline at end of file +{"version":"13.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index 3030b687f1244..5c4072c3efc5c 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -98,6 +98,13 @@ export interface DefaultStackSynthesizerProps { */ readonly imageAssetPublishingExternalId?: string; + /** + * External ID to use when assuming role for cloudformation deployments + * + * @default - No external ID + */ + readonly deployRoleExternalId?: string; + /** * The role to assume to initiate a deployment in this environment * @@ -424,6 +431,7 @@ export class DefaultStackSynthesizer extends StackSynthesizer { const artifactId = this.writeAssetManifest(session); this.emitStackArtifact(this.stack, session, { + assumeRoleExternalId: this.props.deployRoleExternalId, assumeRoleArn: this._deployRoleArn, cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, stackTemplateAssetObjectUrl: templateManifestUrl, diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts index 1a43f9da11316..3b283eaae24ce 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/stack-synthesizer.ts @@ -86,6 +86,13 @@ export interface SynthesizeStackArtifactOptions { */ readonly assumeRoleArn?: string; + /** + * The externalID to use with the assumeRoleArn + * + * @default - No externalID is used + */ + readonly assumeRoleExternalId?: string; + /** * The role that is passed to CloudFormation to execute the change set * @@ -121,4 +128,4 @@ export interface SynthesizeStackArtifactOptions { * @default - Bootstrap stack version number looked up */ readonly bootstrapStackVersionSsmParameter?: string; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts b/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts index 93ba7be190e48..67e8fcbfaae17 100644 --- a/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts +++ b/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts @@ -273,6 +273,26 @@ nodeunitShim({ test.done(); }, + 'customize deploy role externalId'(test: Test) { + // GIVEN + const myapp = new App(); + + // WHEN + const mystack = new Stack(myapp, 'mystack', { + synthesizer: new DefaultStackSynthesizer({ + deployRoleExternalId: 'deploy-external-id', + }), + }); + + // THEN + const asm = myapp.synth(); + + const stackArtifact = asm.getStack(mystack.stackName); + expect(stackArtifact.assumeRoleExternalId).toEqual('deploy-external-id'); + + test.done(); + }, + 'synthesis with bucketPrefix'(test: Test) { // GIVEN const myapp = new App(); diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts index dba5bacb6beb0..225f256e85f5f 100644 --- a/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -61,6 +61,13 @@ export class CloudFormationStackArtifact extends CloudArtifact { */ public readonly assumeRoleArn?: string; + /** + * External ID to use when assuming role for cloudformation deployments + * + * @default - No external ID + */ + readonly assumeRoleExternalId?: string; + /** * The role that is passed to CloudFormation to execute the change set * @@ -121,6 +128,7 @@ export class CloudFormationStackArtifact extends CloudArtifact { // from the stack metadata this.tags = properties.tags ?? this.tagsFromMetadata(); this.assumeRoleArn = properties.assumeRoleArn; + this.assumeRoleExternalId = properties.assumeRoleExternalId; this.cloudFormationExecutionRoleArn = properties.cloudFormationExecutionRoleArn; this.stackTemplateAssetObjectUrl = properties.stackTemplateAssetObjectUrl; this.requiresBootstrapStackVersion = properties.requiresBootstrapStackVersion; @@ -165,4 +173,4 @@ export class CloudFormationStackArtifact extends CloudArtifact { } return ret; } -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index 84fac5cb15922..ed783e0826fd7 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -251,6 +251,7 @@ export class CloudFormationDeployments { const stackSdk = await this.sdkProvider.forEnvironment(resolvedEnvironment, mode, { assumeRoleArn: arns.assumeRoleArn, + assumeRoleExternalId: stack.assumeRoleExternalId, }); return { From 9c4e51ca1719b89bcdd9d4032f50063876fac69a Mon Sep 17 00:00:00 2001 From: haimlit Date: Sun, 18 Jul 2021 11:16:49 +0300 Subject: [PATCH 066/105] fix(appsync): update timestamp for apikey test (#15624) Fixes #15623 by updating timestamp for apikey integ test. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json | 2 +- packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json index 2d1ef3f31504f..ed0307de0e72f 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json @@ -28,7 +28,7 @@ "ApiId" ] }, - "Expires": 1626566400 + "Expires": 1658053715 }, "DependsOn": [ "ApiSchema510EECD7" diff --git a/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.ts b/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.ts index 5ddcb9abd1dbb..f2e8cb4145732 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.ts @@ -29,7 +29,7 @@ const api = new GraphqlApi(stack, 'Api', { authorizationType: AuthorizationType.API_KEY, apiKeyConfig: { // Generate a timestamp that's 365 days ahead, use atTimestamp so integ test doesn't fail - expires: Expiration.atTimestamp(1626566400000), + expires: Expiration.atTimestamp(1658053715000), }, }, }, From fc68df424aa3e8a89c21dabd15052bd479e3832c Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 18 Jul 2021 19:23:32 +0300 Subject: [PATCH 067/105] chore: some cli tests still rely on legacy behavior and fail in v2 (#15633) A similar case to #15565 in which CLI tests assume stacks can be selected by ID and then fail when they land in v2-main. To detect this upstream (on `master`), I added an environment variable that explicitly disables the legacy behavior in cx-api and use it in the CLI tests to ensure that tests are executed without legacy behavior enabled. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 7 ++++++- packages/aws-cdk/test/api/cloud-assembly.test.ts | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index f42b0ee640fc2..aeb266d888167 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -125,10 +125,15 @@ export class CloudAssembly { private selectMatchingStacks(stacks: cxapi.CloudFormationStackArtifact[], patterns: string[], extend: ExtendedStackSelection = ExtendedStackSelection.None): StackCollection { + + // cli tests use this to ensure tests do not depend on legacy behavior + // (otherwise they will fail in v2) + const disableLegacy = process.env.CXAPI_DISABLE_SELECT_BY_ID === '1'; + const matchingPattern = (pattern: string) => (stack: cxapi.CloudFormationStackArtifact) => { if (minimatch(stack.hierarchicalId, pattern)) { return true; - } else if (stack.id === pattern && semver.major(versionNumber()) < 2) { + } else if (!disableLegacy && stack.id === pattern && semver.major(versionNumber()) < 2) { warning('Selecting stack by identifier "%s". This identifier is deprecated and will be removed in v2. Please use "%s" instead.', colors.bold(stack.id), colors.bold(stack.hierarchicalId)); warning('Run "cdk ls" to see a list of all stack identifiers'); return true; diff --git a/packages/aws-cdk/test/api/cloud-assembly.test.ts b/packages/aws-cdk/test/api/cloud-assembly.test.ts index c17f108142a1e..de60d00486c17 100644 --- a/packages/aws-cdk/test/api/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/api/cloud-assembly.test.ts @@ -2,12 +2,15 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { DefaultSelection } from '../../lib/api/cxapp/cloud-assembly'; import { MockCloudExecutable } from '../util'; +// behave like v2 +process.env.CXAPI_DISABLE_SELECT_BY_ID = '1'; + test('do not throw when selecting stack without errors', async () => { // GIVEN const cxasm = await testCloudAssembly(); // WHEN - const selected = await cxasm.selectStacks( { patterns: ['withouterrors'] }, { + const selected = await cxasm.selectStacks( { patterns: ['withouterrorsNODEPATH'] }, { defaultBehavior: DefaultSelection.AllStacks, }); selected.processMetadataMessages(); @@ -100,7 +103,7 @@ test('select behavior: repeat', async () => { const cxasm = await testCloudAssembly(); // WHEN - const x = await cxasm.selectStacks({ patterns: ['withouterrors', 'withouterrors'] }, { + const x = await cxasm.selectStacks({ patterns: ['withouterrorsNODEPATH', 'withouterrorsNODEPATH'] }, { defaultBehavior: DefaultSelection.AllStacks, }); From 8668e15b8a85b369153e9d36d4eab016fc8856f4 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Sun, 18 Jul 2021 18:39:49 +0100 Subject: [PATCH 068/105] chore: migrate sns, lambda-event-sources, cloudwatch to jest (#15606) Also migrate sns and lambda-event-sources to the new 'assertions' module. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../package.json | 2 - packages/@aws-cdk/aws-cloudwatch/.gitignore | 3 +- packages/@aws-cdk/aws-cloudwatch/.npmignore | 3 +- .../@aws-cdk/aws-cloudwatch/jest.config.js | 2 + packages/@aws-cdk/aws-cloudwatch/package.json | 5 +- ...-widget.ts => alarm-status-widget.test.ts} | 13 +- .../test/{test.alarm.ts => alarm.test.ts} | 108 ++-- ...osite-alarm.ts => composite-alarm.test.ts} | 17 +- ...vironment.ts => cross-environment.test.ts} | 77 ++- .../{test.dashboard.ts => dashboard.test.ts} | 74 +-- .../test/{test.graphs.ts => graphs.test.ts} | 202 +++---- .../test/{test.layout.ts => layout.test.ts} | 59 +- ...est.metric-math.ts => metric-math.test.ts} | 200 +++--- .../test/{test.metrics.ts => metrics.test.ts} | 127 ++-- .../aws-lambda-event-sources/.gitignore | 3 +- .../aws-lambda-event-sources/.npmignore | 3 +- .../aws-lambda-event-sources/jest.config.js | 2 + .../aws-lambda-event-sources/package.json | 7 +- .../test/{test.api.ts => api.test.ts} | 65 +- .../test/{test.dynamo.ts => dynamo.test.ts} | 237 ++++---- .../test/{test.kafka.ts => kafka.test.ts} | 131 ++-- .../test/{test.kinesis.ts => kinesis.test.ts} | 103 ++-- .../test/{test.s3.ts => s3.test.ts} | 16 +- .../test/{test.sns.ts => sns.test.ts} | 35 +- .../test/{test.sqs.ts => sqs.test.ts} | 137 +++-- packages/@aws-cdk/aws-sns/.gitignore | 3 +- packages/@aws-cdk/aws-sns/.npmignore | 3 +- packages/@aws-cdk/aws-sns/jest.config.js | 10 + packages/@aws-cdk/aws-sns/package.json | 9 +- packages/@aws-cdk/aws-sns/test/sns.test.ts | 483 +++++++++++++++ ...t.subscription.ts => subscription.test.ts} | 85 ++- packages/@aws-cdk/aws-sns/test/test.sns.ts | 570 ------------------ 32 files changed, 1354 insertions(+), 1440 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudwatch/jest.config.js rename packages/@aws-cdk/aws-cloudwatch/test/{test.alarm-status-widget.ts => alarm-status-widget.test.ts} (80%) rename packages/@aws-cdk/aws-cloudwatch/test/{test.alarm.ts => alarm.test.ts} (73%) rename packages/@aws-cdk/aws-cloudwatch/test/{test.composite-alarm.ts => composite-alarm.test.ts} (89%) rename packages/@aws-cdk/aws-cloudwatch/test/{test.cross-environment.ts => cross-environment.test.ts} (57%) rename packages/@aws-cdk/aws-cloudwatch/test/{test.dashboard.ts => dashboard.test.ts} (75%) rename packages/@aws-cdk/aws-cloudwatch/test/{test.graphs.ts => graphs.test.ts} (79%) rename packages/@aws-cdk/aws-cloudwatch/test/{test.layout.ts => layout.test.ts} (66%) rename packages/@aws-cdk/aws-cloudwatch/test/{test.metric-math.ts => metric-math.test.ts} (78%) rename packages/@aws-cdk/aws-cloudwatch/test/{test.metrics.ts => metrics.test.ts} (64%) create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/jest.config.js rename packages/@aws-cdk/aws-lambda-event-sources/test/{test.api.ts => api.test.ts} (66%) rename packages/@aws-cdk/aws-lambda-event-sources/test/{test.dynamo.ts => dynamo.test.ts} (77%) rename packages/@aws-cdk/aws-lambda-event-sources/test/{test.kafka.ts => kafka.test.ts} (84%) rename packages/@aws-cdk/aws-lambda-event-sources/test/{test.kinesis.ts => kinesis.test.ts} (74%) rename packages/@aws-cdk/aws-lambda-event-sources/test/{test.s3.ts => s3.test.ts} (88%) rename packages/@aws-cdk/aws-lambda-event-sources/test/{test.sns.ts => sns.test.ts} (77%) rename packages/@aws-cdk/aws-lambda-event-sources/test/{test.sqs.ts => sqs.test.ts} (62%) create mode 100644 packages/@aws-cdk/aws-sns/jest.config.js create mode 100644 packages/@aws-cdk/aws-sns/test/sns.test.ts rename packages/@aws-cdk/aws-sns/test/{test.subscription.ts => subscription.test.ts} (76%) delete mode 100644 packages/@aws-cdk/aws-sns/test/test.sns.ts diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json index e9d7dfdf5b8fc..77e37b1fd5d0d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json @@ -72,10 +72,8 @@ "license": "Apache-2.0", "devDependencies": { "@types/jest": "^26.0.24", - "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-cloudwatch/.gitignore b/packages/@aws-cdk/aws-cloudwatch/.gitignore index 0f4bf01dd552c..9f6a9219fad75 100644 --- a/packages/@aws-cdk/aws-cloudwatch/.gitignore +++ b/packages/@aws-cdk/aws-cloudwatch/.gitignore @@ -16,4 +16,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/.npmignore b/packages/@aws-cdk/aws-cloudwatch/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-cloudwatch/.npmignore +++ b/packages/@aws-cdk/aws-cloudwatch/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/jest.config.js b/packages/@aws-cdk/aws-cloudwatch/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index c57fc64d6f8a0..246ad8a89ef37 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -55,6 +55,7 @@ }, "cdk-build": { "cloudformation": "AWS::CloudWatch", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": "true" } @@ -72,11 +73,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm-status-widget.ts b/packages/@aws-cdk/aws-cloudwatch/test/alarm-status-widget.test.ts similarity index 80% rename from packages/@aws-cdk/aws-cloudwatch/test/test.alarm-status-widget.ts rename to packages/@aws-cdk/aws-cloudwatch/test/alarm-status-widget.test.ts index 17d24cfd3793f..3c771ba47ea05 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm-status-widget.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/alarm-status-widget.test.ts @@ -1,8 +1,7 @@ import { Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { Metric, Alarm, AlarmStatusWidget } from '../lib'; -export = { - 'alarm status widget'(test: Test) { +describe('Alarm Status Widget', () => { + test('alarm status widget', () => { // GIVEN const stack = new Stack(); const metric = new Metric({ namespace: 'CDK', metricName: 'Test' }); @@ -18,7 +17,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [ + expect(stack.resolve(widget.toJson())).toEqual([ { type: 'alarm', width: 6, @@ -30,6 +29,6 @@ export = { }, ]); - test.done(); - }, -}; + + }); +}); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts similarity index 73% rename from packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts rename to packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts index fbce3d9c4ef9c..c143e8baec65b 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts @@ -1,7 +1,7 @@ -import { ABSENT, expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT } from '@aws-cdk/assert-internal'; import { Duration, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { Test } from 'nodeunit'; import { Alarm, IAlarm, IAlarmAction, Metric, MathExpression, IMetric } from '../lib'; const testMetric = new Metric({ @@ -9,9 +9,9 @@ const testMetric = new Metric({ metricName: 'Metric', }); -export = { +describe('Alarm', () => { - 'alarm does not accept a math expression with more than 10 metrics'(test: Test) { + test('alarm does not accept a math expression with more than 10 metrics', () => { const stack = new Stack(); @@ -30,7 +30,7 @@ export = { usingMetrics, }); - test.throws(() => { + expect(() => { new Alarm(stack, 'Alarm', { metric: math, @@ -38,11 +38,11 @@ export = { evaluationPeriods: 3, }); - }, /Alarms on math expressions cannot contain more than 10 individual metrics/); + }).toThrow(/Alarms on math expressions cannot contain more than 10 individual metrics/); - test.done(); - }, - 'non ec2 instance related alarm does not accept EC2 action'(test: Test) { + + }); + test('non ec2 instance related alarm does not accept EC2 action', () => { const stack = new Stack(); const alarm = new Alarm(stack, 'Alarm', { @@ -51,12 +51,12 @@ export = { evaluationPeriods: 2, }); - test.throws(() => { + expect(() => { alarm.addAlarmAction(new Ec2TestAlarmAction('arn:aws:automate:us-east-1:ec2:reboot')); - }, /EC2 alarm actions requires an EC2 Per-Instance Metric. \(.+ does not have an 'InstanceId' dimension\)/); - test.done(); - }, - 'can make simple alarm'(test: Test) { + }).toThrow(/EC2 alarm actions requires an EC2 Per-Instance Metric. \(.+ does not have an 'InstanceId' dimension\)/); + + }); + test('can make simple alarm', () => { // GIVEN const stack = new Stack(); @@ -68,7 +68,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, MetricName: 'Metric', @@ -76,12 +76,12 @@ export = { Period: 300, Statistic: 'Average', Threshold: 1000, - })); + }); - test.done(); - }, - 'override metric period in Alarm'(test: Test) { + }); + + test('override metric period in Alarm', () => { // GIVEN const stack = new Stack(); @@ -94,7 +94,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, MetricName: 'Metric', @@ -102,12 +102,12 @@ export = { Period: 600, Statistic: 'Average', Threshold: 1000, - })); + }); - test.done(); - }, - 'override statistic Alarm'(test: Test) { + }); + + test('override statistic Alarm', () => { // GIVEN const stack = new Stack(); @@ -120,7 +120,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, MetricName: 'Metric', @@ -129,12 +129,12 @@ export = { Statistic: 'Maximum', ExtendedStatistic: ABSENT, Threshold: 1000, - })); + }); + - test.done(); - }, + }); - 'can use percentile in Alarm'(test: Test) { + test('can use percentile in Alarm', () => { // GIVEN const stack = new Stack(); @@ -147,7 +147,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, MetricName: 'Metric', @@ -156,12 +156,12 @@ export = { Statistic: ABSENT, ExtendedStatistic: 'p99', Threshold: 1000, - })); + }); + - test.done(); - }, + }); - 'can set DatapointsToAlarm'(test: Test) { + test('can set DatapointsToAlarm', () => { // GIVEN const stack = new Stack(); @@ -174,7 +174,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, DatapointsToAlarm: 2, @@ -183,12 +183,12 @@ export = { Period: 300, Statistic: 'Average', Threshold: 1000, - })); + }); + - test.done(); - }, + }); - 'can add actions to alarms'(test: Test) { + test('can add actions to alarms', () => { // GIVEN const stack = new Stack(); @@ -204,16 +204,16 @@ export = { alarm.addOkAction(new TestAlarmAction('C')); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { AlarmActions: ['A'], InsufficientDataActions: ['B'], OKActions: ['C'], - })); + }); - test.done(); - }, - 'can make alarm directly from metric'(test: Test) { + }); + + test('can make alarm directly from metric', () => { // GIVEN const stack = new Stack(); @@ -226,7 +226,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 2, MetricName: 'Metric', @@ -234,12 +234,12 @@ export = { Period: 10, Statistic: 'Minimum', Threshold: 1000, - })); + }); - test.done(); - }, - 'can use percentile string to make alarm'(test: Test) { + }); + + test('can use percentile string to make alarm', () => { // GIVEN const stack = new Stack(); @@ -251,13 +251,13 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ExtendedStatistic: 'p99.9', - })); + }); - test.done(); - }, -}; + + }); +}); class TestAlarmAction implements IAlarmAction { constructor(private readonly arn: string) { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts similarity index 89% rename from packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts rename to packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts index 87625abcd6aa6..054f1b21724ee 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { Alarm, AlarmRule, AlarmState, CompositeAlarm, Metric } from '../lib'; -export = { - 'test alarm rule expression builder'(test: Test) { +describe('CompositeAlarm', () => { + test('test alarm rule expression builder', () => { const stack = new Stack(); const testMetric = new Metric({ @@ -60,7 +59,7 @@ export = { alarmRule, }); - expect(stack).to(haveResource('AWS::CloudWatch::CompositeAlarm', { + expect(stack).toHaveResource('AWS::CloudWatch::CompositeAlarm', { AlarmName: 'CompositeAlarm', AlarmRule: { 'Fn::Join': [ @@ -105,9 +104,9 @@ export = { ], ], }, - })); + }); + - test.done(); - }, + }); -}; +}); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.cross-environment.ts b/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts similarity index 57% rename from packages/@aws-cdk/aws-cloudwatch/test/test.cross-environment.ts rename to packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts index 1afb4cdca882c..959ceafab54fc 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.cross-environment.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { Alarm, GraphWidget, IWidget, Metric } from '../lib'; const a = new Metric({ namespace: 'Test', metricName: 'ACount' }); let stack1: Stack; let stack2: Stack; -export = { - 'setUp'(cb: () => void) { +describe('cross environment', () => { + beforeEach(() => { stack1 = new Stack(undefined, undefined, { env: { region: 'pluto', account: '1234' } }); stack2 = new Stack(undefined, undefined, { env: { region: 'mars', account: '5678' } }); - cb(); - }, - 'in graphs': { - 'metric attached to stack1 will not render region and account in stack1'(test: Test) { + }); + + describe('in graphs', () => { + test('metric attached to stack1 will not render region and account in stack1', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -24,14 +23,14 @@ export = { }); // THEN - graphMetricsAre(test, stack1, graph, [ + graphMetricsAre(stack1, graph, [ ['Test', 'ACount'], ]); - test.done(); - }, - 'metric attached to stack1 will render region and account in stack2'(test: Test) { + }); + + test('metric attached to stack1 will render region and account in stack2', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -40,14 +39,14 @@ export = { }); // THEN - graphMetricsAre(test, stack2, graph, [ + graphMetricsAre(stack2, graph, [ ['Test', 'ACount', { region: 'pluto', accountId: '1234' }], ]); - test.done(); - }, - 'metric with explicit account and region will render in environment agnostic stack'(test: Test) { + }); + + test('metric with explicit account and region will render in environment agnostic stack', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -56,14 +55,14 @@ export = { }); // THEN - graphMetricsAre(test, new Stack(), graph, [ + graphMetricsAre(new Stack(), graph, [ ['Test', 'ACount', { accountId: '1234', region: 'us-north-5' }], ]); - test.done(); - }, - 'metric attached to agnostic stack will not render in agnostic stack'(test: Test) { + }); + + test('metric attached to agnostic stack will not render in agnostic stack', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -72,16 +71,16 @@ export = { }); // THEN - graphMetricsAre(test, new Stack(), graph, [ + graphMetricsAre(new Stack(), graph, [ ['Test', 'ACount'], ]); - test.done(); - }, - }, - 'in alarms': { - 'metric attached to stack1 will not render region and account in stack1'(test: Test) { + }); + }); + + describe('in alarms', () => { + test('metric attached to stack1 will not render region and account in stack1', () => { // GIVEN new Alarm(stack1, 'Alarm', { threshold: 1, @@ -90,34 +89,34 @@ export = { }); // THEN - expect(stack1).to(haveResourceLike('AWS::CloudWatch::Alarm', { + expect(stack1).toHaveResourceLike('AWS::CloudWatch::Alarm', { MetricName: 'ACount', Namespace: 'Test', Period: 300, - })); + }); - test.done(); - }, - 'metric attached to stack1 will throw in stack2'(test: Test) { + }); + + test('metric attached to stack1 will throw in stack2', () => { // Cross-region/cross-account metrics are supported in Dashboards but not in Alarms // GIVEN - test.throws(() => { + expect(() => { new Alarm(stack2, 'Alarm', { threshold: 1, evaluationPeriods: 1, metric: a.attachTo(stack1), }); - }, /Cannot create an Alarm in region 'mars' based on metric 'ACount' in 'pluto'/); + }).toThrow(/Cannot create an Alarm in region 'mars' based on metric 'ACount' in 'pluto'/); - test.done(); - }, - }, -}; -function graphMetricsAre(test: Test, stack: Stack, w: IWidget, metrics: any[]) { - test.deepEqual(stack.resolve(w.toJson()), [{ + }); + }); +}); + +function graphMetricsAre(stack: Stack, w: IWidget, metrics: any[]) { + expect(stack.resolve(w.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.dashboard.ts b/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts similarity index 75% rename from packages/@aws-cdk/aws-cloudwatch/test/test.dashboard.ts rename to packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts index 4249f8675d1be..5501a47ba5c3a 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.dashboard.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts @@ -1,10 +1,10 @@ -import { expect, haveResource, isSuperObject } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { isSuperObject } from '@aws-cdk/assert-internal'; import { App, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { Dashboard, GraphWidget, PeriodOverride, TextWidget } from '../lib'; -export = { - 'widgets in different adds are laid out underneath each other'(test: Test) { +describe('Dashboard', () => { + test('widgets in different adds are laid out underneath each other', () => { // GIVEN const stack = new Stack(); const dashboard = new Dashboard(stack, 'Dash'); @@ -27,16 +27,16 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Dashboard', thatHasWidgets([ + expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', thatHasWidgets([ { type: 'text', width: 10, height: 2, x: 0, y: 0, properties: { markdown: 'first' } }, { type: 'text', width: 1, height: 4, x: 0, y: 2, properties: { markdown: 'second' } }, { type: 'text', width: 4, height: 1, x: 0, y: 6, properties: { markdown: 'third' } }, - ]))); + ])); - test.done(); - }, - 'widgets in same add are laid out next to each other'(test: Test) { + }); + + test('widgets in same add are laid out next to each other', () => { // GIVEN const stack = new Stack(); const dashboard = new Dashboard(stack, 'Dash'); @@ -61,16 +61,16 @@ export = { ); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Dashboard', thatHasWidgets([ + expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', thatHasWidgets([ { type: 'text', width: 10, height: 2, x: 0, y: 0, properties: { markdown: 'first' } }, { type: 'text', width: 1, height: 4, x: 10, y: 0, properties: { markdown: 'second' } }, { type: 'text', width: 4, height: 1, x: 11, y: 0, properties: { markdown: 'third' } }, - ]))); + ])); + - test.done(); - }, + }); - 'tokens in widgets are retained'(test: Test) { + test('tokens in widgets are retained', () => { // GIVEN const stack = new Stack(); const dashboard = new Dashboard(stack, 'Dash'); @@ -81,7 +81,7 @@ export = { ); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Dashboard', { + expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', { DashboardBody: { 'Fn::Join': ['', [ '{"widgets":[{"type":"metric","width":1,"height":1,"x":0,"y":0,"properties":{"view":"timeSeries","region":"', @@ -89,12 +89,12 @@ export = { '","yAxis":{}}}]}', ]], }, - })); + }); - test.done(); - }, - 'dashboard body includes non-widget fields'(test: Test) { + }); + + test('dashboard body includes non-widget fields', () => { // GIVEN const stack = new Stack(); const dashboard = new Dashboard(stack, 'Dash', @@ -110,7 +110,7 @@ export = { ); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Dashboard', { + expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', { DashboardBody: { 'Fn::Join': ['', [ '{"start":"-9H","end":"2018-12-17T06:00:00.000Z","periodOverride":"inherit",\ @@ -119,12 +119,12 @@ export = { '","yAxis":{}}}]}', ]], }, - })); + }); - test.done(); - }, - 'DashboardName is set when provided'(test: Test) { + }); + + test('DashboardName is set when provided', () => { // GIVEN const app = new App(); const stack = new Stack(app, 'MyStack'); @@ -135,14 +135,14 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Dashboard', { + expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', { DashboardName: 'MyCustomDashboardName', - })); + }); - test.done(); - }, - 'DashboardName is not generated if not provided'(test: Test) { + }); + + test('DashboardName is not generated if not provided', () => { // GIVEN const app = new App(); const stack = new Stack(app, 'MyStack'); @@ -151,12 +151,12 @@ export = { new Dashboard(stack, 'MyDashboard'); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Dashboard', {})); + expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', {}); + - test.done(); - }, + }); - 'throws if DashboardName is not valid'(test: Test) { + test('throws if DashboardName is not valid', () => { // GIVEN const app = new App(); const stack = new Stack(app, 'MyStack'); @@ -169,11 +169,11 @@ export = { }; // THEN - test.throws(() => toThrow(), /field dashboardName contains invalid characters/); + expect(() => toThrow()).toThrow(/field dashboardName contains invalid characters/); + - test.done(); - }, -}; + }); +}); /** * Returns a property predicate that checks that the given Dashboard has the indicated widgets diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts b/packages/@aws-cdk/aws-cloudwatch/test/graphs.test.ts similarity index 79% rename from packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts rename to packages/@aws-cdk/aws-cloudwatch/test/graphs.test.ts index e5cc11781393d..0c1adfb58b1f8 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.graphs.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/graphs.test.ts @@ -1,9 +1,8 @@ import { Duration, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { Alarm, AlarmWidget, Color, GraphWidget, GraphWidgetView, LegendPosition, LogQueryWidget, Metric, Shading, SingleValueWidget, LogQueryVisualizationType } from '../lib'; -export = { - 'add stacked property to graphs'(test: Test) { +describe('Graphs', () => { + test('add stacked property to graphs', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -12,7 +11,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -25,10 +24,10 @@ export = { }, }]); - test.done(); - }, - 'add metrics to graphs on either axis'(test: Test) { + }); + + test('add metrics to graphs on either axis', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -42,7 +41,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -58,10 +57,10 @@ export = { }, }]); - test.done(); - }, - 'add metrics to graphs on either axis lazily'(test: Test) { + }); + + test('add metrics to graphs on either axis lazily', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -71,7 +70,7 @@ export = { widget.addRightMetric(new Metric({ namespace: 'CDK', metricName: 'Tast' })); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -87,10 +86,10 @@ export = { }, }]); - test.done(); - }, - 'label and color are respected in constructor'(test: Test) { + }); + + test('label and color are respected in constructor', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -98,7 +97,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -112,10 +111,10 @@ export = { }, }]); - test.done(); - }, - 'bar view'(test: Test) { + }); + + test('bar view', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -124,7 +123,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -136,10 +135,10 @@ export = { }, }]); - test.done(); - }, - 'singlevalue widget'(test: Test) { + }); + + test('singlevalue widget', () => { // GIVEN const stack = new Stack(); const metric = new Metric({ namespace: 'CDK', metricName: 'Test' }); @@ -150,7 +149,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 3, @@ -163,10 +162,10 @@ export = { }, }]); - test.done(); - }, - 'query result widget'(test: Test) { + }); + + test('query result widget', () => { // GIVEN const stack = new Stack(); const logGroup = { logGroupName: 'my-log-group' }; @@ -181,7 +180,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'log', width: 6, height: 6, @@ -192,10 +191,10 @@ export = { }, }]); - test.done(); - }, - 'query result widget - bar'(test: Test) { + }); + + test('query result widget - bar', () => { // GIVEN const stack = new Stack(); const logGroup = { logGroupName: 'my-log-group' }; @@ -211,7 +210,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'log', width: 6, height: 6, @@ -222,10 +221,10 @@ export = { }, }]); - test.done(); - }, - 'query result widget - pie'(test: Test) { + }); + + test('query result widget - pie', () => { // GIVEN const stack = new Stack(); const logGroup = { logGroupName: 'my-log-group' }; @@ -241,7 +240,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'log', width: 6, height: 6, @@ -252,10 +251,10 @@ export = { }, }]); - test.done(); - }, - 'query result widget - line'(test: Test) { + }); + + test('query result widget - line', () => { // GIVEN const stack = new Stack(); const logGroup = { logGroupName: 'my-log-group' } ; @@ -271,7 +270,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'log', width: 6, height: 6, @@ -283,10 +282,10 @@ export = { }, }]); - test.done(); - }, - 'query result widget - stackedarea'(test: Test) { + }); + + test('query result widget - stackedarea', () => { // GIVEN const stack = new Stack(); const logGroup = { logGroupName: 'my-log-group' }; @@ -302,7 +301,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'log', width: 6, height: 6, @@ -314,10 +313,10 @@ export = { }, }]); - test.done(); - }, - 'alarm widget'(test: Test) { + }); + + test('alarm widget', () => { // GIVEN const stack = new Stack(); @@ -332,7 +331,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -346,10 +345,10 @@ export = { }, }]); - test.done(); - }, - 'add annotations to graph'(test: Test) { + }); + + test('add annotations to graph', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -366,7 +365,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -390,10 +389,10 @@ export = { }, }]); - test.done(); - }, - 'convert alarm to annotation'(test: Test) { + }); + + test('convert alarm to annotation', () => { // GIVEN const stack = new Stack(); @@ -412,7 +411,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -433,10 +432,10 @@ export = { }, }]); - test.done(); - }, - 'add yAxis to graph'(test: Test) { + }); + + test('add yAxis to graph', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -459,7 +458,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -478,10 +477,10 @@ export = { }, }]); - test.done(); - }, - 'specify liveData property on graph'(test: Test) { + }); + + test('specify liveData property on graph', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -493,7 +492,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -509,10 +508,10 @@ export = { }, }]); - test.done(); - }, - 'can use imported alarm with graph'(test: Test) { + }); + + test('can use imported alarm with graph', () => { // GIVEN const stack = new Stack(); const alarm = Alarm.fromAlarmArn(stack, 'Alarm', 'arn:aws:cloudwatch:region:account-id:alarm:alarm-name'); @@ -525,10 +524,10 @@ export = { // THEN: Compiles - test.done(); - }, - 'add setPeriodToTimeRange to singleValueWidget'(test: Test) { + }); + + test('add setPeriodToTimeRange to singleValueWidget', () => { // GIVEN const stack = new Stack(); const metric = new Metric({ namespace: 'CDK', metricName: 'Test' }); @@ -540,7 +539,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 3, @@ -554,10 +553,10 @@ export = { }, }]); - test.done(); - }, - 'add singleValueFullPrecision to singleValueWidget'(test: Test) { + }); + + test('add singleValueFullPrecision to singleValueWidget', () => { // GIVEN const stack = new Stack(); const metric = new Metric({ namespace: 'CDK', metricName: 'Test' }); @@ -569,7 +568,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 3, @@ -583,10 +582,10 @@ export = { }, }]); - test.done(); - }, - 'allows overriding custom values of dashboard widgets'(test: Test) { + }); + + test('allows overriding custom values of dashboard widgets', () => { class HiddenMetric extends Metric { public toMetricConfig() { const ret = super.toMetricConfig(); @@ -603,16 +602,13 @@ export = { ], }); - // test.ok(widget.toJson()[0].properties.metrics[0].visible === false); - test.deepEqual( - stack.resolve(widget.toJson())[0].properties.metrics[0], - ['CDK', 'Test', { visible: false }], - ); + expect(stack.resolve(widget.toJson())[0].properties.metrics[0]) + .toEqual(['CDK', 'Test', { visible: false }]); + - test.done(); - }, + }); - 'GraphColor is correctly converted into the correct hexcode'(test: Test) { + test('GraphColor is correctly converted into the correct hexcode', () => { // GIVEN const stack = new Stack(); const metric = new Metric({ namespace: 'CDK', metricName: 'Test' }); @@ -627,12 +623,12 @@ export = { ], }); - test.deepEqual(stack.resolve(widget.toJson())[0].properties.metrics[0], ['CDK', 'Test', { color: '#1f77b4' }]); - test.deepEqual(stack.resolve(widget.toJson())[0].properties.annotations.horizontal[0], { yAxis: 'left', value: 100, color: '#d62728' }); - test.done(); - }, + expect(stack.resolve(widget.toJson())[0].properties.metrics[0]).toEqual(['CDK', 'Test', { color: '#1f77b4' }]); + expect(stack.resolve(widget.toJson())[0].properties.annotations.horizontal[0]).toEqual({ yAxis: 'left', value: 100, color: '#d62728' }); + + }); - 'legend position is respected in constructor'(test: Test) { + test('legend position is respected in constructor', () => { // WHEN const stack = new Stack(); const widget = new GraphWidget({ @@ -641,7 +637,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -658,10 +654,10 @@ export = { }, }]); - test.done(); - }, - 'add setPeriodToTimeRange to GraphWidget'(test: Test) { + }); + + test('add setPeriodToTimeRange to GraphWidget', () => { // GIVEN const stack = new Stack(); const widget = new GraphWidget({ @@ -671,7 +667,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -686,10 +682,10 @@ export = { }, }]); - test.done(); - }, - 'GraphWidget supports stat and period'(test: Test) { + }); + + test('GraphWidget supports stat and period', () => { // GIVEN const stack = new Stack(); const widget = new GraphWidget({ @@ -699,7 +695,7 @@ export = { }); // THEN - test.deepEqual(stack.resolve(widget.toJson()), [{ + expect(stack.resolve(widget.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -715,6 +711,6 @@ export = { }, }]); - test.done(); - }, -}; + + }); +}); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.layout.ts b/packages/@aws-cdk/aws-cloudwatch/test/layout.test.ts similarity index 66% rename from packages/@aws-cdk/aws-cloudwatch/test/test.layout.ts rename to packages/@aws-cdk/aws-cloudwatch/test/layout.test.ts index 3a8cbb35ed1a3..f7062189ddd04 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.layout.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/layout.test.ts @@ -1,8 +1,7 @@ -import { Test } from 'nodeunit'; import { Column, IWidget, Row, Spacer, TextWidget } from '../lib'; -export = { - 'row has the height of the tallest element'(test: Test) { +describe('Layout', () => { + test('row has the height of the tallest element', () => { // WHEN const row = new Row( new Spacer({ width: 10, height: 1 }), @@ -10,24 +9,24 @@ export = { ); // THEN - test.equal(4, row.height); - test.equal(20, row.width); + expect(4).toEqual(row.height); + expect(20).toEqual(row.width); - test.done(); - }, - 'spacer has default height and width'(test: Test) { + }); + + test('spacer has default height and width', () => { // WHEN const spacer = new Spacer(); // THEN - test.equal(1, spacer.height); - test.equal(1, spacer.width); + expect(1).toEqual(spacer.height); + expect(1).toEqual(spacer.width); + - test.done(); - }, + }); - 'column has the width of the tallest element'(test: Test) { + test('column has the width of the tallest element', () => { // WHEN const col = new Column( new Spacer({ width: 1, height: 1 }), @@ -35,13 +34,13 @@ export = { ); // THEN - test.equal(4, col.width); - test.equal(5, col.height); + expect(4).toEqual(col.width); + expect(5).toEqual(col.height); + - test.done(); - }, + }); - 'row wraps to width of 24, taking tallest widget into account while wrapping'(test: Test) { + test('row wraps to width of 24, taking tallest widget into account while wrapping', () => { // Try the tall box in all positions for (const heights of [[4, 1, 1], [1, 4, 1], [1, 1, 4]]) { // GIVEN @@ -57,13 +56,13 @@ export = { row.position(1000, 1000); // Check that we correctly offset all inner widgets // THEN - test.equal(21, row.width); - test.equal(5, row.height); + expect(21).toEqual(row.width); + expect(5).toEqual(row.height); function assertWidgetPos(x: number, y: number, w: IWidget) { const json = w.toJson()[0]; - test.equal(x, json.x); - test.equal(y, json.y); + expect(x).toEqual(json.x); + expect(y).toEqual(json.y); } assertWidgetPos(1000, 1000, widgets[0]); @@ -72,10 +71,10 @@ export = { assertWidgetPos(1000, 1004, widgets[3]); } - test.done(); - }, - 'row can fit exactly 3 8-wide widgets without wrapping'(test: Test) { + }); + + test('row can fit exactly 3 8-wide widgets without wrapping', () => { // Try the tall box in all positions for (const heights of [[4, 1, 1], [1, 4, 1], [1, 1, 4]]) { // WHEN @@ -86,10 +85,10 @@ export = { ); // THEN - test.equal(24, row.width); - test.equal(4, row.height); + expect(24).toEqual(row.width); + expect(4).toEqual(row.height); } - test.done(); - }, -}; + + }); +}); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts b/packages/@aws-cdk/aws-cloudwatch/test/metric-math.test.ts similarity index 78% rename from packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts rename to packages/@aws-cdk/aws-cloudwatch/test/metric-math.test.ts index dcf65649236aa..b9379bbdc360c 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/metric-math.test.ts @@ -1,6 +1,5 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Duration, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { Alarm, GraphWidget, IWidget, MathExpression, Metric } from '../lib'; const a = new Metric({ namespace: 'Test', metricName: 'ACount' }); @@ -9,14 +8,13 @@ const c = new Metric({ namespace: 'Test', metricName: 'CCount' }); const b99 = new Metric({ namespace: 'Test', metricName: 'BCount', statistic: 'p99' }); let stack: Stack; -export = { - 'setUp'(cb: () => void) { +describe('Metric Math', () => { + beforeEach(() => { stack = new Stack(); - cb(); - }, + }); - 'can not use invalid variable names in MathExpression'(test: Test) { - test.throws(() => { + test('can not use invalid variable names in MathExpression', () => { + expect(() => { new MathExpression({ expression: 'HAPPY + JOY', usingMetrics: { @@ -24,14 +22,14 @@ export = { JOY: b, }, }); - }, /Invalid variable names in expression/); + }).toThrow(/Invalid variable names in expression/); - test.done(); - }, - 'cannot reuse variable names in nested MathExpressions'(test: Test) { + }); + + test('cannot reuse variable names in nested MathExpressions', () => { // WHEN - test.throws(() => { + expect(() => { new MathExpression({ expression: 'a + e', usingMetrics: { @@ -42,37 +40,37 @@ export = { }), }, }); - }, /The ID 'a' used for two metrics in the expression: 'BCount' and 'ACount'. Rename one/); + }).toThrow(/The ID 'a' used for two metrics in the expression: 'BCount' and 'ACount'. Rename one/); + - test.done(); - }, + }); - 'can not use invalid period in MathExpression'(test: Test) { - test.throws(() => { + test('can not use invalid period in MathExpression', () => { + expect(() => { new MathExpression({ expression: 'a+b', usingMetrics: { a, b }, period: Duration.seconds(20), }); - }, /'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received 20/); + }).toThrow(/'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received 20/); + - test.done(); - }, + }); - 'MathExpression optimization: "with" with the same period returns the same object'(test: Test) { + test('MathExpression optimization: "with" with the same period returns the same object', () => { const m = new MathExpression({ expression: 'SUM(METRICS())', usingMetrics: {}, period: Duration.minutes(10) }); // Note: object equality, NOT deep equality on purpose - test.equals(m.with({}), m); - test.equals(m.with({ period: Duration.minutes(10) }), m); + expect(m.with({})).toEqual(m); + expect(m.with({ period: Duration.minutes(10) })).toEqual(m); - test.notEqual(m.with({ period: Duration.minutes(5) }), m); + expect(m.with({ period: Duration.minutes(5) })).not.toEqual(m); - test.done(); - }, - 'in graphs': { - 'MathExpressions can be added to a graph'(test: Test) { + }); + + describe('in graphs', () => { + test('MathExpressions can be added to a graph', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -84,16 +82,16 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ [{ expression: 'a + b', label: 'a + b' }], ['Test', 'ACount', { visible: false, id: 'a' }], ['Test', 'BCount', { visible: false, id: 'b' }], ]); - test.done(); - }, - 'can nest MathExpressions in a graph'(test: Test) { + }); + + test('can nest MathExpressions in a graph', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -111,7 +109,7 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ [{ label: 'a + e', expression: 'a + e' }], ['Test', 'ACount', { visible: false, id: 'a' }], [{ expression: 'b + c', visible: false, id: 'e' }], @@ -119,10 +117,10 @@ export = { ['Test', 'CCount', { visible: false, id: 'c' }], ]); - test.done(); - }, - 'can add the same metric under different ids'(test: Test) { + }); + + test('can add the same metric under different ids', () => { const graph = new GraphWidget({ left: [ new MathExpression({ @@ -138,7 +136,7 @@ export = { ], }); - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ [{ label: 'a + e', expression: 'a + e' }], ['Test', 'ACount', { visible: false, id: 'a' }], [{ expression: 'b + c', visible: false, id: 'e' }], @@ -146,10 +144,10 @@ export = { ['Test', 'CCount', { visible: false, id: 'c' }], ]); - test.done(); - }, - 'can reuse identifiers in MathExpressions if metrics are the same'(test: Test) { + }); + + test('can reuse identifiers in MathExpressions if metrics are the same', () => { const graph = new GraphWidget({ left: [ new MathExpression({ @@ -166,17 +164,17 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ [{ label: 'a + e', expression: 'a + e' }], ['Test', 'ACount', { visible: false, id: 'a' }], [{ expression: 'a + c', visible: false, id: 'e' }], ['Test', 'CCount', { visible: false, id: 'c' }], ]); - test.done(); - }, - 'MathExpression and its constituent metrics can both be added to a graph'(test: Test) { + }); + + test('MathExpression and its constituent metrics can both be added to a graph', () => { const graph = new GraphWidget({ left: [ a, @@ -188,15 +186,15 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ ['Test', 'ACount', { id: 'a' }], [{ label: 'a + b', expression: 'a + b' }], ['Test', 'BCount', { visible: false, id: 'b' }], ]); - test.done(); - }, - 'MathExpression controls period of metrics directly used in it'(test: Test) { + }); + + test('MathExpression controls period of metrics directly used in it', () => { // Check that if we add A with { period: 10s } to a mathexpression of period 5m // then two metric lines are added for A, one at 10s and one at 5m const graph = new GraphWidget({ @@ -210,16 +208,16 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ ['Test', 'ACount', { period: 10 }], [{ label: 'a + b', expression: 'a + b' }], ['Test', 'ACount', { visible: false, id: 'a' }], ['Test', 'BCount', { visible: false, id: 'b' }], ]); - test.done(); - }, - 'top level period in a MathExpression is respected in its metrics'(test: Test) { + }); + + test('top level period in a MathExpression is respected in its metrics', () => { const graph = new GraphWidget({ left: [ a, @@ -232,16 +230,16 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ ['Test', 'ACount'], [{ label: 'a + b', expression: 'a + b', period: 60 }], ['Test', 'ACount', { visible: false, id: 'a', period: 60 }], ['Test', 'BCount', { visible: false, id: 'b', period: 60 }], ]); - test.done(); - }, - 'MathExpression controls period of metrics transitively used in it'(test: Test) { + }); + + test('MathExpression controls period of metrics transitively used in it', () => { // Same as the previous test, but recursively const graph = new GraphWidget({ @@ -261,16 +259,16 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ [{ expression: 'a + e', label: 'a + e' }], ['Test', 'ACount', { visible: false, id: 'a' }], [{ expression: 'a + b', visible: false, id: 'e' }], ['Test', 'BCount', { visible: false, id: 'b' }], ]); - test.done(); - }, - 'can use percentiles in expression metrics in graphs'(test: Test) { + }); + + test('can use percentiles in expression metrics in graphs', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -282,16 +280,16 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ [{ expression: 'a + b99', label: 'a + b99' }], ['Test', 'ACount', { visible: false, id: 'a' }], ['Test', 'BCount', { visible: false, id: 'b99', stat: 'p99' }], ]); - test.done(); - }, - 'can reuse the same metric between left and right axes'(test: Test) { + }); + + test('can reuse the same metric between left and right axes', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -309,16 +307,16 @@ export = { }); // THEN - graphMetricsAre(test, graph, [ + graphMetricsAre(graph, [ [{ label: 'a + 1', expression: 'a + 1' }], ['Test', 'ACount', { visible: false, id: 'a' }], [{ label: 'a + 2', expression: 'a + 2', yAxis: 'right' }], ]); - test.done(); - }, - 'detect name conflicts between left and right axes'(test: Test) { + }); + + test('detect name conflicts between left and right axes', () => { // GIVEN const graph = new GraphWidget({ left: [ @@ -336,16 +334,16 @@ export = { }); // THEN - test.throws(() => { - graphMetricsAre(test, graph, []); - }, /Cannot have two different metrics share the same id \('m1'\)/); + expect(() => { + graphMetricsAre(graph, []); + }).toThrow(/Cannot have two different metrics share the same id \('m1'\)/); - test.done(); - }, - }, - 'in alarms': { - 'MathExpressions can be used for an alarm'(test: Test) { + }); + }); + + describe('in alarms', () => { + test('MathExpressions can be used for an alarm', () => { // GIVEN new Alarm(stack, 'Alarm', { threshold: 1, @@ -389,10 +387,10 @@ export = { ]); - test.done(); - }, - 'can nest MathExpressions in an alarm'(test: Test) { + }); + + test('can nest MathExpressions in an alarm', () => { // GIVEN new Alarm(stack, 'Alarm', { threshold: 1, @@ -458,10 +456,10 @@ export = { }, ]); - test.done(); - }, - 'MathExpression controls period of metrics transitively used in it with alarms'(test: Test) { + }); + + test('MathExpression controls period of metrics transitively used in it with alarms', () => { // GIVEN new Alarm(stack, 'Alarm', { threshold: 1, @@ -529,10 +527,10 @@ export = { }, ]); - test.done(); - }, - 'MathExpression without inner metrics emits its own period'(test: Test) { + }); + + test('MathExpression without inner metrics emits its own period', () => { // WHEN new Alarm(stack, 'Alarm', { threshold: 1, @@ -552,10 +550,10 @@ export = { }, ]); - test.done(); - }, - 'annotation for a mathexpression alarm is calculated based upon constituent metrics'(test: Test) { + }); + + test('annotation for a mathexpression alarm is calculated based upon constituent metrics', () => { // GIVEN const alarm = new Alarm(stack, 'Alarm', { threshold: 1, @@ -571,12 +569,12 @@ export = { const alarmLabel = alarm.toAnnotation().label; // THEN - test.equals(alarmLabel, 'a + b >= 1 for 1 datapoints within 10 minutes'); + expect(alarmLabel).toEqual('a + b >= 1 for 1 datapoints within 10 minutes'); - test.done(); - }, - 'can use percentiles in expression metrics in alarms'(test: Test) { + }); + + test('can use percentiles in expression metrics in alarms', () => { // GIVEN new Alarm(stack, 'Alarm', { threshold: 1, @@ -619,13 +617,13 @@ export = { }, ]); - test.done(); - }, - }, -}; -function graphMetricsAre(test: Test, w: IWidget, metrics: any[]) { - test.deepEqual(stack.resolve(w.toJson()), [{ + }); + }); +}); + +function graphMetricsAre(w: IWidget, metrics: any[]) { + expect(stack.resolve(w.toJson())).toEqual([{ type: 'metric', width: 6, height: 6, @@ -640,7 +638,7 @@ function graphMetricsAre(test: Test, w: IWidget, metrics: any[]) { } function alarmMetricsAre(metrics: any[]) { - expect(stack).to(haveResourceLike('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResourceLike('AWS::CloudWatch::Alarm', { Metrics: metrics, - })); + }); } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts b/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts similarity index 64% rename from packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts rename to packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts index dc55f7a2bb0e7..dc958860b6a4c 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { Alarm, Metric } from '../lib'; -export = { - 'metric grant'(test: Test) { +describe('Metrics', () => { + test('metric grant', () => { // GIVEN const stack = new cdk.Stack(); const role = new iam.Role(stack, 'SomeRole', { @@ -16,7 +15,7 @@ export = { Metric.grantPutMetricData(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -27,33 +26,33 @@ export = { }, ], }, - })); + }); + - test.done(); - }, + }); - 'can not use invalid period in Metric'(test: Test) { - test.throws(() => { + test('can not use invalid period in Metric', () => { + expect(() => { new Metric({ namespace: 'Test', metricName: 'ACount', period: cdk.Duration.seconds(20) }); - }, /'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received 20/); + }).toThrow(/'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received 20/); - test.done(); - }, - 'Metric optimization: "with" with the same period returns the same object'(test: Test) { + }); + + test('Metric optimization: "with" with the same period returns the same object', () => { const m = new Metric({ namespace: 'Test', metricName: 'Metric', period: cdk.Duration.minutes(10) }); // Note: object equality, NOT deep equality on purpose - test.equals(m.with({}), m); - test.equals(m.with({ period: cdk.Duration.minutes(10) }), m); + expect(m.with({})).toEqual(m); + expect(m.with({ period: cdk.Duration.minutes(10) })).toEqual(m); + + expect(m.with({ period: cdk.Duration.minutes(5) })).not.toEqual(m); - test.notEqual(m.with({ period: cdk.Duration.minutes(5) }), m); - test.done(); - }, + }); - 'cannot use null dimension value'(test: Test) { - test.throws(() => { + test('cannot use null dimension value', () => { + expect(() => { new Metric({ namespace: 'Test', metricName: 'ACount', @@ -62,13 +61,13 @@ export = { DimensionWithNull: null, }, }); - }, /Dimension value of 'null' is invalid/); + }).toThrow(/Dimension value of 'null' is invalid/); - test.done(); - }, - 'cannot use undefined dimension value'(test: Test) { - test.throws(() => { + }); + + test('cannot use undefined dimension value', () => { + expect(() => { new Metric({ namespace: 'Test', metricName: 'ACount', @@ -77,16 +76,16 @@ export = { DimensionWithUndefined: undefined, }, }); - }, /Dimension value of 'undefined' is invalid/); + }).toThrow(/Dimension value of 'undefined' is invalid/); + - test.done(); - }, + }); - 'cannot use long dimension values'(test: Test) { + test('cannot use long dimension values', () => { const arr = new Array(256); const invalidDimensionValue = arr.fill('A', 0).join(''); - test.throws(() => { + expect(() => { new Metric({ namespace: 'Test', metricName: 'ACount', @@ -95,16 +94,16 @@ export = { DimensionWithLongValue: invalidDimensionValue, }, }); - }, `Dimension value must be at least 1 and no more than 255 characters; received ${invalidDimensionValue}`); + }).toThrow(`Dimension value must be at least 1 and no more than 255 characters; received ${invalidDimensionValue}`); + - test.done(); - }, + }); - 'cannot use long dimension values in dimensionsMap'(test: Test) { + test('cannot use long dimension values in dimensionsMap', () => { const arr = new Array(256); const invalidDimensionValue = arr.fill('A', 0).join(''); - test.throws(() => { + expect(() => { new Metric({ namespace: 'Test', metricName: 'ACount', @@ -113,13 +112,13 @@ export = { DimensionWithLongValue: invalidDimensionValue, }, }); - }, `Dimension value must be at least 1 and no more than 255 characters; received ${invalidDimensionValue}`); + }).toThrow(`Dimension value must be at least 1 and no more than 255 characters; received ${invalidDimensionValue}`); - test.done(); - }, - 'throws error when there are more than 10 dimensions'(test: Test) { - test.throws(() => { + }); + + test('throws error when there are more than 10 dimensions', () => { + expect(() => { new Metric({ namespace: 'Test', metricName: 'ACount', @@ -138,13 +137,13 @@ export = { dimensionK: 'value11', }, } ); - }, /The maximum number of dimensions is 10, received 11/); + }).toThrow(/The maximum number of dimensions is 10, received 11/); + - test.done(); - }, + }); - 'throws error when there are more than 10 dimensions in dimensionsMap'(test: Test) { - test.throws(() => { + test('throws error when there are more than 10 dimensions in dimensionsMap', () => { + expect(() => { new Metric({ namespace: 'Test', metricName: 'ACount', @@ -163,12 +162,12 @@ export = { dimensionK: 'value11', }, } ); - }, /The maximum number of dimensions is 10, received 11/); + }).toThrow(/The maximum number of dimensions is 10, received 11/); - test.done(); - }, - 'can create metric with dimensionsMap property'(test: Test) { + }); + + test('can create metric with dimensionsMap property', () => { const stack = new cdk.Stack(); const metric = new Metric({ namespace: 'Test', @@ -185,11 +184,11 @@ export = { evaluationPeriods: 1, }); - test.deepEqual(metric.dimensions, { + expect(metric.dimensions).toEqual({ dimensionA: 'value1', dimensionB: 'value2', }); - expect(stack).to(haveResourceLike('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResourceLike('AWS::CloudWatch::Alarm', { Namespace: 'Test', MetricName: 'Metric', Dimensions: [ @@ -204,12 +203,12 @@ export = { ], Threshold: 10, EvaluationPeriods: 1, - })); + }); - test.done(); - }, - '"with" with a different dimensions property'(test: Test) { + }); + + test('"with" with a different dimensions property', () => { const dims = { dimensionA: 'value1', }; @@ -225,20 +224,20 @@ export = { dimensionB: 'value2', }; - test.deepEqual(metric.with({ + expect(metric.with({ dimensionsMap: newDims, - }).dimensions, newDims); + }).dimensions).toEqual(newDims); + - test.done(); - }, + }); - 'metric accepts a variety of statistics'(test: Test) { + test('metric accepts a variety of statistics', () => { new Metric({ namespace: 'Test', metricName: 'Metric', statistic: 'myCustomStatistic', }); - test.done(); - }, -}; + + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/.gitignore b/packages/@aws-cdk/aws-lambda-event-sources/.gitignore index d0a956699806b..be330198b9888 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/.gitignore +++ b/packages/@aws-cdk/aws-lambda-event-sources/.gitignore @@ -16,4 +16,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/.npmignore b/packages/@aws-cdk/aws-lambda-event-sources/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/.npmignore +++ b/packages/@aws-cdk/aws-lambda-event-sources/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-event-sources/jest.config.js b/packages/@aws-cdk/aws-lambda-event-sources/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index 8a1897a9fc7a5..0db8f4d09f9a6 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -64,12 +64,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "@aws-cdk/assertions": "0.0.0" }, "dependencies": { "@aws-cdk/aws-apigateway": "0.0.0", @@ -129,6 +129,7 @@ }, "maturity": "stable", "cdk-build": { + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true } diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.api.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/api.test.ts similarity index 66% rename from packages/@aws-cdk/aws-lambda-event-sources/test/test.api.ts rename to packages/@aws-cdk/aws-lambda-event-sources/test/api.test.ts index b4aea8a304a16..b0b2e9fb5ec71 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.api.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/api.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as events from '../lib'; -export = { - 'minimal example'(test: Test) { +describe('ApiEventSource', () => { + test('minimal example', () => { // GIVEN const stack = new cdk.Stack(); const handler = new lambda.Function(stack, 'MyFunc', { @@ -18,20 +17,20 @@ export = { handler.addEventSource(new events.ApiEventSource('get', '/foo')); // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'foo', ParentId: { 'Fn::GetAtt': ['MyFuncApiEventSourceA7A86A4FFB3F557C', 'RootResourceId'] }, - })); + }); - expect(stack).to(haveResource('AWS::ApiGateway::Method', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: { Ref: 'MyFuncApiEventSourceA7A86A4FfooCA6F87E4' }, - })); + }); - test.done(); - }, - 'disjoint routes'(test: Test) { + }); + + test('disjoint routes', () => { // GIVEN const stack = new cdk.Stack(); const handler = new lambda.Function(stack, 'MyFunc', { @@ -45,30 +44,30 @@ export = { handler.addEventSource(new events.ApiEventSource('post', '/bar')); // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'foo', ParentId: { 'Fn::GetAtt': ['MyFuncApiEventSourceA7A86A4FFB3F557C', 'RootResourceId'] }, - })); + }); - expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'bar', ParentId: { 'Fn::GetAtt': ['MyFuncApiEventSourceA7A86A4FFB3F557C', 'RootResourceId'] }, - })); + }); - expect(stack).to(haveResource('AWS::ApiGateway::Method', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: { Ref: 'MyFuncApiEventSourceA7A86A4FfooCA6F87E4' }, - })); + }); - expect(stack).to(haveResource('AWS::ApiGateway::Method', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST', ResourceId: { Ref: 'MyFuncApiEventSourceA7A86A4FbarDFB0F21B' }, - })); + }); - test.done(); - }, - 'tree of routes'(test: Test) { + }); + + test('tree of routes', () => { // GIVEN const stack = new cdk.Stack(); const handler = new lambda.Function(stack, 'MyFunc', { @@ -83,26 +82,26 @@ export = { handler.addEventSource(new events.ApiEventSource('post', '/foo/bar/zoo')); // THEN - expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'foo', ParentId: { 'Fn::GetAtt': ['MyFuncApiEventSourceA7A86A4FFB3F557C', 'RootResourceId'] }, - })); + }); - expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Resource', { PathPart: 'bar', ParentId: { Ref: 'MyFuncApiEventSourceA7A86A4FfooCA6F87E4' }, - })); + }); - expect(stack).to(haveResource('AWS::ApiGateway::Method', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'GET', ResourceId: { Ref: 'MyFuncApiEventSourceA7A86A4FfooCA6F87E4' }, - })); + }); - expect(stack).to(haveResource('AWS::ApiGateway::Method', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', { HttpMethod: 'POST', ResourceId: { Ref: 'MyFuncApiEventSourceA7A86A4Ffoobar028FFFDE' }, - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.dynamo.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/dynamo.test.ts similarity index 77% rename from packages/@aws-cdk/aws-lambda-event-sources/test/test.dynamo.ts rename to packages/@aws-cdk/aws-lambda-event-sources/test/dynamo.test.ts index d79a8f17020ed..8ea8da3c026f4 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.dynamo.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/dynamo.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import * as dynamodb from '@aws-cdk/aws-dynamodb'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as sources from '../lib'; import { TestFunction } from './test-function'; /* eslint-disable quote-props */ -export = { - 'sufficiently complex example'(test: Test) { +describe('DynamoEventSource', () => { + test('sufficiently complex example', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -28,7 +27,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -57,9 +56,9 @@ export = { 'Roles': [{ 'Ref': 'FnServiceRoleB9001A96', }], - })); + }); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -71,12 +70,12 @@ export = { }, 'BatchSize': 100, 'StartingPosition': 'TRIM_HORIZON', - })); + }); + - test.done(); - }, + }); - 'specific tumblingWindow'(test: Test) { + test('specific tumblingWindow', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -96,14 +95,14 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { TumblingWindowInSeconds: 60, - })); + }); + - test.done(); - }, + }); - 'specific batch size'(test: Test) { + test('specific batch size', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -122,7 +121,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -134,12 +133,12 @@ export = { }, 'BatchSize': 50, 'StartingPosition': 'LATEST', - })); + }); + - test.done(); - }, + }); - 'fails if streaming not enabled on table'(test: Test) { + test('fails if streaming not enabled on table', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -151,15 +150,15 @@ export = { }); // WHEN - test.throws(() => fn.addEventSource(new sources.DynamoEventSource(table, { + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { batchSize: 50, startingPosition: lambda.StartingPosition.LATEST, - })), /DynamoDB Streams must be enabled on the table Default\/T/); + }))).toThrow(/DynamoDB Streams must be enabled on the table Default\/T/); + - test.done(); - }, + }); - 'fails if batch size < 1'(test: Test) { + test('fails if batch size < 1', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -172,15 +171,15 @@ export = { }); // WHEN - test.throws(() => fn.addEventSource(new sources.DynamoEventSource(table, { + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { batchSize: 0, startingPosition: lambda.StartingPosition.LATEST, - })), /Maximum batch size must be between 1 and 1000 inclusive \(given 0\)/); + }))).toThrow(/Maximum batch size must be between 1 and 1000 inclusive \(given 0\)/); - test.done(); - }, - 'fails if batch size > 1000'(test: Test) { + }); + + test('fails if batch size > 1000', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -193,15 +192,15 @@ export = { }); // WHEN - test.throws(() => fn.addEventSource(new sources.DynamoEventSource(table, { + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { batchSize: 1001, startingPosition: lambda.StartingPosition.LATEST, - })), /Maximum batch size must be between 1 and 1000 inclusive \(given 1001\)/); + }))).toThrow(/Maximum batch size must be between 1 and 1000 inclusive \(given 1001\)/); + - test.done(); - }, + }); - 'specific maxBatchingWindow'(test: Test) { + test('specific maxBatchingWindow', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -220,7 +219,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -232,12 +231,12 @@ export = { }, 'MaximumBatchingWindowInSeconds': 120, 'StartingPosition': 'LATEST', - })); + }); + - test.done(); - }, + }); - 'throws if maxBatchingWindow > 300 seconds'(test: Test) { + test('throws if maxBatchingWindow > 300 seconds', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -250,16 +249,16 @@ export = { }); // THEN - test.throws(() => + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { maxBatchingWindow: cdk.Duration.seconds(301), startingPosition: lambda.StartingPosition.LATEST, - })), /maxBatchingWindow cannot be over 300 seconds/); + }))).toThrow(/maxBatchingWindow cannot be over 300 seconds/); + - test.done(); - }, + }); - 'contains eventSourceMappingId after lambda binding'(test: Test) { + test('contains eventSourceMappingId after lambda binding', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -278,11 +277,11 @@ export = { fn.addEventSource(eventSource); // THEN - test.ok(eventSource.eventSourceMappingId); - test.done(); - }, + expect(eventSource.eventSourceMappingId).toBeDefined(); - 'eventSourceMappingId throws error before binding to lambda'(test: Test) { + }); + + test('eventSourceMappingId throws error before binding to lambda', () => { // GIVEN const stack = new cdk.Stack(); const table = new dynamodb.Table(stack, 'T', { @@ -297,11 +296,11 @@ export = { }); // WHEN/THEN - test.throws(() => eventSource.eventSourceMappingId, /DynamoEventSource is not yet bound to an event source mapping/); - test.done(); - }, + expect(() => eventSource.eventSourceMappingId).toThrow(/DynamoEventSource is not yet bound to an event source mapping/); + + }); - 'specific retryAttempts'(test: Test) { + test('specific retryAttempts', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -320,7 +319,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -332,12 +331,12 @@ export = { }, 'MaximumRetryAttempts': 10, 'StartingPosition': 'LATEST', - })); + }); + - test.done(); - }, + }); - 'fails if retryAttempts < 0'(test: Test) { + test('fails if retryAttempts < 0', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -350,16 +349,16 @@ export = { }); // THEN - test.throws(() => + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { retryAttempts: -1, startingPosition: lambda.StartingPosition.LATEST, - })), /retryAttempts must be between 0 and 10000 inclusive, got -1/); + }))).toThrow(/retryAttempts must be between 0 and 10000 inclusive, got -1/); + - test.done(); - }, + }); - 'fails if retryAttempts > 10000'(test: Test) { + test('fails if retryAttempts > 10000', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -372,16 +371,16 @@ export = { }); // THEN - test.throws(() => + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { retryAttempts: 10001, startingPosition: lambda.StartingPosition.LATEST, - })), /retryAttempts must be between 0 and 10000 inclusive, got 10001/); + }))).toThrow(/retryAttempts must be between 0 and 10000 inclusive, got 10001/); - test.done(); - }, - 'specific bisectBatchOnFunctionError'(test: Test) { + }); + + test('specific bisectBatchOnFunctionError', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -400,7 +399,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -412,12 +411,12 @@ export = { }, 'BisectBatchOnFunctionError': true, 'StartingPosition': 'LATEST', - })); + }); - test.done(); - }, - 'specific parallelizationFactor'(test: Test) { + }); + + test('specific parallelizationFactor', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -436,7 +435,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -448,12 +447,12 @@ export = { }, 'ParallelizationFactor': 5, 'StartingPosition': 'LATEST', - })); + }); - test.done(); - }, - 'fails if parallelizationFactor < 1'(test: Test) { + }); + + test('fails if parallelizationFactor < 1', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -466,16 +465,16 @@ export = { }); // THEN - test.throws(() => + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { parallelizationFactor: 0, startingPosition: lambda.StartingPosition.LATEST, - })), /parallelizationFactor must be between 1 and 10 inclusive, got 0/); + }))).toThrow(/parallelizationFactor must be between 1 and 10 inclusive, got 0/); + - test.done(); - }, + }); - 'fails if parallelizationFactor > 10'(test: Test) { + test('fails if parallelizationFactor > 10', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -488,16 +487,16 @@ export = { }); // THEN - test.throws(() => + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { parallelizationFactor: 11, startingPosition: lambda.StartingPosition.LATEST, - })), /parallelizationFactor must be between 1 and 10 inclusive, got 11/); + }))).toThrow(/parallelizationFactor must be between 1 and 10 inclusive, got 11/); + - test.done(); - }, + }); - 'specific maxRecordAge'(test: Test) { + test('specific maxRecordAge', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -516,7 +515,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -528,12 +527,12 @@ export = { }, 'MaximumRecordAgeInSeconds': 100, 'StartingPosition': 'LATEST', - })); + }); + - test.done(); - }, + }); - 'fails if maxRecordAge < 60 seconds'(test: Test) { + test('fails if maxRecordAge < 60 seconds', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -546,16 +545,16 @@ export = { }); // THEN - test.throws(() => + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { maxRecordAge: cdk.Duration.seconds(59), startingPosition: lambda.StartingPosition.LATEST, - })), /maxRecordAge must be between 60 seconds and 7 days inclusive/); + }))).toThrow(/maxRecordAge must be between 60 seconds and 7 days inclusive/); - test.done(); - }, - 'fails if maxRecordAge > 7 days'(test: Test) { + }); + + test('fails if maxRecordAge > 7 days', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -568,16 +567,16 @@ export = { }); // THEN - test.throws(() => + expect(() => fn.addEventSource(new sources.DynamoEventSource(table, { maxRecordAge: cdk.Duration.seconds(604801), startingPosition: lambda.StartingPosition.LATEST, - })), /maxRecordAge must be between 60 seconds and 7 days inclusive/); + }))).toThrow(/maxRecordAge must be between 60 seconds and 7 days inclusive/); + - test.done(); - }, + }); - 'specific destinationConfig'(test: Test) { + test('specific destinationConfig', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -597,7 +596,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -619,12 +618,12 @@ export = { }, }, 'StartingPosition': 'LATEST', - })); + }); + - test.done(); - }, + }); - 'specific functionResponseTypes'(test: Test) { + test('specific functionResponseTypes', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -643,7 +642,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'TD925BC7E', @@ -655,12 +654,12 @@ export = { }, 'StartingPosition': 'LATEST', 'FunctionResponseTypes': ['ReportBatchItemFailures'], - })); + }); + - test.done(); - }, + }); - 'event source disabled'(test: Test) { + test('event source disabled', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -679,10 +678,10 @@ export = { })); //THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'Enabled': false, - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts similarity index 84% rename from packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts rename to packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts index 615f3bcbc7d51..1b973242f2f3c 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/kafka.test.ts @@ -1,15 +1,14 @@ -import { arrayWith, expect, haveResource } from '@aws-cdk/assert-internal'; +import { TemplateAssertions, Match } from '@aws-cdk/assertions'; import { SecurityGroup, SubnetType, Vpc } from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; import { Secret } from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as sources from '../lib'; import { TestFunction } from './test-function'; -export = { - 'msk': { - 'default'(test: Test) { +describe('KafkaEventSource', () => { + describe('msk', () => { + test('default', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -25,7 +24,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -46,9 +45,9 @@ export = { Ref: 'FnServiceRoleB9001A96', }, ], - })); + }); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { EventSourceArn: clusterArn, FunctionName: { Ref: 'Fn9270CBC0', @@ -58,11 +57,11 @@ export = { Topics: [ kafkaTopic, ], - })); + }); - test.done(); - }, - 'with secret'(test: Test) { + + }); + test('with secret', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -80,7 +79,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -111,9 +110,9 @@ export = { Ref: 'FnServiceRoleB9001A96', }, ], - })); + }); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { EventSourceArn: clusterArn, FunctionName: { Ref: 'Fn9270CBC0', @@ -131,14 +130,14 @@ export = { }, }, ], - })); + }); + - test.done(); - }, - }, + }); + }); - 'self-managed kafka': { - 'default'(test: Test) { + describe('self-managed kafka', () => { + test('default', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -156,7 +155,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -178,9 +177,9 @@ export = { Ref: 'FnServiceRoleB9001A96', }, ], - })); + }); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { FunctionName: { Ref: 'Fn9270CBC0', }, @@ -202,30 +201,30 @@ export = { }, }, ], - })); + }); + - test.done(); - }, - 'without vpc, secret must be set'(test: Test) { + }); + test('without vpc, secret must be set', () => { const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const kafkaTopic = 'some-topic'; const bootstrapServers = ['kafka-broker:9092']; - test.throws(() => { + expect(() => { fn.addEventSource(new sources.SelfManagedKafkaEventSource( { bootstrapServers: bootstrapServers, topic: kafkaTopic, startingPosition: lambda.StartingPosition.TRIM_HORIZON, })); - }, /secret must be set/); + }).toThrow(/secret must be set/); - test.done(); - }, - VPC: { - 'correctly rendered in the stack'(test: Test) { + }); + + describe('vpc', () => { + test('correctly rendered in the stack', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -246,8 +245,8 @@ export = { })); // THEN - expect(stack).notTo(haveResource('AWS::IAM::Policy')); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 0); + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { FunctionName: { Ref: 'Fn9270CBC0', }, @@ -279,11 +278,11 @@ export = { }, }, ], - })); + }); + - test.done(); - }, - 'with secret'(test: Test) { + }); + test('with secret', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -306,7 +305,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -328,9 +327,9 @@ export = { Ref: 'FnServiceRoleB9001A96', }, ], - })); + }); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { FunctionName: { Ref: 'Fn9270CBC0', }, @@ -368,11 +367,11 @@ export = { }, }, ], - })); + }); + - test.done(); - }, - 'setting vpc requires vpcSubnets to be set'(test: Test) { + }); + test('setting vpc requires vpcSubnets to be set', () => { const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const kafkaTopic = 'some-topic'; @@ -380,7 +379,7 @@ export = { const bootstrapServers = ['kafka-broker:9092']; const vpc = new Vpc(stack, 'Vpc'); - test.throws(() => { + expect(() => { fn.addEventSource(new sources.SelfManagedKafkaEventSource( { bootstrapServers: bootstrapServers, @@ -391,12 +390,12 @@ export = { securityGroup: SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-0123456789'), })); - }, /vpcSubnets must be set/); + }).toThrow(/vpcSubnets must be set/); - test.done(); - }, - 'setting vpc requires securityGroup to be set'(test: Test) { + }); + + test('setting vpc requires securityGroup to be set', () => { const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const kafkaTopic = 'some-topic'; @@ -404,7 +403,7 @@ export = { const bootstrapServers = ['kafka-broker:9092']; const vpc = new Vpc(stack, 'Vpc'); - test.throws(() => { + expect(() => { fn.addEventSource(new sources.SelfManagedKafkaEventSource( { bootstrapServers: bootstrapServers, @@ -414,13 +413,13 @@ export = { vpc: vpc, vpcSubnets: { subnetType: SubnetType.PRIVATE }, })); - }, /securityGroup must be set/); + }).toThrow(/securityGroup must be set/); + - test.done(); - }, - }, + }); + }); - 'using SCRAM-SHA-256'(test: Test) { + test('using SCRAM-SHA-256', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -443,19 +442,19 @@ export = { authenticationMethod: sources.AuthenticationMethod.SASL_SCRAM_256_AUTH, })); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { - SourceAccessConfigurations: arrayWith( + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { + SourceAccessConfigurations: Match.arrayWith([ { Type: 'SASL_SCRAM_256_AUTH', URI: { Ref: 'SecretA720EF05', }, }, - ), - })); + ]), + }); + - test.done(); - }, - }, + }); + }); -} +}); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.kinesis.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/kinesis.test.ts similarity index 74% rename from packages/@aws-cdk/aws-lambda-event-sources/test/test.kinesis.ts rename to packages/@aws-cdk/aws-lambda-event-sources/test/kinesis.test.ts index dc3ff6e6637d1..b8217be186f0f 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.kinesis.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/kinesis.test.ts @@ -1,15 +1,14 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import * as kinesis from '@aws-cdk/aws-kinesis'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as sources from '../lib'; import { TestFunction } from './test-function'; /* eslint-disable quote-props */ -export = { - 'sufficiently complex example'(test: Test) { +describe('KinesisEventSource', () => { + test('sufficiently complex example', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -21,7 +20,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -57,9 +56,9 @@ export = { 'Roles': [{ 'Ref': 'FnServiceRoleB9001A96', }], - })); + }); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'S509448A1', @@ -71,12 +70,12 @@ export = { }, 'BatchSize': 100, 'StartingPosition': 'TRIM_HORIZON', - })); + }); + - test.done(); - }, + }); - 'specific tumblingWindowInSeconds'(test: Test) { + test('specific tumblingWindowInSeconds', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -90,7 +89,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'S509448A1', @@ -103,12 +102,12 @@ export = { 'BatchSize': 50, 'StartingPosition': 'LATEST', 'TumblingWindowInSeconds': 60, - })); + }); + - test.done(); - }, + }); - 'specific batch size'(test: Test) { + test('specific batch size', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -121,7 +120,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'S509448A1', @@ -133,42 +132,42 @@ export = { }, 'BatchSize': 50, 'StartingPosition': 'LATEST', - })); + }); + - test.done(); - }, + }); - 'fails if batch size < 1'(test: Test) { + test('fails if batch size < 1', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const stream = new kinesis.Stream(stack, 'S'); // WHEN - test.throws(() => fn.addEventSource(new sources.KinesisEventSource(stream, { + expect(() => fn.addEventSource(new sources.KinesisEventSource(stream, { batchSize: 0, startingPosition: lambda.StartingPosition.LATEST, - })), /Maximum batch size must be between 1 and 10000 inclusive \(given 0\)/); + }))).toThrow(/Maximum batch size must be between 1 and 10000 inclusive \(given 0\)/); - test.done(); - }, - 'fails if batch size > 10000'(test: Test) { + }); + + test('fails if batch size > 10000', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const stream = new kinesis.Stream(stack, 'S'); // WHEN - test.throws(() => fn.addEventSource(new sources.KinesisEventSource(stream, { + expect(() => fn.addEventSource(new sources.KinesisEventSource(stream, { batchSize: 10001, startingPosition: lambda.StartingPosition.LATEST, - })), /Maximum batch size must be between 1 and 10000 inclusive \(given 10001\)/); + }))).toThrow(/Maximum batch size must be between 1 and 10000 inclusive \(given 10001\)/); + - test.done(); - }, + }); - 'accepts if batch size is a token'(test: Test) { + test('accepts if batch size is a token', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -180,10 +179,10 @@ export = { startingPosition: lambda.StartingPosition.LATEST, })); - test.done(); - }, - 'specific maxBatchingWindow'(test: Test) { + }); + + test('specific maxBatchingWindow', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -196,7 +195,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'S509448A1', @@ -208,12 +207,12 @@ export = { }, 'MaximumBatchingWindowInSeconds': 120, 'StartingPosition': 'LATEST', - })); + }); + - test.done(); - }, + }); - 'contains eventSourceMappingId after lambda binding'(test: Test) { + test('contains eventSourceMappingId after lambda binding', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -226,11 +225,11 @@ export = { fn.addEventSource(eventSource); // THEN - test.ok(eventSource.eventSourceMappingId); - test.done(); - }, + expect(eventSource.eventSourceMappingId).toBeDefined(); - 'eventSourceMappingId throws error before binding to lambda'(test: Test) { + }); + + test('eventSourceMappingId throws error before binding to lambda', () => { // GIVEN const stack = new cdk.Stack(); const stream = new kinesis.Stream(stack, 'S'); @@ -239,11 +238,11 @@ export = { }); // WHEN/THEN - test.throws(() => eventSource.eventSourceMappingId, /KinesisEventSource is not yet bound to an event source mapping/); - test.done(); - }, + expect(() => eventSource.eventSourceMappingId).toThrow(/KinesisEventSource is not yet bound to an event source mapping/); + + }); - 'event source disabled'(test: Test) { + test('event source disabled', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -257,9 +256,9 @@ export = { fn.addEventSource(eventSource); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'Enabled': false, - })); - test.done(); - }, -}; + }); + + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.s3.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/s3.test.ts similarity index 88% rename from packages/@aws-cdk/aws-lambda-event-sources/test/test.s3.ts rename to packages/@aws-cdk/aws-lambda-event-sources/test/s3.test.ts index f4ab1b3f75d5b..5c77a130fa23f 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.s3.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/s3.test.ts @@ -1,14 +1,13 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as sources from '../lib'; import { TestFunction } from './test-function'; /* eslint-disable quote-props */ -export = { - 'sufficiently complex example'(test: Test) { +describe('S3EventSource', () => { + test('sufficiently complex example', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -24,7 +23,7 @@ export = { })); // THEN - expect(stack).to(haveResource('Custom::S3BucketNotifications', { + TemplateAssertions.fromStack(stack).hasResourceProperties('Custom::S3BucketNotifications', { 'NotificationConfiguration': { 'LambdaFunctionConfigurations': [ { @@ -79,7 +78,6 @@ export = { }, ], }, - })); - test.done(); - }, -}; + }); + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/sns.test.ts similarity index 77% rename from packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts rename to packages/@aws-cdk/aws-lambda-event-sources/test/sns.test.ts index bc7204d056491..498172c2a321e 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.sns.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/sns.test.ts @@ -1,15 +1,14 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as sources from '../lib'; import { TestFunction } from './test-function'; /* eslint-disable quote-props */ -export = { - 'sufficiently complex example'(test: Test) { +describe('SNSEventSource', () => { + test('sufficiently complex example', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -19,7 +18,7 @@ export = { fn.addEventSource(new sources.SnsEventSource(topic)); // THEN - expect(stack).to(haveResource('AWS::Lambda::Permission', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { 'Action': 'lambda:InvokeFunction', 'FunctionName': { 'Fn::GetAtt': [ @@ -31,9 +30,9 @@ export = { 'SourceArn': { 'Ref': 'TD925BC7E', }, - })); + }); - expect(stack).to(haveResource('AWS::SNS::Subscription', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { 'Endpoint': { 'Fn::GetAtt': [ 'Fn9270CBC0', @@ -44,12 +43,12 @@ export = { 'TopicArn': { 'Ref': 'TD925BC7E', }, - })); + }); - test.done(); - }, - 'props are passed to subscription'(test: Test) { + }); + + test('props are passed to subscription', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -68,7 +67,7 @@ export = { fn.addEventSource(new sources.SnsEventSource(topic, props)); // THEN - expect(stack).to(haveResource('AWS::Lambda::Permission', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { 'Action': 'lambda:InvokeFunction', 'FunctionName': { 'Fn::GetAtt': [ @@ -80,9 +79,9 @@ export = { 'SourceArn': { 'Ref': 'TD925BC7E', }, - })); + }); - expect(stack).to(haveResource('AWS::SNS::Subscription', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { 'Endpoint': { 'Fn::GetAtt': [ 'Fn9270CBC0', @@ -107,8 +106,8 @@ export = { ], }, }, - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.sqs.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/sqs.test.ts similarity index 62% rename from packages/@aws-cdk/aws-lambda-event-sources/test/test.sqs.ts rename to packages/@aws-cdk/aws-lambda-event-sources/test/sqs.test.ts index 0840546569c4e..b6b12c3b17a63 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/sqs.test.ts @@ -1,14 +1,13 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as sources from '../lib'; import { TestFunction } from './test-function'; /* eslint-disable quote-props */ -export = { - 'defaults'(test: Test) { +describe('SQSEventSource', () => { + test('defaults', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -18,7 +17,7 @@ export = { fn.addEventSource(new sources.SqsEventSource(q)); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -40,9 +39,9 @@ export = { ], 'Version': '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'Q63C6E3AB', @@ -52,12 +51,12 @@ export = { 'FunctionName': { 'Ref': 'Fn9270CBC0', }, - })); + }); - test.done(); - }, - 'specific batch size'(test: Test) { + }); + + test('specific batch size', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -69,7 +68,7 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'EventSourceArn': { 'Fn::GetAtt': [ 'Q63C6E3AB', @@ -80,12 +79,12 @@ export = { 'Ref': 'Fn9270CBC0', }, 'BatchSize': 5, - })); + }); - test.done(); - }, - 'unresolved batch size'(test: Test) { + }); + + test('unresolved batch size', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -102,42 +101,42 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'BatchSize': 500, - })); + }); + - test.done(); - }, + }); - 'fails if batch size is < 1'(test: Test) { + test('fails if batch size is < 1', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const q = new sqs.Queue(stack, 'Q'); // WHEN/THEN - test.throws(() => fn.addEventSource(new sources.SqsEventSource(q, { + expect(() => fn.addEventSource(new sources.SqsEventSource(q, { batchSize: 0, - })), /Maximum batch size must be between 1 and 10 inclusive \(given 0\) when batching window is not specified\./); + }))).toThrow(/Maximum batch size must be between 1 and 10 inclusive \(given 0\) when batching window is not specified\./); - test.done(); - }, - 'fails if batch size is > 10'(test: Test) { + }); + + test('fails if batch size is > 10', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const q = new sqs.Queue(stack, 'Q'); // WHEN/THEN - test.throws(() => fn.addEventSource(new sources.SqsEventSource(q, { + expect(() => fn.addEventSource(new sources.SqsEventSource(q, { batchSize: 11, - })), /Maximum batch size must be between 1 and 10 inclusive \(given 11\) when batching window is not specified\./); + }))).toThrow(/Maximum batch size must be between 1 and 10 inclusive \(given 11\) when batching window is not specified\./); + - test.done(); - }, + }); - 'batch size is > 10 and batch window is defined'(test: Test) { + test('batch size is > 10 and batch window is defined', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -150,30 +149,30 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'BatchSize': 1000, 'MaximumBatchingWindowInSeconds': 300, - })); + }); + - test.done(); - }, + }); - 'fails if batch size is > 10000 and batch window is defined'(test: Test) { + test('fails if batch size is > 10000 and batch window is defined', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const q = new sqs.Queue(stack, 'Q'); // WHEN/THEN - test.throws(() => fn.addEventSource(new sources.SqsEventSource(q, { + expect(() => fn.addEventSource(new sources.SqsEventSource(q, { batchSize: 11000, maxBatchingWindow: cdk.Duration.minutes(5), - })), /Maximum batch size must be between 1 and 10000 inclusive/i); + }))).toThrow(/Maximum batch size must be between 1 and 10000 inclusive/i); - test.done(); - }, - 'specific batch window'(test: Test) { + }); + + test('specific batch window', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -185,14 +184,14 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'MaximumBatchingWindowInSeconds': 300, - })); + }); + - test.done(); - }, + }); - 'fails if batch window defined for FIFO queue'(test: Test) { + test('fails if batch window defined for FIFO queue', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -201,28 +200,28 @@ export = { }); // WHEN/THEN - test.throws(() => fn.addEventSource(new sources.SqsEventSource(q, { + expect(() => fn.addEventSource(new sources.SqsEventSource(q, { maxBatchingWindow: cdk.Duration.minutes(5), - })), /Batching window is not supported for FIFO queues/); + }))).toThrow(/Batching window is not supported for FIFO queues/); + - test.done(); - }, + }); - 'fails if batch window is > 5'(test: Test) { + test('fails if batch window is > 5', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); const q = new sqs.Queue(stack, 'Q'); // WHEN/THEN - test.throws(() => fn.addEventSource(new sources.SqsEventSource(q, { + expect(() => fn.addEventSource(new sources.SqsEventSource(q, { maxBatchingWindow: cdk.Duration.minutes(7), - })), /Maximum batching window must be 300 seconds or less/i); + }))).toThrow(/Maximum batching window must be 300 seconds or less/i); - test.done(); - }, - 'contains eventSourceMappingId after lambda binding'(test: Test) { + }); + + test('contains eventSourceMappingId after lambda binding', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -233,22 +232,22 @@ export = { fn.addEventSource(eventSource); // THEN - test.ok(eventSource.eventSourceMappingId); - test.done(); - }, + expect(eventSource.eventSourceMappingId).toBeDefined(); + + }); - 'eventSourceMappingId throws error before binding to lambda'(test: Test) { + test('eventSourceMappingId throws error before binding to lambda', () => { // GIVEN const stack = new cdk.Stack(); const q = new sqs.Queue(stack, 'Q'); const eventSource = new sources.SqsEventSource(q); // WHEN/THEN - test.throws(() => eventSource.eventSourceMappingId, /SqsEventSource is not yet bound to an event source mapping/); - test.done(); - }, + expect(() => eventSource.eventSourceMappingId).toThrow(/SqsEventSource is not yet bound to an event source mapping/); + + }); - 'event source disabled'(test: Test) { + test('event source disabled', () => { // GIVEN const stack = new cdk.Stack(); const fn = new TestFunction(stack, 'Fn'); @@ -260,10 +259,10 @@ export = { })); // THEN - expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', { 'Enabled': false, - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-sns/.gitignore b/packages/@aws-cdk/aws-sns/.gitignore index 86fc837df8fca..a82230b5888d0 100644 --- a/packages/@aws-cdk/aws-sns/.gitignore +++ b/packages/@aws-cdk/aws-sns/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns/.npmignore b/packages/@aws-cdk/aws-sns/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-sns/.npmignore +++ b/packages/@aws-cdk/aws-sns/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns/jest.config.js b/packages/@aws-cdk/aws-sns/jest.config.js new file mode 100644 index 0000000000000..1f611abd50de4 --- /dev/null +++ b/packages/@aws-cdk/aws-sns/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + branches: 70, + statements: 80, + }, + }, +}; diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index 9ec8371fc3a6a..c7734d512cdfd 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -58,6 +58,7 @@ }, "cdk-build": { "cloudformation": "AWS::SNS", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true } @@ -76,13 +77,13 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.32", + "@aws-cdk/assertions": "0.0.0", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", - "pkglint": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "jest": "^26.6.3", + "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/aws-codestarnotifications": "0.0.0", diff --git a/packages/@aws-cdk/aws-sns/test/sns.test.ts b/packages/@aws-cdk/aws-sns/test/sns.test.ts new file mode 100644 index 0000000000000..b07fdce78dab0 --- /dev/null +++ b/packages/@aws-cdk/aws-sns/test/sns.test.ts @@ -0,0 +1,483 @@ +import { TemplateAssertions } from '@aws-cdk/assertions'; +import * as notifications from '@aws-cdk/aws-codestarnotifications'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import * as sns from '../lib'; + +/* eslint-disable quote-props */ + +describe('Topic', () => { + describe('topic tests', () => { + test('all defaults', () => { + const stack = new cdk.Stack(); + new sns.Topic(stack, 'MyTopic'); + + TemplateAssertions.fromStack(stack).resourceCountIs('AWS::SNS::Topic', 1); + + }); + + test('specify topicName', () => { + const stack = new cdk.Stack(); + + new sns.Topic(stack, 'MyTopic', { + topicName: 'topicName', + }); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'TopicName': 'topicName', + }); + + + }); + + test('specify displayName', () => { + const stack = new cdk.Stack(); + + new sns.Topic(stack, 'MyTopic', { + displayName: 'displayName', + }); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'DisplayName': 'displayName', + }); + + + }); + + test('specify kmsMasterKey', () => { + const stack = new cdk.Stack(); + const key = new kms.Key(stack, 'CustomKey'); + + new sns.Topic(stack, 'MyTopic', { + masterKey: key, + }); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'KmsMasterKeyId': { 'Fn::GetAtt': ['CustomKey1E6D0D07', 'Arn'] }, + }); + + + }); + + test('specify displayName and topicName', () => { + const stack = new cdk.Stack(); + + new sns.Topic(stack, 'MyTopic', { + topicName: 'topicName', + displayName: 'displayName', + }); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'DisplayName': 'displayName', + 'TopicName': 'topicName', + }); + + + }); + + // NOTE: This test case should be invalid when CloudFormation problem reported in CDK issue 12386 is resolved + // see https://github.com/aws/aws-cdk/issues/12386 + test('throw with missing topicName on fifo topic', () => { + const stack = new cdk.Stack(); + + expect(() => new sns.Topic(stack, 'MyTopic', { + fifo: true, + })).toThrow(/FIFO SNS topics must be given a topic name./); + + + }); + + test('specify fifo without .fifo suffix in topicName', () => { + const stack = new cdk.Stack(); + + new sns.Topic(stack, 'MyTopic', { + fifo: true, + topicName: 'topicName', + }); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'FifoTopic': true, + 'TopicName': 'topicName.fifo', + }); + + + }); + + test('specify fifo with .fifo suffix in topicName', () => { + const stack = new cdk.Stack(); + + new sns.Topic(stack, 'MyTopic', { + fifo: true, + topicName: 'topicName.fifo', + }); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'FifoTopic': true, + 'TopicName': 'topicName.fifo', + }); + + + }); + + test('specify fifo without contentBasedDeduplication', () => { + const stack = new cdk.Stack(); + + new sns.Topic(stack, 'MyTopic', { + fifo: true, + topicName: 'topicName', + }); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'FifoTopic': true, + 'TopicName': 'topicName.fifo', + }); + + + }); + + test('specify fifo with contentBasedDeduplication', () => { + const stack = new cdk.Stack(); + + new sns.Topic(stack, 'MyTopic', { + contentBasedDeduplication: true, + fifo: true, + topicName: 'topicName', + }); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'ContentBasedDeduplication': true, + 'FifoTopic': true, + 'TopicName': 'topicName.fifo', + }); + + + }); + + test('throw with contentBasedDeduplication on non-fifo topic', () => { + const stack = new cdk.Stack(); + + expect(() => new sns.Topic(stack, 'MyTopic', { + contentBasedDeduplication: true, + })).toThrow(/Content based deduplication can only be enabled for FIFO SNS topics./); + + + }); + }); + + test('can add a policy to the topic', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + topic.addToResourcePolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['sns:*'], + principals: [new iam.ArnPrincipal('arn')], + })); + + // THEN + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + 'Sid': '0', + 'Action': 'sns:*', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Resource': '*', + }], + }, + }); + + + }); + + test('give publishing permissions', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + const user = new iam.User(stack, 'User'); + + // WHEN + topic.grantPublish(user); + + // THEN + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + 'PolicyDocument': { + Version: '2012-10-17', + 'Statement': [ + { + 'Action': 'sns:Publish', + 'Effect': 'Allow', + 'Resource': stack.resolve(topic.topicArn), + }, + ], + }, + }); + + + }); + + test('TopicPolicy passed document', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + const ps = new iam.PolicyStatement({ + actions: ['service:statement0'], + principals: [new iam.ArnPrincipal('arn')], + }); + + // WHEN + new sns.TopicPolicy(stack, 'topicpolicy', { topics: [topic], policyDocument: new iam.PolicyDocument({ assignSids: true, statements: [ps] }) }); + + // THEN + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'service:statement0', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Sid': '0', + }, + ], + 'Version': '2012-10-17', + }, + 'Topics': [ + { + 'Ref': 'MyTopic86869434', + }, + ], + }); + + + }); + + test('Add statements to policy', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + const topicPolicy = new sns.TopicPolicy(stack, 'TopicPolicy', { + topics: [topic], + }); + topicPolicy.document.addStatements(new iam.PolicyStatement({ + actions: ['service:statement0'], + principals: [new iam.ArnPrincipal('arn')], + })); + + // THEN + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'service:statement0', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Sid': '0', + }, + ], + 'Version': '2012-10-17', + }, + 'Topics': [ + { + 'Ref': 'MyTopic86869434', + }, + ], + }); + + }); + + test('topic resource policy includes unique SIDs', () => { + const stack = new cdk.Stack(); + + const topic = new sns.Topic(stack, 'MyTopic'); + + topic.addToResourcePolicy(new iam.PolicyStatement({ + actions: ['service:statement0'], + principals: [new iam.ArnPrincipal('arn')], + })); + topic.addToResourcePolicy(new iam.PolicyStatement({ + actions: ['service:statement1'], + principals: [new iam.ArnPrincipal('arn')], + })); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'service:statement0', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Sid': '0', + }, + { + 'Action': 'service:statement1', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Sid': '1', + }, + ], + 'Version': '2012-10-17', + }, + 'Topics': [ + { + 'Ref': 'MyTopic86869434', + }, + ], + }); + + + }); + + test('fromTopicArn', () => { + // GIVEN + const stack2 = new cdk.Stack(); + + // WHEN + const imported = sns.Topic.fromTopicArn(stack2, 'Imported', 'arn:aws:sns:*:123456789012:my_corporate_topic'); + + // THEN + expect(imported.topicName).toEqual('my_corporate_topic'); + expect(imported.topicArn).toEqual('arn:aws:sns:*:123456789012:my_corporate_topic'); + + }); + + test('test metrics', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + // THEN + expect(stack.resolve(topic.metricNumberOfMessagesPublished())).toEqual({ + dimensions: { TopicName: { 'Fn::GetAtt': ['TopicBFC7AF6E', 'TopicName'] } }, + namespace: 'AWS/SNS', + metricName: 'NumberOfMessagesPublished', + period: cdk.Duration.minutes(5), + statistic: 'Sum', + }); + + expect(stack.resolve(topic.metricPublishSize())).toEqual({ + dimensions: { TopicName: { 'Fn::GetAtt': ['TopicBFC7AF6E', 'TopicName'] } }, + namespace: 'AWS/SNS', + metricName: 'PublishSize', + period: cdk.Duration.minutes(5), + statistic: 'Average', + }); + + + }); + + test('subscription is created under the topic scope by default', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + topic.addSubscription({ + bind: () => ({ + protocol: sns.SubscriptionProtocol.HTTP, + endpoint: 'http://foo/bar', + subscriberId: 'my-subscription', + }), + }); + + // THEN + TemplateAssertions.fromStack(stack).resourceCountIs('AWS::SNS::Subscription', 1); + + }); + + test('if "scope" is defined, subscription will be created under that scope', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'A'); + const stack2 = new cdk.Stack(app, 'B'); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + topic.addSubscription({ + bind: () => ({ + protocol: sns.SubscriptionProtocol.HTTP, + endpoint: 'http://foo/bar', + subscriberScope: stack2, + subscriberId: 'subscriberId', + }), + }); + + // THEN + TemplateAssertions.fromStack(stack).resourceCountIs('AWS::SNS::Subscription', 0); + TemplateAssertions.fromStack(stack2).resourceCountIs('AWS::SNS::Subscription', 1); + + }); + + test('fails if topic policy has no actions', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + topic.addToResourcePolicy(new iam.PolicyStatement({ + resources: ['*'], + principals: [new iam.ArnPrincipal('arn')], + })); + + // THEN + expect(() => app.synth()).toThrow(/A PolicyStatement must specify at least one \'action\' or \'notAction\'/); + + }); + + test('fails if topic policy has no IAM principals', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + topic.addToResourcePolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['sns:*'], + })); + + // THEN + expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); + + }); + + test('topic policy should be set if topic as a notifications rule target', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const topic = new sns.Topic(stack, 'Topic'); + const rule = new notifications.NotificationRule(stack, 'MyNotificationRule', { + source: { + bindAsNotificationRuleSource: () => ({ + sourceArn: 'ARN', + }), + }, + events: ['codebuild-project-build-state-succeeded'], + }); + + rule.addTarget(topic); + + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + 'Sid': '0', + 'Action': 'sns:Publish', + 'Effect': 'Allow', + 'Principal': { 'Service': 'codestar-notifications.amazonaws.com' }, + 'Resource': { 'Ref': 'TopicBFC7AF6E' }, + }], + }, + Topics: [{ + Ref: 'TopicBFC7AF6E', + }], + }); + + + }); +}); diff --git a/packages/@aws-cdk/aws-sns/test/test.subscription.ts b/packages/@aws-cdk/aws-sns/test/subscription.test.ts similarity index 76% rename from packages/@aws-cdk/aws-sns/test/test.subscription.ts rename to packages/@aws-cdk/aws-sns/test/subscription.test.ts index c8ade98bc81d5..7717b7f12c7d4 100644 --- a/packages/@aws-cdk/aws-sns/test/test.subscription.ts +++ b/packages/@aws-cdk/aws-sns/test/subscription.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import { Queue } from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as sns from '../lib'; -export = { - 'create a subscription'(test: Test) { +describe('Subscription', () => { + test('create a subscription', () => { // GIVEN const stack = new cdk.Stack(); const topic = new sns.Topic(stack, 'Topic'); @@ -18,17 +17,17 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::SNS::Subscription', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Endpoint: 'endpoint', Protocol: 'lambda', TopicArn: { Ref: 'TopicBFC7AF6E', }, - })); - test.done(); - }, + }); + + }); - 'create a subscription with DLQ when client provides DLQ'(test: Test) { + test('create a subscription with DLQ when client provides DLQ', () => { // GIVEN const stack = new cdk.Stack(); const topic = new sns.Topic(stack, 'Topic'); @@ -46,7 +45,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::SNS::Subscription', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { Endpoint: 'endpoint', Protocol: 'lambda', TopicArn: { @@ -60,12 +59,12 @@ export = { ], }, }, - })); - expect(stack).to(haveResource('AWS::SQS::Queue', { + }); + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SQS::Queue', { QueueName: 'MySubscription_DLQ', MessageRetentionPeriod: 1209600, - })); - expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + }); + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SQS::QueuePolicy', { PolicyDocument: { Statement: [ { @@ -96,11 +95,11 @@ export = { Ref: 'DeadLetterQueue9F481546', }, ], - })); - test.done(); - }, + }); + + }); - 'with filter policy'(test: Test) { + test('with filter policy', () => { // GIVEN const stack = new cdk.Stack(); const topic = new sns.Topic(stack, 'Topic'); @@ -129,7 +128,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::SNS::Subscription', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { FilterPolicy: { color: [ 'red', @@ -149,11 +148,11 @@ export = { { numeric: ['>', 2000, '<', 3000] }, ], }, - })); - test.done(); - }, + }); - 'with existsFilter'(test: Test) { + }); + + test('with existsFilter', () => { // GIVEN const stack = new cdk.Stack(); const topic = new sns.Topic(stack, 'Topic'); @@ -169,37 +168,37 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::SNS::Subscription', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { FilterPolicy: { size: [{ exists: true }], }, - })); - test.done(); - }, + }); + + }); - 'throws with raw delivery for protocol other than http, https or sqs'(test: Test) { + test('throws with raw delivery for protocol other than http, https or sqs', () => { // GIVEN const stack = new cdk.Stack(); const topic = new sns.Topic(stack, 'Topic'); // THEN - test.throws(() => new sns.Subscription(stack, 'Subscription', { + expect(() => new sns.Subscription(stack, 'Subscription', { endpoint: 'endpoint', protocol: sns.SubscriptionProtocol.LAMBDA, topic, rawMessageDelivery: true, - }), /Raw message delivery/); - test.done(); - }, + })).toThrow(/Raw message delivery/); - 'throws with more than 5 attributes in a filter policy'(test: Test) { + }); + + test('throws with more than 5 attributes in a filter policy', () => { // GIVEN const stack = new cdk.Stack(); const topic = new sns.Topic(stack, 'Topic'); const cond = { conditions: [] }; // THEN - test.throws(() => new sns.Subscription(stack, 'Subscription', { + expect(() => new sns.Subscription(stack, 'Subscription', { endpoint: 'endpoint', protocol: sns.SubscriptionProtocol.LAMBDA, topic, @@ -211,17 +210,17 @@ export = { e: cond, f: cond, }, - }), /5 attribute names/); - test.done(); - }, + })).toThrow(/5 attribute names/); + + }); - 'throws with more than 100 conditions in a filter policy'(test: Test) { + test('throws with more than 100 conditions in a filter policy', () => { // GIVEN const stack = new cdk.Stack(); const topic = new sns.Topic(stack, 'Topic'); // THEN - test.throws(() => new sns.Subscription(stack, 'Subscription', { + expect(() => new sns.Subscription(stack, 'Subscription', { endpoint: 'endpoint', protocol: sns.SubscriptionProtocol.LAMBDA, topic, @@ -230,7 +229,7 @@ export = { b: { conditions: [...Array.from(Array(10).keys())] }, c: { conditions: [...Array.from(Array(6).keys())] }, }, - }), /\(120\) must not exceed 100/); - test.done(); - }, -}; + })).toThrow(/\(120\) must not exceed 100/); + + }); +}); diff --git a/packages/@aws-cdk/aws-sns/test/test.sns.ts b/packages/@aws-cdk/aws-sns/test/test.sns.ts deleted file mode 100644 index 09cf051922e5e..0000000000000 --- a/packages/@aws-cdk/aws-sns/test/test.sns.ts +++ /dev/null @@ -1,570 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import * as notifications from '@aws-cdk/aws-codestarnotifications'; -import * as iam from '@aws-cdk/aws-iam'; -import * as kms from '@aws-cdk/aws-kms'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as sns from '../lib'; - -/* eslint-disable quote-props */ - -export = { - 'topic tests': { - 'all defaults'(test: Test) { - const stack = new cdk.Stack(); - new sns.Topic(stack, 'MyTopic'); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - }, - }, - }); - - test.done(); - }, - - 'specify topicName'(test: Test) { - const stack = new cdk.Stack(); - - new sns.Topic(stack, 'MyTopic', { - topicName: 'topicName', - }); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - 'Properties': { - 'TopicName': 'topicName', - }, - }, - }, - }); - - test.done(); - }, - - 'specify displayName'(test: Test) { - const stack = new cdk.Stack(); - - new sns.Topic(stack, 'MyTopic', { - displayName: 'displayName', - }); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - 'Properties': { - 'DisplayName': 'displayName', - }, - }, - }, - }); - - test.done(); - }, - - 'specify kmsMasterKey'(test: Test) { - const stack = new cdk.Stack(); - const key = new kms.Key(stack, 'CustomKey'); - - new sns.Topic(stack, 'MyTopic', { - masterKey: key, - }); - - expect(stack).to(haveResource('AWS::SNS::Topic', { - 'KmsMasterKeyId': { 'Fn::GetAtt': ['CustomKey1E6D0D07', 'Arn'] }, - })); - - test.done(); - }, - - 'specify displayName and topicName'(test: Test) { - const stack = new cdk.Stack(); - - new sns.Topic(stack, 'MyTopic', { - topicName: 'topicName', - displayName: 'displayName', - }); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - 'Properties': { - 'DisplayName': 'displayName', - 'TopicName': 'topicName', - }, - }, - }, - }); - - test.done(); - }, - - // NOTE: This test case should be invalid when CloudFormation problem reported in CDK issue 12386 is resolved - // see https://github.com/aws/aws-cdk/issues/12386 - 'throw with missing topicName on fifo topic'(test: Test) { - const stack = new cdk.Stack(); - - test.throws(() => new sns.Topic(stack, 'MyTopic', { - fifo: true, - }), /FIFO SNS topics must be given a topic name./); - - test.done(); - }, - - 'specify fifo without .fifo suffix in topicName'(test: Test) { - const stack = new cdk.Stack(); - - new sns.Topic(stack, 'MyTopic', { - fifo: true, - topicName: 'topicName', - }); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - 'Properties': { - 'FifoTopic': true, - 'TopicName': 'topicName.fifo', - }, - }, - }, - }); - - test.done(); - }, - - 'specify fifo with .fifo suffix in topicName'(test: Test) { - const stack = new cdk.Stack(); - - new sns.Topic(stack, 'MyTopic', { - fifo: true, - topicName: 'topicName.fifo', - }); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - 'Properties': { - 'FifoTopic': true, - 'TopicName': 'topicName.fifo', - }, - }, - }, - }); - - test.done(); - }, - - 'specify fifo without contentBasedDeduplication'(test: Test) { - const stack = new cdk.Stack(); - - new sns.Topic(stack, 'MyTopic', { - fifo: true, - topicName: 'topicName', - }); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - 'Properties': { - 'FifoTopic': true, - 'TopicName': 'topicName.fifo', - }, - }, - }, - }); - - test.done(); - }, - - 'specify fifo with contentBasedDeduplication'(test: Test) { - const stack = new cdk.Stack(); - - new sns.Topic(stack, 'MyTopic', { - contentBasedDeduplication: true, - fifo: true, - topicName: 'topicName', - }); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - 'Properties': { - 'ContentBasedDeduplication': true, - 'FifoTopic': true, - 'TopicName': 'topicName.fifo', - }, - }, - }, - }); - - test.done(); - }, - - 'throw with contentBasedDeduplication on non-fifo topic'(test: Test) { - const stack = new cdk.Stack(); - - test.throws(() => new sns.Topic(stack, 'MyTopic', { - contentBasedDeduplication: true, - }), /Content based deduplication can only be enabled for FIFO SNS topics./); - - test.done(); - }, - }, - - 'can add a policy to the topic'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const topic = new sns.Topic(stack, 'Topic'); - - // WHEN - topic.addToResourcePolicy(new iam.PolicyStatement({ - resources: ['*'], - actions: ['sns:*'], - principals: [new iam.ArnPrincipal('arn')], - })); - - // THEN - expect(stack).to(haveResource('AWS::SNS::TopicPolicy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [{ - 'Sid': '0', - 'Action': 'sns:*', - 'Effect': 'Allow', - 'Principal': { 'AWS': 'arn' }, - 'Resource': '*', - }], - }, - })); - - test.done(); - }, - - 'give publishing permissions'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const topic = new sns.Topic(stack, 'Topic'); - const user = new iam.User(stack, 'User'); - - // WHEN - topic.grantPublish(user); - - // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { - 'PolicyDocument': { - Version: '2012-10-17', - 'Statement': [ - { - 'Action': 'sns:Publish', - 'Effect': 'Allow', - 'Resource': stack.resolve(topic.topicArn), - }, - ], - }, - })); - - test.done(); - }, - - 'TopicPolicy passed document'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const topic = new sns.Topic(stack, 'MyTopic'); - const ps = new iam.PolicyStatement({ - actions: ['service:statement0'], - principals: [new iam.ArnPrincipal('arn')], - }); - - // WHEN - new sns.TopicPolicy(stack, 'topicpolicy', { topics: [topic], policyDocument: new iam.PolicyDocument({ assignSids: true, statements: [ps] }) }); - - // THEN - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - }, - 'topicpolicyF8CF12FD': { - 'Type': 'AWS::SNS::TopicPolicy', - 'Properties': { - 'PolicyDocument': { - 'Statement': [ - { - 'Action': 'service:statement0', - 'Effect': 'Allow', - 'Principal': { 'AWS': 'arn' }, - 'Sid': '0', - }, - ], - 'Version': '2012-10-17', - }, - 'Topics': [ - { - 'Ref': 'MyTopic86869434', - }, - ], - }, - }, - }, - }); - - test.done(); - }, - - 'Add statements to policy'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const topic = new sns.Topic(stack, 'MyTopic'); - - // WHEN - const topicPolicy = new sns.TopicPolicy(stack, 'TopicPolicy', { - topics: [topic], - }); - topicPolicy.document.addStatements(new iam.PolicyStatement({ - actions: ['service:statement0'], - principals: [new iam.ArnPrincipal('arn')], - })); - - // THEN - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - }, - 'TopicPolicyA24B096F': { - 'Type': 'AWS::SNS::TopicPolicy', - 'Properties': { - 'PolicyDocument': { - 'Statement': [ - { - 'Action': 'service:statement0', - 'Effect': 'Allow', - 'Principal': { 'AWS': 'arn' }, - 'Sid': '0', - }, - ], - 'Version': '2012-10-17', - }, - 'Topics': [ - { - 'Ref': 'MyTopic86869434', - }, - ], - }, - }, - }, - }); - test.done(); - }, - - 'topic resource policy includes unique SIDs'(test: Test) { - const stack = new cdk.Stack(); - - const topic = new sns.Topic(stack, 'MyTopic'); - - topic.addToResourcePolicy(new iam.PolicyStatement({ - actions: ['service:statement0'], - principals: [new iam.ArnPrincipal('arn')], - })); - topic.addToResourcePolicy(new iam.PolicyStatement({ - actions: ['service:statement1'], - principals: [new iam.ArnPrincipal('arn')], - })); - - expect(stack).toMatch({ - 'Resources': { - 'MyTopic86869434': { - 'Type': 'AWS::SNS::Topic', - }, - 'MyTopicPolicy12A5EC17': { - 'Type': 'AWS::SNS::TopicPolicy', - 'Properties': { - 'PolicyDocument': { - 'Statement': [ - { - 'Action': 'service:statement0', - 'Effect': 'Allow', - 'Principal': { 'AWS': 'arn' }, - 'Sid': '0', - }, - { - 'Action': 'service:statement1', - 'Effect': 'Allow', - 'Principal': { 'AWS': 'arn' }, - 'Sid': '1', - }, - ], - 'Version': '2012-10-17', - }, - 'Topics': [ - { - 'Ref': 'MyTopic86869434', - }, - ], - }, - }, - }, - }); - - test.done(); - }, - - 'fromTopicArn'(test: Test) { - // GIVEN - const stack2 = new cdk.Stack(); - - // WHEN - const imported = sns.Topic.fromTopicArn(stack2, 'Imported', 'arn:aws:sns:*:123456789012:my_corporate_topic'); - - // THEN - test.deepEqual(imported.topicName, 'my_corporate_topic'); - test.deepEqual(imported.topicArn, 'arn:aws:sns:*:123456789012:my_corporate_topic'); - test.done(); - }, - - 'test metrics'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const topic = new sns.Topic(stack, 'Topic'); - - // THEN - test.deepEqual(stack.resolve(topic.metricNumberOfMessagesPublished()), { - dimensions: { TopicName: { 'Fn::GetAtt': ['TopicBFC7AF6E', 'TopicName'] } }, - namespace: 'AWS/SNS', - metricName: 'NumberOfMessagesPublished', - period: cdk.Duration.minutes(5), - statistic: 'Sum', - }); - - test.deepEqual(stack.resolve(topic.metricPublishSize()), { - dimensions: { TopicName: { 'Fn::GetAtt': ['TopicBFC7AF6E', 'TopicName'] } }, - namespace: 'AWS/SNS', - metricName: 'PublishSize', - period: cdk.Duration.minutes(5), - statistic: 'Average', - }); - - test.done(); - }, - - 'subscription is created under the topic scope by default'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const topic = new sns.Topic(stack, 'Topic'); - - // WHEN - topic.addSubscription({ - bind: () => ({ - protocol: sns.SubscriptionProtocol.HTTP, - endpoint: 'http://foo/bar', - subscriberId: 'my-subscription', - }), - }); - - // THEN - expect(stack).to(haveResource('AWS::SNS::Subscription')); - test.done(); - }, - - 'if "scope" is defined, subscription will be created under that scope'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'A'); - const stack2 = new cdk.Stack(app, 'B'); - const topic = new sns.Topic(stack, 'Topic'); - - // WHEN - topic.addSubscription({ - bind: () => ({ - protocol: sns.SubscriptionProtocol.HTTP, - endpoint: 'http://foo/bar', - subscriberScope: stack2, - subscriberId: 'subscriberId', - }), - }); - - // THEN - expect(stack).notTo(haveResource('AWS::SNS::Subscription')); - expect(stack2).to(haveResource('AWS::SNS::Subscription')); - test.done(); - }, - - 'fails if topic policy has no actions'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'my-stack'); - const topic = new sns.Topic(stack, 'Topic'); - - // WHEN - topic.addToResourcePolicy(new iam.PolicyStatement({ - resources: ['*'], - principals: [new iam.ArnPrincipal('arn')], - })); - - // THEN - test.throws(() => app.synth(), /A PolicyStatement must specify at least one \'action\' or \'notAction\'/); - test.done(); - }, - - 'fails if topic policy has no IAM principals'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'my-stack'); - const topic = new sns.Topic(stack, 'Topic'); - - // WHEN - topic.addToResourcePolicy(new iam.PolicyStatement({ - resources: ['*'], - actions: ['sns:*'], - })); - - // THEN - test.throws(() => app.synth(), /A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); - test.done(); - }, - - 'topic policy should be set if topic as a notifications rule target'(test: Test) { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'my-stack'); - const topic = new sns.Topic(stack, 'Topic'); - const rule = new notifications.NotificationRule(stack, 'MyNotificationRule', { - source: { - bindAsNotificationRuleSource: () => ({ - sourceArn: 'ARN', - }), - }, - events: ['codebuild-project-build-state-succeeded'], - }); - - rule.addTarget(topic); - - expect(stack).to(haveResource('AWS::SNS::TopicPolicy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [{ - 'Sid': '0', - 'Action': 'sns:Publish', - 'Effect': 'Allow', - 'Principal': { 'Service': 'codestar-notifications.amazonaws.com' }, - 'Resource': { 'Ref': 'TopicBFC7AF6E' }, - }], - }, - Topics: [{ - Ref: 'TopicBFC7AF6E', - }], - })); - - test.done(); - }, -}; From fdce08cee6f0eb58aad93572641a1dd4b59e8d37 Mon Sep 17 00:00:00 2001 From: TikiTDO Date: Mon, 19 Jul 2021 05:42:16 -0400 Subject: [PATCH 069/105] fix(iam): `PrincipalWithConditions.addCondition` does not work (#15414) In `PrincipalWithConditions` the `conditions` parameter uses the getter syntax to return a new conditions object each time, which means the `addCondition` call will effectively throw away any conditions being added. This PR ensures the conditions added with `addCondition` show up in the resulting principal. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-iam/lib/principals.ts | 4 +- .../@aws-cdk/aws-iam/test/principals.test.ts | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-iam/lib/principals.ts b/packages/@aws-cdk/aws-iam/lib/principals.ts index 80b4480209fb9..2c89f96749324 100644 --- a/packages/@aws-cdk/aws-iam/lib/principals.ts +++ b/packages/@aws-cdk/aws-iam/lib/principals.ts @@ -165,8 +165,8 @@ export class PrincipalWithConditions implements IPrincipal { * Add a condition to the principal */ public addCondition(key: string, value: Condition) { - const existingValue = this.conditions[key]; - this.conditions[key] = existingValue ? { ...existingValue, ...value } : value; + const existingValue = this.additionalConditions[key]; + this.additionalConditions[key] = existingValue ? { ...existingValue, ...value } : value; } /** diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index c2495b2975bfd..1914f174adfd4 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -167,6 +167,45 @@ test('SAML principal', () => { }); }); +test('PrincipalWithConditions.addCondition should work', () => { + // GIVEN + const stack = new Stack(); + const basePrincipal = new iam.ServicePrincipal('service.amazonaws.com'); + const principalWithConditions = new iam.PrincipalWithConditions(basePrincipal, { + StringEquals: { + 'aws:PrincipalOrgID': ['o-xxxxxxxxxxx'], + }, + }); + + // WHEN + principalWithConditions.addCondition('StringEquals', { 'aws:PrincipalTag/critical': 'true' }); + new iam.Role(stack, 'Role', { + assumedBy: principalWithConditions, + }); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Condition: { + StringEquals: { + 'aws:PrincipalOrgID': ['o-xxxxxxxxxxx'], + 'aws:PrincipalTag/critical': 'true', + }, + }, + Effect: 'Allow', + Principal: { + Service: 'service.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + test('PrincipalWithConditions inherits principalAccount from AccountPrincipal ', () => { // GIVEN const accountPrincipal = new iam.AccountPrincipal('123456789012'); From 5fd8b7b7090db85d309d342e5906b095cea52da6 Mon Sep 17 00:00:00 2001 From: Thorsten Hoeger Date: Mon, 19 Jul 2021 12:25:29 +0200 Subject: [PATCH 070/105] chore(pipelines): cleanup some docs (#15626) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/pipelines/lib/blueprint/index.ts | 2 +- .../{script-step.ts => shell-step.ts} | 0 .../lib/codepipeline/_codebuild-factory.ts | 18 +++++++++--------- .../lib/codepipeline/codebuild-step.ts | 4 ++-- .../pipelines/lib/codepipeline/codepipeline.ts | 4 ++-- .../lib/helpers-internal/pipeline-graph.ts | 8 ++++---- .../lib/private/application-security-check.ts | 2 +- .../pipelines/lib/private/asset-manifest.ts | 6 +++--- .../test/compliance/escape-hatching.test.ts | 2 ++ .../pipelines/test/compliance/synths.test.ts | 6 ++++++ 10 files changed, 30 insertions(+), 22 deletions(-) rename packages/@aws-cdk/pipelines/lib/blueprint/{script-step.ts => shell-step.ts} (100%) diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/index.ts b/packages/@aws-cdk/pipelines/lib/blueprint/index.ts index d842ca1c7cd67..4720a55ce649b 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/index.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/index.ts @@ -1,6 +1,6 @@ export * from './asset-type'; export * from './file-set'; -export * from './script-step'; +export * from './shell-step'; export * from './stack-deployment'; export * from './stage-deployment'; export * from './step'; diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts similarity index 100% rename from packages/@aws-cdk/pipelines/lib/blueprint/script-step.ts rename to packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts index f814e8b8fe272..1dc6125a9a74e 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts @@ -113,22 +113,22 @@ export interface CodeBuildFactoryProps { } /** - * Produce a CodeBuild project from a RunScript step and some CodeBuild-specific customizations + * Produce a CodeBuild project from a ShellStep and some CodeBuild-specific customizations * * The functionality here is shared between the `CodePipeline` translating a `ShellStep` into * a CodeBuild project, as well as the `CodeBuildStep` straight up. */ export class CodeBuildFactory implements ICodePipelineActionFactory { // eslint-disable-next-line max-len - public static fromShellStep(constructId: string, scriptStep: ShellStep, additional?: Partial): ICodePipelineActionFactory { + public static fromShellStep(constructId: string, shellStep: ShellStep, additional?: Partial): ICodePipelineActionFactory { return new CodeBuildFactory(constructId, { - commands: scriptStep.commands, - env: scriptStep.env, - envFromCfnOutputs: scriptStep.envFromCfnOutputs, - inputs: scriptStep.inputs, - outputs: scriptStep.outputs, - stepId: scriptStep.id, - installCommands: scriptStep.installCommands, + commands: shellStep.commands, + env: shellStep.env, + envFromCfnOutputs: shellStep.envFromCfnOutputs, + inputs: shellStep.inputs, + outputs: shellStep.outputs, + stepId: shellStep.id, + installCommands: shellStep.installCommands, ...additional, }); } diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts index 064c283ef2f12..1454d34a167c3 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts @@ -4,7 +4,7 @@ import * as iam from '@aws-cdk/aws-iam'; import { ShellStep, ShellStepProps } from '../blueprint'; /** - * Construction props for SimpleSynthAction + * Construction props for a CodeBuildStep */ export interface CodeBuildStepProps extends ShellStepProps { /** @@ -160,7 +160,7 @@ export class CodeBuildStep extends ShellStep { } /** - * CodeBiuld Project generated for the pipeline + * CodeBuild Project generated for the pipeline * * Will only be available after the pipeline has been built. */ diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts index 1c61a2c8cc42d..e3c10d29b0740 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts @@ -471,7 +471,7 @@ export class CodePipeline extends PipelineBase { * * There are only 3 types of Steps we need to support: * - * - RunScript (generic) + * - Shell (generic) * - ManualApproval (generic) * - CodePipelineActionFactory (CodePipeline-specific) * @@ -729,7 +729,7 @@ export class CodePipeline extends PipelineBase { if (!arn) { return undefined; } - // Use placeholdered arn as construct ID. + // Use placeholder arn as construct ID. const id = arn; // https://github.com/aws/aws-cdk/issues/7255 diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts index 0adcea551ff32..8a7b198831808 100644 --- a/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts @@ -27,7 +27,7 @@ export interface PipelineGraphProps { /** * Add a "prepare" step for each stack which can be used to create the change - * set. If this is disbled, only the "execute" step will be included. + * set. If this is disabled, only the "execute" step will be included. * * @default true */ @@ -43,7 +43,7 @@ export class PipelineGraph { /** * A Step object that may be used as the producer of FileSets that should not be represented in the graph */ - public static readonly NO_STEP: Step = new class extends Step { } ('NO_STEP'); + public static readonly NO_STEP: Step = new class extends Step { }('NO_STEP'); public readonly graph: AGraph = Graph.of('', { type: 'group' }); public readonly cloudAssemblyFileSet: FileSet; @@ -244,7 +244,7 @@ export class PipelineGraph { node.dependOn(producerNode); } - // Add stack dependencies (by use of the dependencybuilder this also works + // Add stack dependencies (by use of the dependency builder this also works // if we encounter the Step before the Stack has been properly added yet) if (step instanceof ShellStep) { for (const output of Object.values(step.envFromCfnOutputs)) { @@ -261,7 +261,7 @@ export class PipelineGraph { let assetNode = this.assetNodes.get(stackAsset.assetId); if (assetNode) { - // If there's already a node pubishing this asset, add as a new publishing + // If there's already a node publishing this asset, add as a new publishing // destination to the same node. } else if (this.singlePublisher && this.assetNodesByType.has(stackAsset.assetType)) { // If we're doing a single node per type, lookup by that diff --git a/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts b/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts index 152404db70a30..b8d7a7fd0d2bb 100644 --- a/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts +++ b/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts @@ -11,7 +11,7 @@ import { Construct } from 'constructs'; import { Construct as CoreConstruct } from '@aws-cdk/core'; /** - * Properteis for an ApplicationSecurityCheck + * Properties for an ApplicationSecurityCheck */ export interface ApplicationSecurityCheckProps { /** diff --git a/packages/@aws-cdk/pipelines/lib/private/asset-manifest.ts b/packages/@aws-cdk/pipelines/lib/private/asset-manifest.ts index 64e1d94893357..5b93aa17dc8bb 100644 --- a/packages/@aws-cdk/pipelines/lib/private/asset-manifest.ts +++ b/packages/@aws-cdk/pipelines/lib/private/asset-manifest.ts @@ -21,7 +21,7 @@ export class AssetManifestReader { return new AssetManifestReader(path.dirname(fileName), obj); } catch (e) { - throw new Error(`Canot read asset manifest '${fileName}': ${e.message}`); + throw new Error(`Cannot read asset manifest '${fileName}': ${e.message}`); } } @@ -63,7 +63,7 @@ export class AssetManifestReader { if (selection === undefined) { return this; } const ret: AssetManifest & Required> - = { version: this.manifest.version, dockerImages: {}, files: {} }; + = { version: this.manifest.version, dockerImages: {}, files: {} }; for (const assetType of ASSET_TYPES) { for (const [assetId, asset] of Object.entries(this.manifest[assetType] || {})) { @@ -267,7 +267,7 @@ export class DestinationPattern { */ public matches(id: DestinationIdentifier) { return (this.assetId === undefined || this.assetId === id.assetId) - && (this.destinationId === undefined || this.destinationId === id.destinationId); + && (this.destinationId === undefined || this.destinationId === id.destinationId); } /** diff --git a/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts b/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts index 82754f52d5cba..3a8cb26d4d2fd 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/escape-hatching.test.ts @@ -112,6 +112,8 @@ describe('with custom Source stage in existing Pipeline', () => { commands: ['true'], }), }); + + THEN_codePipelineExpectation(); }); function THEN_codePipelineExpectation() { diff --git a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts index 92d1c9164fcba..597d40338a455 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts @@ -195,6 +195,8 @@ behavior('npm synth sets, or allows setting, UNSAFE_PERM=true', (suite) => { NPM_CONFIG_UNSAFE_PERM: 'true', }, }); + + THEN_codePipelineExpectation(); }); function THEN_codePipelineExpectation() { @@ -553,6 +555,8 @@ behavior('Synth can be made to run in a VPC', (suite) => { new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { codeBuildDefaults: { vpc }, }); + + THEN_codePipelineExpectation(); }); suite.additional('Modern, using CodeBuildStep', () => { @@ -567,6 +571,8 @@ behavior('Synth can be made to run in a VPC', (suite) => { }), codeBuildDefaults: { vpc }, }); + + THEN_codePipelineExpectation(); }); function THEN_codePipelineExpectation() { From 85cb788e3d30de6317c1612c9d25d85a2c06b972 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 19 Jul 2021 13:29:05 +0200 Subject: [PATCH 071/105] docs(pipelines): explain 'no matching base directory' error (#15644) Add a Troubleshooting section for the 'no matching base directory' error, which is probably going to be common with the new API. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 711a73f7e64e1..9760e382e2155 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -953,6 +953,20 @@ One of the target (account, region) environments has not been bootstrapped with the new bootstrap stack. Check your target environments and make sure they are all bootstrapped. +### Message: no matching base directory path found for cdk.out + +If you see this error during the **Synth** step, it means that CodeBuild +is expecting to find a `cdk.out` directory in the root of your CodeBuild project, +but the directory wasn't there. There are two common causes for this: + +* `cdk synth` is not being executed: `cdk synth` used to be run + implicitly for you, but you now have to explicitly include the command. + For NPM-based projects, add `npx cdk synth` to the end of the `commands` + property, for other languages add `npm install -g aws-cdk` and `cdk synth`. +* Your CDK project lives in a subdirectory: you added a `cd ` command + to the list of commands; don't forget to tell the `ScriptStep` about the + different location of `cdk.out`, by passing `primaryOutputDirectory: '/cdk.out'`. + ### is in ROLLBACK_COMPLETE state and can not be updated If you see the following error during execution of your pipeline: From 28e08059d47c3834b175c2cef67d2963d2b2aff9 Mon Sep 17 00:00:00 2001 From: Otavio Macedo Date: Mon, 19 Jul 2021 14:47:23 +0100 Subject: [PATCH 072/105] chore(pipelines): new tests for CodePipelineSource (#15643) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/codepipeline/codepipeline-source.ts | 2 +- .../codepipeline/codepipeline-sources.test.ts | 79 ++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts index d9075b45f8334..b6d10b03f2f67 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -131,7 +131,7 @@ export interface GitHubSourceOptions { * * ```ts * const oauth = cdk.SecretValue.secretsManager('my-github-token'); - * new GitHubSource(this, 'GitHubSource', { oauthToken: oauth, ... }); + * new GitHubSource(this, 'GitHubSource', { authentication: oauth, ... }); * ``` * * The GitHub Personal Access Token should have these scopes: diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts index f1991792bfd86..72553c38262d3 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts @@ -1,8 +1,10 @@ import { anything, arrayWith, objectLike } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; import * as ccommit from '@aws-cdk/aws-codecommit'; +import { CodeCommitTrigger, GitHubTrigger } from '@aws-cdk/aws-codepipeline-actions'; +import { AnyPrincipal, Role } from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; -import { Stack } from '@aws-cdk/core'; +import { SecretValue, Stack, Token } from '@aws-cdk/core'; import * as cdkp from '../../lib'; import { PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; @@ -41,6 +43,38 @@ test('CodeCommit source handles tokenized names correctly', () => { }); }); +test('CodeCommit source honors all valid properties', () => { + const repo = new ccommit.Repository(pipelineStack, 'Repo', { + repositoryName: 'MyRepo', + }); + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: cdkp.CodePipelineSource.codeCommit(repo, 'main', { + codeBuildCloneOutput: true, + trigger: CodeCommitTrigger.POLL, + eventRole: new Role(pipelineStack, 'role', { + assumedBy: new AnyPrincipal(), + roleName: 'some-role', + }), + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Source', + Actions: [ + objectLike({ + Configuration: objectLike({ + BranchName: 'main', + PollForSourceChanges: true, + OutputArtifactFormat: 'CODEBUILD_CLONE_REF', + }), + RoleArn: { 'Fn::GetAtt': [anything(), 'Arn'] }, + }), + ], + }), + }); +}); + test('S3 source handles tokenized names correctly', () => { const buckit = new s3.Bucket(pipelineStack, 'Buckit'); new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { @@ -63,5 +97,48 @@ test('S3 source handles tokenized names correctly', () => { }); }); +test('GitHub source honors all valid properties', () => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: cdkp.CodePipelineSource.gitHub('owner/repo', 'main', { + trigger: GitHubTrigger.POLL, + authentication: SecretValue.plainText('super-secret'), + }), + }); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Source', + Actions: [ + objectLike({ + Configuration: objectLike({ + Owner: 'owner', + Repo: 'repo', + Branch: 'main', + PollForSourceChanges: true, + OAuthToken: 'super-secret', + }), + Name: 'owner_repo', + }), + ], + }), + }); +}); + +test('GitHub source does not accept ill-formatted identifiers', () => { + expect(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: cdkp.CodePipelineSource.gitHub('repo-only', 'main'), + }); + }).toThrow('GitHub repository name should be a resolved string like \'/\', got \'repo-only\''); +}); + +test('GitHub source does not accept unresolved identifiers', () => { + expect(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: cdkp.CodePipelineSource.gitHub(Token.asString({}), 'main'), + }); + }).toThrow(/Step id cannot be unresolved/); +}); + // a-z0-9.@-_ \ No newline at end of file From d8dc8185203e73172786024eea90eeb60153ce0e Mon Sep 17 00:00:00 2001 From: Thorsten Hoeger Date: Mon, 19 Jul 2021 16:47:27 +0200 Subject: [PATCH 073/105] fix(pipelines): `CodeBuildStep.partialBuildSpec` not used, buildspec control for legacy API (#15625) this fixes #15169 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/codepipeline/_codebuild-factory.ts | 4 +- .../legacy/actions/publish-assets-action.ts | 11 +- .../legacy/actions/update-pipeline-action.ts | 45 ++++--- .../@aws-cdk/pipelines/lib/legacy/pipeline.ts | 19 +++ .../lib/legacy/synths/simple-synth-action.ts | 13 +- .../pipelines/test/compliance/assets.test.ts | 77 +++++++++++ .../test/compliance/self-mutation.test.ts | 67 +++++++++- .../pipelines/test/compliance/synths.test.ts | 120 ++++++++++++++++++ 8 files changed, 331 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts index 1dc6125a9a74e..c4f2eb41ab3fd 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts @@ -224,8 +224,8 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { environmentVariables: noEmptyObject(mapValues(mkdict(projectEnvs), value => ({ value }))), }); - const fullBuildSpec = options.codeBuildDefaults?.partialBuildSpec - ? codebuild.mergeBuildSpecs(options.codeBuildDefaults?.partialBuildSpec, buildSpecHere) + const fullBuildSpec = projectOptions?.partialBuildSpec + ? codebuild.mergeBuildSpecs(projectOptions.partialBuildSpec, buildSpecHere) : buildSpecHere; const osFromEnvironment = environment.buildImage && environment.buildImage instanceof codebuild.WindowsBuildImage diff --git a/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts index 72f2924e1690a..1c112aec9afbe 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/publish-assets-action.ts @@ -78,6 +78,14 @@ export interface PublishAssetsActionProps { */ readonly subnetSelection?: ec2.SubnetSelection; + + /** + * Custom BuildSpec that is merged with generated one + * + * @default - none + */ + readonly buildSpec?: codebuild.BuildSpec; + /** * Use a file buildspec written to the cloud assembly instead of an inline buildspec. * This prevents size limitation errors as inline specs have a max length of 25600 characters @@ -116,7 +124,7 @@ export class PublishAssetsAction extends CoreConstruct implements codepipeline.I const installSuffix = props.cdkCliVersion ? `@${props.cdkCliVersion}` : ''; const installCommand = `npm install -g cdk-assets${installSuffix}`; - this.buildSpec = codebuild.BuildSpec.fromObject({ + const buildSpec = codebuild.BuildSpec.fromObject({ version: '0.2', phases: { install: { @@ -127,6 +135,7 @@ export class PublishAssetsAction extends CoreConstruct implements codepipeline.I }, }, }); + this.buildSpec = props.buildSpec ? codebuild.mergeBuildSpecs(props.buildSpec, buildSpec) : buildSpec; const project = new codebuild.PipelineProject(this, 'Default', { projectName: this.props.projectName, diff --git a/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts index cc866c20e51d8..bc50e80c0aa56 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/actions/update-pipeline-action.ts @@ -62,6 +62,13 @@ export interface UpdatePipelineActionProps { * @default [] */ readonly dockerCredentials?: DockerCredential[]; + + /** + * Custom BuildSpec that is merged with generated one + * + * @default - none + */ + readonly buildSpec?: codebuild.BuildSpec; } /** @@ -82,29 +89,30 @@ export class UpdatePipelineAction extends CoreConstruct implements codepipeline. const installSuffix = props.cdkCliVersion ? `@${props.cdkCliVersion}` : ''; const stackIdentifier = props.pipelineStackHierarchicalId ?? props.pipelineStackName; + const buildSpec = codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + install: { + commands: [ + `npm install -g aws-cdk${installSuffix}`, + ...dockerCredentialsInstallCommands(DockerCredentialUsage.SELF_UPDATE, props.dockerCredentials), + ], + }, + build: { + commands: [ + // Cloud Assembly is in *current* directory. + `cdk -a ${embeddedAsmPath(scope)} deploy ${stackIdentifier} --require-approval=never --verbose`, + ], + }, + }, + }); const selfMutationProject = new codebuild.PipelineProject(this, 'SelfMutation', { projectName: props.projectName, environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_5_0, privileged: props.privileged ?? false, }, - buildSpec: codebuild.BuildSpec.fromObject({ - version: '0.2', - phases: { - install: { - commands: [ - `npm install -g aws-cdk${installSuffix}`, - ...dockerCredentialsInstallCommands(DockerCredentialUsage.SELF_UPDATE, props.dockerCredentials), - ], - }, - build: { - commands: [ - // Cloud Assembly is in *current* directory. - `cdk -a ${embeddedAsmPath(scope)} deploy ${stackIdentifier} --require-approval=never --verbose`, - ], - }, - }, - }), + buildSpec: props.buildSpec ? codebuild.mergeBuildSpecs(props.buildSpec, buildSpec) : buildSpec, }); // allow the self-mutating project permissions to assume the bootstrap Action role @@ -142,8 +150,7 @@ export class UpdatePipelineAction extends CoreConstruct implements codepipeline. /** * Exists to implement IAction */ - public bind(scope: CoreConstruct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): - codepipeline.ActionConfig { + public bind(scope: CoreConstruct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { return this.action.bind(scope, stage, options); } diff --git a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts index ec09a80dccb0e..e100c8c4a90f0 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -125,6 +126,13 @@ export interface CdkPipelineProps { */ readonly selfMutating?: boolean; + /** + * Custom BuildSpec that is merged with generated one (for self-mutation stage) + * + * @default - none + */ + readonly selfMutationBuildSpec?: codebuild.BuildSpec; + /** * Whether this pipeline creates one asset upload action per asset type or one asset upload per asset * @@ -140,6 +148,13 @@ export interface CdkPipelineProps { */ readonly assetPreInstallCommands?: string[]; + /** + * Custom BuildSpec that is merged with generated one (for asset publishing actions) + * + * @default - none + */ + readonly assetBuildSpec?: codebuild.BuildSpec; + /** * Whether the pipeline needs to build Docker images in the UpdatePipeline stage. * @@ -254,6 +269,7 @@ export class CdkPipeline extends CoreConstruct { projectName: maybeSuffix(props.pipelineName, '-selfupdate'), privileged: props.supportDockerAssets, dockerCredentials: this._dockerCredentials, + buildSpec: props.selfMutationBuildSpec, })], }); } @@ -267,6 +283,7 @@ export class CdkPipeline extends CoreConstruct { subnetSelection: props.subnetSelection, singlePublisherPerType: props.singlePublisherPerType, preInstallCommands: props.assetPreInstallCommands, + buildSpec: props.assetBuildSpec, dockerCredentials: this._dockerCredentials, }); } @@ -436,6 +453,7 @@ interface AssetPublishingProps { readonly subnetSelection?: ec2.SubnetSelection; readonly singlePublisherPerType?: boolean; readonly preInstallCommands?: string[]; + readonly buildSpec?: codebuild.BuildSpec; readonly dockerCredentials: DockerCredential[]; } @@ -542,6 +560,7 @@ class AssetPublishing extends CoreConstruct { dependable: this.assetAttachedPolicies[command.assetType], vpc: this.props.vpc, subnetSelection: this.props.subnetSelection, + buildSpec: this.props.buildSpec, createBuildspecFile: this.props.singlePublisherPerType, preInstallCommands: [...(this.props.preInstallCommands ?? []), ...credsInstallCommands], }); diff --git a/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts b/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts index b0fe2bcd466fb..1fd0b96bb2db6 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/synths/simple-synth-action.ts @@ -117,6 +117,13 @@ export interface SimpleSynthOptions { * @default - All private subnets. */ readonly subnetSelection?: ec2.SubnetSelection; + + /** + * custom BuildSpec that is merged with the generated one + * + * @default - none + */ + readonly buildSpec?: codebuild.BuildSpec; } /** @@ -358,13 +365,15 @@ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { ...copyEnvironmentVariables(...this.props.copyEnvironmentVariables || []), }; + const mergedBuildSpec = this.props.buildSpec ? codebuild.mergeBuildSpecs(this.props.buildSpec, buildSpec) : buildSpec; + // A hash over the values that make the CodeBuild Project unique (and necessary // to restart the pipeline if one of them changes). projectName is not necessary to include // here because the pipeline will definitely restart if projectName changes. // (Resolve tokens) const projectConfigHash = hash(Stack.of(scope).resolve({ environment: serializeBuildEnvironment(environment), - buildSpecString: buildSpec.toBuildSpec(), + buildSpecString: mergedBuildSpec.toBuildSpec(), environmentVariables, })); @@ -373,7 +382,7 @@ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { environment, vpc: this.props.vpc, subnetSelection: this.props.subnetSelection, - buildSpec, + buildSpec: mergedBuildSpec, environmentVariables, }); diff --git a/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts index 0f71dbde34650..8a465309016ef 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts @@ -808,6 +808,83 @@ describe('pipeline with single asset publisher', () => { }); }); + +describe('pipeline with custom asset publisher BuildSpec', () => { + + behavior('custom buildspec is merged correctly', (suite) => { + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + singlePublisherPerType: true, + assetBuildSpec: cb.BuildSpec.fromObject({ + phases: { + pre_install: { + commands: 'preinstall', + }, + }, + cache: { + paths: 'node_modules', + }, + }), + }); + pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + // WHEN + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + publishAssetsInParallel: false, + assetPublishingCodeBuildDefaults: { + partialBuildSpec: cb.BuildSpec.fromObject({ + phases: { + pre_install: { + commands: 'preinstall', + }, + }, + cache: { + paths: 'node_modules', + }, + }), + }, + }); + pipeline.addStage(new TwoFileAssetsApp(app, 'FileAssetApp')); + + THEN_codePipelineExpectation(); + }); + + + function THEN_codePipelineExpectation() { + const buildSpecName = Capture.aString(); + + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Assets', + Actions: [ + // Only one file asset action + objectLike({ RunOrder: 1, Name: 'FileAsset' }), + ], + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + }, + Source: { + BuildSpec: buildSpecName.capture(stringLike('buildspec-*.yaml')), + }, + }); + const assembly = SynthUtils.synthesize(pipelineStack, { skipValidation: true }).assembly; + const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, buildSpecName.capturedValue)).toString()); + expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`); + expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`); + expect(buildSpec.phases.pre_install.commands).toContain('preinstall'); + expect(buildSpec.cache.paths).toContain('node_modules'); + } + }); +}); + function expectedAssetRolePolicy(assumeRolePattern: string | string[], attachedRole: string) { if (typeof assumeRolePattern === 'string') { assumeRolePattern = [assumeRolePattern]; } diff --git a/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts index 8aa1ed8293c30..8196c84a0920b 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts @@ -1,6 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import { anything, arrayWith, deepObjectLike, encodedJson, notMatching, objectLike } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; +import * as cb from '@aws-cdk/aws-codebuild'; import * as cp from '@aws-cdk/aws-codepipeline'; import { Stack, Stage } from '@aws-cdk/core'; import { behavior, LegacyTestGitHubNpmPipeline, PIPELINE_ENV, stackTemplate, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; @@ -238,4 +239,68 @@ behavior('self-update project role uses tagged bootstrap-role permissions', (sui }, }); } -}); \ No newline at end of file +}); + + +behavior('self-mutation stage can be customized with BuildSpec', (suite) => { + suite.legacy(() => { + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + selfMutationBuildSpec: cb.BuildSpec.fromObject({ + phases: { + install: { + commands: 'npm config set registry example.com', + }, + }, + cache: { + paths: 'node_modules', + }, + }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + selfMutationCodeBuildDefaults: { + partialBuildSpec: cb.BuildSpec.fromObject({ + phases: { + install: { + commands: ['npm config set registry example.com'], + }, + }, + cache: { + paths: ['node_modules'], + }, + }), + }, + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:5.0', + PrivilegedMode: false, + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + install: { + commands: ['npm config set registry example.com', 'npm install -g aws-cdk'], + }, + build: { + commands: arrayWith('cdk -a . deploy PipelineStack --require-approval=never --verbose'), + }, + }, + cache: { + paths: ['node_modules'], + }, + })), + Type: 'CODEPIPELINE', + }, + }); + } +}); diff --git a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts index 597d40338a455..70e012aad8f1d 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts @@ -984,4 +984,124 @@ behavior('Can easily switch on privileged mode for synth', (suite) => { }, }); }); +}); + + +behavior('can provide custom BuildSpec that is merged with generated one', (suite) => { + suite.legacy(() => { + new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: new cdkp.SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + environmentVariables: { + SOME_ENV_VAR: { value: 'SomeValue' }, + }, + environment: { + environmentVariables: { + INNER_VAR: { value: 'InnerValue' }, + }, + privileged: true, + }, + installCommands: [ + 'install1', + 'install2', + ], + synthCommand: 'synth', + buildSpec: cbuild.BuildSpec.fromObject({ + env: { + variables: { + FOO: 'bar', + }, + }, + phases: { + pre_build: { + commands: 'installCustom', + }, + }, + cache: { + paths: ['node_modules'], + }, + }), + }), + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + synth: new cdkp.CodeBuildStep('Synth', { + input: cdkp.CodePipelineSource.gitHub('test/test', 'main'), + env: { + SOME_ENV_VAR: 'SomeValue', + }, + buildEnvironment: { + environmentVariables: { + INNER_VAR: { value: 'InnerValue' }, + }, + privileged: true, + }, + installCommands: [ + 'install1', + 'install2', + ], + commands: ['synth'], + partialBuildSpec: cbuild.BuildSpec.fromObject({ + env: { + variables: { + FOO: 'bar', + }, + }, + phases: { + pre_build: { + commands: ['installCustom'], + }, + }, + cache: { + paths: ['node_modules'], + }, + }), + }), + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: objectLike({ + PrivilegedMode: true, + EnvironmentVariables: arrayWith( + { + Name: 'INNER_VAR', + Type: 'PLAINTEXT', + Value: 'InnerValue', + }, + ), + }), + Source: { + BuildSpec: encodedJson(deepObjectLike({ + env: { + variables: { + FOO: 'bar', + }, + }, + phases: { + pre_build: { + commands: arrayWith('installCustom'), + }, + build: { + commands: ['synth'], + }, + }, + cache: { + paths: ['node_modules'], + }, + })), + }, + }); + } }); \ No newline at end of file From e7760ee9da19d7d006cdf1836ce6f71bf9f31327 Mon Sep 17 00:00:00 2001 From: wanjacki <83792238+wanjacki@users.noreply.github.com> Date: Mon, 19 Jul 2021 15:19:20 -0700 Subject: [PATCH 074/105] feat(servicecatalog): Add TagOptions for portfolio (#15612) Allows users to add TagOptions to their portfolio. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-servicecatalog/README.md | 16 ++++ .../@aws-cdk/aws-servicecatalog/lib/index.ts | 1 + .../aws-servicecatalog/lib/portfolio.ts | 22 ++++++ .../lib/private/association-manager.ts | 31 +++++++- .../aws-servicecatalog/lib/tag-options.ts | 14 ++++ .../test/integ.portfolio.expected.json | 57 ++++++++++++++ .../test/integ.portfolio.ts | 6 ++ .../aws-servicecatalog/test/portfolio.test.ts | 75 +++++++++++++++++++ 8 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index 63b461e1460f3..cbdf1d0224998 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -31,6 +31,7 @@ enables organizations to create and manage catalogs of products for their end us - [Sharing a portfolio with another AWS account](#sharing-a-portfolio-with-another-aws-account) - [Product](#product) - [Adding a product to a portfolio](#adding-a-product-to-a-portfolio) +- [TagOptions](#tag-options) - [Constraints](#constraints) - [Tag update constraint](#tag-update-constraint) @@ -157,6 +158,21 @@ A product can be added to multiple portfolios depending on your resource and org portfolio.addProduct(product); ``` +### Tag Options + +TagOptions allow administrators to easily manage tags on provisioned products by creating a selection of tags for end users to choose from. +For example, an end user can choose an `ec2` for the instance type size. +TagOptions are created by specifying a key with a selection of values. +At the moment, TagOptions can only be disabled in the console. + +```ts fixture=basic-portfolio +const tagOptions = new servicecatalog.TagOptions({ + ec2InstanceType: ['A1', 'M4'], + ec2InstanceSize: ['medium', 'large'], +}); +portfolio.associateTagOptions(tagOptions); +``` + ## Constraints Constraints define governance mechanisms that allow you to manage permissions, notifications, and options related to actions end users can perform on products, diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts index 7c621b4438ea4..8a7b0f0ff9ac6 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts @@ -3,6 +3,7 @@ export * from './constraints'; export * from './cloudformation-template'; export * from './portfolio'; export * from './product'; +export * from './tag-options'; // AWS::ServiceCatalog CloudFormation Resources: export * from './servicecatalog.generated'; diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts index ce1ac8edee493..7db7998b34c83 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts @@ -7,6 +7,7 @@ import { hashValues } from './private/util'; import { InputValidator } from './private/validation'; import { IProduct } from './product'; import { CfnPortfolio, CfnPortfolioPrincipalAssociation, CfnPortfolioShare } from './servicecatalog.generated'; +import { TagOptions } from './tag-options'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -79,6 +80,13 @@ export interface IPortfolio extends cdk.IResource { */ addProduct(product: IProduct): void; + /** + * Associate Tag Options. + * A TagOption is a key-value pair managed in AWS Service Catalog. + * It is not an AWS tag, but serves as a template for creating an AWS tag based on the TagOption. + */ + associateTagOptions(tagOptions: TagOptions): void; + /** * Add a Resource Update Constraint. */ @@ -116,6 +124,10 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio { }); } + public associateTagOptions(tagOptions: TagOptions) { + AssociationManager.associateTagOptions(this, tagOptions); + } + public constrainTagUpdates(product: IProduct, options: TagUpdateConstraintOptions = {}): void { AssociationManager.constrainTagUpdates(this, product, options); } @@ -170,6 +182,13 @@ export interface PortfolioProps { * @default - No description provided */ readonly description?: string; + + /** + * TagOptions associated directly on portfolio + * + * @default - No tagOptions provided + */ + readonly tagOptions?: TagOptions } /** @@ -226,6 +245,9 @@ export class Portfolio extends PortfolioBase { resource: 'portfolio', resourceName: this.portfolioId, }); + if (props.tagOptions !== undefined) { + this.associateTagOptions(props.tagOptions); + } } protected generateUniqueHash(value: string): string { diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts index 5a163073b29a1..6ba7e6ebb8eb0 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts @@ -2,7 +2,8 @@ import * as cdk from '@aws-cdk/core'; import { TagUpdateConstraintOptions } from '../constraints'; import { IPortfolio } from '../portfolio'; import { IProduct } from '../product'; -import { CfnPortfolioProductAssociation, CfnResourceUpdateConstraint } from '../servicecatalog.generated'; +import { CfnPortfolioProductAssociation, CfnResourceUpdateConstraint, CfnTagOption, CfnTagOptionAssociation } from '../servicecatalog.generated'; +import { TagOptions } from '../tag-options'; import { hashValues } from './util'; import { InputValidator } from './validation'; @@ -48,6 +49,34 @@ export class AssociationManager { } } + public static associateTagOptions(portfolio: IPortfolio, tagOptions: TagOptions): void { + const portfolioStack = cdk.Stack.of(portfolio); + for (const [key, tagOptionsList] of Object.entries(tagOptions.tagOptionsMap)) { + InputValidator.validateLength(portfolio.node.addr, 'TagOption key', 1, 128, key); + tagOptionsList.forEach((value: string) => { + InputValidator.validateLength(portfolio.node.addr, 'TagOption value', 1, 256, value); + const tagOptionKey = hashValues(key, value, portfolioStack.node.addr); + const tagOptionConstructId = `TagOption${tagOptionKey}`; + let cfnTagOption = portfolioStack.node.tryFindChild(tagOptionConstructId) as CfnTagOption; + if (!cfnTagOption) { + cfnTagOption = new CfnTagOption(portfolioStack, tagOptionConstructId, { + key: key, + value: value, + active: true, + }); + } + const tagAssocationKey = hashValues(key, value, portfolio.node.addr); + const tagAssocationConstructId = `TagOptionAssociation${tagAssocationKey}`; + if (!portfolio.node.tryFindChild(tagAssocationConstructId)) { + new CfnTagOptionAssociation(portfolio as unknown as cdk.Resource, tagAssocationConstructId, { + resourceId: portfolio.portfolioId, + tagOptionId: cfnTagOption.ref, + }); + } + }); + }; + } + private static prettyPrintAssociation(portfolio: IPortfolio, product: IProduct): string { return `- Portfolio: ${portfolio.node.path} | Product: ${product.node.path}`; } diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts b/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts new file mode 100644 index 0000000000000..808ea78add4a3 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/lib/tag-options.ts @@ -0,0 +1,14 @@ +/** + * Defines a Tag Option, which are similar to tags + * but have multiple values per key. + */ +export class TagOptions { + /** + * List of CfnTagOption + */ + public readonly tagOptionsMap: { [key: string]: string[] }; + + constructor(tagOptionsMap: { [key: string]: string[]} ) { + this.tagOptionsMap = { ...tagOptionsMap }; + } +} diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json index 1fc70614de939..1de1c2a264218 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json @@ -74,6 +74,39 @@ "PrincipalType": "IAM" } }, + "TestPortfolioTagOptionAssociation517ba9dbaf19EA8252F0": { + "Type": "AWS::ServiceCatalog::TagOptionAssociation", + "Properties": { + "ResourceId": { + "Ref": "TestPortfolio4AC794EB" + }, + "TagOptionId": { + "Ref": "TagOptionc0d88a3c4b8b" + } + } + }, + "TestPortfolioTagOptionAssociationb38e9aae7f1bD3708991": { + "Type": "AWS::ServiceCatalog::TagOptionAssociation", + "Properties": { + "ResourceId": { + "Ref": "TestPortfolio4AC794EB" + }, + "TagOptionId": { + "Ref": "TagOption9b16df08f83d" + } + } + }, + "TestPortfolioTagOptionAssociationeeabbf0db0e3ADBF0A6D": { + "Type": "AWS::ServiceCatalog::TagOptionAssociation", + "Properties": { + "ResourceId": { + "Ref": "TestPortfolio4AC794EB" + }, + "TagOptionId": { + "Ref": "TagOptiondf34c1c83580" + } + } + }, "TestPortfolioPortfolioSharebf5b82f042508F035880": { "Type": "AWS::ServiceCatalog::PortfolioShare", "Properties": { @@ -109,6 +142,30 @@ "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" ] }, + "TagOptionc0d88a3c4b8b": { + "Type": "AWS::ServiceCatalog::TagOption", + "Properties": { + "Key": "key1", + "Value": "value1", + "Active": true + } + }, + "TagOption9b16df08f83d": { + "Type": "AWS::ServiceCatalog::TagOption", + "Properties": { + "Key": "key1", + "Value": "value2", + "Active": true + } + }, + "TagOptiondf34c1c83580": { + "Type": "AWS::ServiceCatalog::TagOption", + "Properties": { + "Key": "key2", + "Value": "value1", + "Active": true + } + }, "TestProduct7606930B": { "Type": "AWS::ServiceCatalog::CloudFormationProduct", "Properties": { diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts index d48bd9796286e..31a7cff042551 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts @@ -21,6 +21,12 @@ const portfolio = new servicecatalog.Portfolio(stack, 'TestPortfolio', { portfolio.giveAccessToRole(role); portfolio.giveAccessToGroup(group); +const tagOptions = new servicecatalog.TagOptions({ + key1: ['value1', 'value2'], + key2: ['value1'], +}); +portfolio.associateTagOptions(tagOptions); + portfolio.shareWithAccount('123456789012'); const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts index 1df07c37ae356..e21ace272c4b8 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts @@ -297,6 +297,81 @@ describe('portfolio associations and product constraints', () => { expect(stack).toCountResources('AWS::ServiceCatalog::PortfolioProductAssociation', 1); //check anyway }), + test('add tag options to portfolio', () => { + const tagOptions = new servicecatalog.TagOptions({ + key1: ['value1', 'value2'], + key2: ['value1'], + }); + + portfolio.associateTagOptions(tagOptions); + + expect(stack).toCountResources('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair + expect(stack).toHaveResource('AWS::ServiceCatalog::TagOptionAssociation'); + }), + + test('add tag options to portfolio as prop', () => { + const tagOptions = new servicecatalog.TagOptions({ + key1: ['value1', 'value2'], + key2: ['value1'], + }); + + portfolio = new servicecatalog.Portfolio(stack, 'MyPortfolioWithTag', { + displayName: 'testPortfolio', + providerName: 'testProvider', + tagOptions: tagOptions, + }); + + expect(stack).toCountResources('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair + expect(stack).toHaveResource('AWS::ServiceCatalog::TagOptionAssociation'); + }), + + test('adding identical tag options to portfolio is idempotent', () => { + const tagOptions1 = new servicecatalog.TagOptions({ + key1: ['value1', 'value2'], + key2: ['value1'], + }); + + const tagOptions2 = new servicecatalog.TagOptions({ + key1: ['value1', 'value2'], + }); + + portfolio.associateTagOptions(tagOptions1); + portfolio.associateTagOptions(tagOptions2); // If not idempotent this would fail + + expect(stack).toCountResources('AWS::ServiceCatalog::TagOption', 3); //Generates a resource for each unique key-value pair + expect(stack).toHaveResource('AWS::ServiceCatalog::TagOptionAssociation'); + }), + + test('fails to add tag options with invalid minimum key length', () => { + const tagOptions = new servicecatalog.TagOptions({ + '': ['value1', 'value2'], + 'key2': ['value1'], + }); + expect(() => { + portfolio.associateTagOptions(tagOptions); + }).toThrowError(/Invalid TagOption key for resource/); + }); + + test('fails to add tag options with invalid maxium key length', () => { + const tagOptions = new servicecatalog.TagOptions({ + ['key1'.repeat(1000)]: ['value1', 'value2'], + key2: ['value1'], + }); + expect(() => { + portfolio.associateTagOptions(tagOptions); + }).toThrowError(/Invalid TagOption key for resource/); + }), + + test('fails to add tag options with invalid value length', () => { + const tagOptions = new servicecatalog.TagOptions({ + key1: ['value1'.repeat(1000), 'value2'], + key2: ['value1'], + }); + expect(() => { + portfolio.associateTagOptions(tagOptions); + }).toThrowError(/Invalid TagOption value for resource/); + }), + test('add tag update constraint', () => { portfolio.addProduct(product); portfolio.constrainTagUpdates(product, { From efd2e68d9799f0827c542a562f3c17a5a0dbdee1 Mon Sep 17 00:00:00 2001 From: Bryan Pan Date: Mon, 19 Jul 2021 16:00:58 -0700 Subject: [PATCH 075/105] feat(appsync): optional operation parameter for lambdaRequest mapping template (#15283) **[CORE CHANGES]** Add optional `operation` parameter to `lamdaRequest` mapping template. - Defaults to `"Invoke"` - Allows for `"BatchInvoke"` operations directly through the static `lambdaRequest` function **[MISC]** * Add integration test w/ a verification script to test mapping template * preliminary mapping template unit tests (created an issue to create more testing #15274 * Use `path.resolve()` to resolve testing integration test in different directories Fixes: #14079 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appsync/.gitignore | 2 +- .../aws-appsync/lib/mapping-template.ts | 5 +- .../aws-appsync/test/appsync-lambda.test.ts | 4 +- .../test/appsync-mapping-template.test.ts | 68 ++++ .../test/appsync-object-type.test.ts | 1 - .../aws-appsync/test/appsync.lambda.graphql | 24 ++ .../test/integ.appsync-lambda.expected.json | 290 ++++++++++++++++++ .../aws-appsync/test/integ.appsync-lambda.ts | 78 +++++ .../aws-appsync/test/integ.graphql-iam.ts | 4 +- .../test/verify.integ.appsync-lambda.sh | 32 ++ .../test/verify/{ => iam-query}/iam-query.js | 0 .../verify/lambda-tutorial/lambda-tutorial.js | 69 +++++ 12 files changed, 569 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/aws-appsync/test/appsync-mapping-template.test.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/appsync.lambda.graphql create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.appsync-lambda.expected.json create mode 100644 packages/@aws-cdk/aws-appsync/test/integ.appsync-lambda.ts create mode 100644 packages/@aws-cdk/aws-appsync/test/verify.integ.appsync-lambda.sh rename packages/@aws-cdk/aws-appsync/test/verify/{ => iam-query}/iam-query.js (100%) create mode 100644 packages/@aws-cdk/aws-appsync/test/verify/lambda-tutorial/lambda-tutorial.js diff --git a/packages/@aws-cdk/aws-appsync/.gitignore b/packages/@aws-cdk/aws-appsync/.gitignore index e0a8bac32ac3f..7f6bb7ab48ff1 100644 --- a/packages/@aws-cdk/aws-appsync/.gitignore +++ b/packages/@aws-cdk/aws-appsync/.gitignore @@ -16,5 +16,5 @@ nyc.config.js !.eslintrc.js !jest.config.js -!test/verify/*.js +!test/verify/**/*.js junit.xml diff --git a/packages/@aws-cdk/aws-appsync/lib/mapping-template.ts b/packages/@aws-cdk/aws-appsync/lib/mapping-template.ts index 91e9162e7900c..09dcf53d8c775 100644 --- a/packages/@aws-cdk/aws-appsync/lib/mapping-template.ts +++ b/packages/@aws-cdk/aws-appsync/lib/mapping-template.ts @@ -91,9 +91,10 @@ export abstract class MappingTemplate { * * @param payload the VTL template snippet of the payload to send to the lambda. * If no payload is provided all available context fields are sent to the Lambda function + * @param operation the type of operation AppSync should perform on the data source */ - public static lambdaRequest(payload: string = '$util.toJson($ctx)'): MappingTemplate { - return this.fromString(`{"version": "2017-02-28", "operation": "Invoke", "payload": ${payload}}`); + public static lambdaRequest(payload: string = '$util.toJson($ctx)', operation: string = 'Invoke'): MappingTemplate { + return this.fromString(`{"version": "2017-02-28", "operation": "${operation}", "payload": ${payload}}`); } /** diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts index 7ad869dc6cdd6..55c2ec0f132eb 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-lambda.test.ts @@ -20,7 +20,7 @@ describe('Lambda Data Source configuration', () => { let func: lambda.Function; beforeEach(() => { func = new lambda.Function(stack, 'func', { - code: lambda.Code.fromAsset('test/verify'), + code: lambda.Code.fromAsset(path.join(__dirname, 'verify/iam-query')), handler: 'iam-query.handler', runtime: lambda.Runtime.NODEJS_12_X, }); @@ -94,7 +94,7 @@ describe('adding lambda data source from imported api', () => { let func: lambda.Function; beforeEach(() => { func = new lambda.Function(stack, 'func', { - code: lambda.Code.fromAsset('test/verify'), + code: lambda.Code.fromAsset(path.join(__dirname, 'verify/iam-query')), handler: 'iam-query.handler', runtime: lambda.Runtime.NODEJS_12_X, }); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-mapping-template.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-mapping-template.test.ts new file mode 100644 index 0000000000000..d19b07d196b21 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-mapping-template.test.ts @@ -0,0 +1,68 @@ +import '@aws-cdk/assert-internal/jest'; +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; + +let stack: cdk.Stack; +let api: appsync.GraphqlApi; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.lambda.graphql')), + }); +}); + +describe('Lambda Mapping Templates', () => { + // GIVEN + let func: lambda.Function; + const invokeMT = '{"version": "2017-02-28", "operation": "Invoke", "payload": $util.toJson($ctx)}'; + const batchMT = '{"version": "2017-02-28", "operation": "BatchInvoke", "payload": $util.toJson($ctx)}'; + + beforeEach(() => { + func = new lambda.Function(stack, 'func', { + code: lambda.Code.fromAsset(path.join(__dirname, 'verify/lambda-tutorial')), + handler: 'lambda-tutorial.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + }); + + test('Lambda request default operation is "Invoke"', () => { + // WHEN + const lambdaDS = api.addLambdaDataSource('LambdaDS', func); + + lambdaDS.createResolver({ + typeName: 'Query', + fieldName: 'allPosts', + requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(), + responseMappingTemplate: appsync.MappingTemplate.lambdaResult(), + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::Resolver', { + FieldName: 'allPosts', + RequestMappingTemplate: invokeMT, + }); + }); + + test('Lambda request supports "BatchInvoke" through custom operation', () => { + // WHEN + const lambdaDS = api.addLambdaDataSource('LambdaDS', func); + + lambdaDS.createResolver({ + typeName: 'Post', + fieldName: 'relatedPosts', + requestMappingTemplate: appsync.MappingTemplate.lambdaRequest('$util.toJson($ctx)', 'BatchInvoke'), + responseMappingTemplate: appsync.MappingTemplate.lambdaResult(), + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::Resolver', { + FieldName: 'relatedPosts', + RequestMappingTemplate: batchMT, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts index f0517b2c0c24e..03e2bb411bbe9 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts @@ -196,7 +196,6 @@ describe('testing Object Type properties', () => { }, }); test.addField({ fieldName: 'resolve', field }); - // test.addField('resolve', field); test.addField({ fieldName: 'dynamic', field: t.string }); api.addType(test); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.lambda.graphql b/packages/@aws-cdk/aws-appsync/test/appsync.lambda.graphql new file mode 100644 index 0000000000000..0d7af74efd581 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync.lambda.graphql @@ -0,0 +1,24 @@ +schema { + query: Query + mutation: Mutation +} + +type Query { + getPost(id:ID!): Post + allPosts: [Post] +} + +type Mutation { + addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! +} + +type Post { + id: ID! + author: String! + title: String + content: String + url: String + ups: Int + downs: Int + relatedPosts: [Post] +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.appsync-lambda.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.appsync-lambda.expected.json new file mode 100644 index 0000000000000..f4bd20a97d90e --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.appsync-lambda.expected.json @@ -0,0 +1,290 @@ +{ + "Resources": { + "LambdaAPID6A5A92B": { + "Type": "AWS::AppSync::GraphQLApi", + "Properties": { + "AuthenticationType": "API_KEY", + "Name": "LambdaAPI" + } + }, + "LambdaAPISchemaCC5CA3D2": { + "Type": "AWS::AppSync::GraphQLSchema", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "LambdaAPID6A5A92B", + "ApiId" + ] + }, + "Definition": "schema {\n query: Query\n mutation: Mutation\n}\n\ntype Query {\n getPost(id:ID!): Post\n allPosts: [Post]\n}\n\ntype Mutation {\n addPost(id: ID!, author: String!, title: String, content: String, url: String): Post!\n}\n\ntype Post {\n id: ID!\n author: String!\n title: String\n content: String\n url: String\n ups: Int\n downs: Int\n relatedPosts: [Post]\n}" + } + }, + "LambdaAPIDefaultApiKey15F6897D": { + "Type": "AWS::AppSync::ApiKey", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "LambdaAPID6A5A92B", + "ApiId" + ] + } + }, + "DependsOn": [ + "LambdaAPISchemaCC5CA3D2" + ] + }, + "LambdaAPILambdaDSServiceRole21CACDF8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "LambdaAPILambdaDSServiceRoleDefaultPolicyFB1B9CE8": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "funcC3A0C2E2", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaAPILambdaDSServiceRoleDefaultPolicyFB1B9CE8", + "Roles": [ + { + "Ref": "LambdaAPILambdaDSServiceRole21CACDF8" + } + ] + } + }, + "LambdaAPILambdaDSFD6DF39B": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "LambdaAPID6A5A92B", + "ApiId" + ] + }, + "Name": "LambdaDS", + "Type": "AWS_LAMBDA", + "LambdaConfig": { + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "funcC3A0C2E2", + "Arn" + ] + } + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "LambdaAPILambdaDSServiceRole21CACDF8", + "Arn" + ] + } + } + }, + "LambdaAPILambdaDSQuerygetPostResolver12F6EC71": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "LambdaAPID6A5A92B", + "ApiId" + ] + }, + "FieldName": "getPost", + "TypeName": "Query", + "DataSourceName": "LambdaDS", + "Kind": "UNIT", + "RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"Invoke\", \"payload\": { \"field\": \"getPost\", \"arguments\": $utils.toJson($context.arguments)}}", + "ResponseMappingTemplate": "$util.toJson($ctx.result)" + }, + "DependsOn": [ + "LambdaAPILambdaDSFD6DF39B", + "LambdaAPISchemaCC5CA3D2" + ] + }, + "LambdaAPILambdaDSQueryallPostsResolver8247596A": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "LambdaAPID6A5A92B", + "ApiId" + ] + }, + "FieldName": "allPosts", + "TypeName": "Query", + "DataSourceName": "LambdaDS", + "Kind": "UNIT", + "RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"Invoke\", \"payload\": { \"field\": \"allPosts\"}}", + "ResponseMappingTemplate": "$util.toJson($ctx.result)" + }, + "DependsOn": [ + "LambdaAPILambdaDSFD6DF39B", + "LambdaAPISchemaCC5CA3D2" + ] + }, + "LambdaAPILambdaDSMutationaddPostResolverDACB9777": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "LambdaAPID6A5A92B", + "ApiId" + ] + }, + "FieldName": "addPost", + "TypeName": "Mutation", + "DataSourceName": "LambdaDS", + "Kind": "UNIT", + "RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"Invoke\", \"payload\": { \"field\": \"addPost\", \"arguments\": $utils.toJson($context.arguments)}}", + "ResponseMappingTemplate": "$util.toJson($ctx.result)" + }, + "DependsOn": [ + "LambdaAPILambdaDSFD6DF39B", + "LambdaAPISchemaCC5CA3D2" + ] + }, + "LambdaAPILambdaDSPostrelatedPostsResolverDE1B941A": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "LambdaAPID6A5A92B", + "ApiId" + ] + }, + "FieldName": "relatedPosts", + "TypeName": "Post", + "DataSourceName": "LambdaDS", + "Kind": "UNIT", + "RequestMappingTemplate": "{\"version\": \"2017-02-28\", \"operation\": \"BatchInvoke\", \"payload\": { \"field\": \"relatedPosts\", \"source\": $utils.toJson($context.source)}}", + "ResponseMappingTemplate": "$util.toJson($ctx.result)" + }, + "DependsOn": [ + "LambdaAPILambdaDSFD6DF39B", + "LambdaAPISchemaCC5CA3D2" + ] + }, + "funcServiceRoleA96CCB44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "funcC3A0C2E2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73S3Bucket096530A6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73S3VersionKeyCF8C25D4" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73S3VersionKeyCF8C25D4" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "funcServiceRoleA96CCB44", + "Arn" + ] + }, + "Handler": "lambda-tutorial.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "funcServiceRoleA96CCB44" + ] + } + }, + "Parameters": { + "AssetParameters8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73S3Bucket096530A6": { + "Type": "String", + "Description": "S3 bucket for asset \"8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73\"" + }, + "AssetParameters8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73S3VersionKeyCF8C25D4": { + "Type": "String", + "Description": "S3 key for asset version \"8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73\"" + }, + "AssetParameters8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73ArtifactHash60312028": { + "Type": "String", + "Description": "Artifact hash for asset \"8deae95d7935a4885bfc15a9af9fa85591ad93f3353790f242fca60881a6ec73\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.appsync-lambda.ts b/packages/@aws-cdk/aws-appsync/test/integ.appsync-lambda.ts new file mode 100644 index 0000000000000..2ddf3df8af6db --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/integ.appsync-lambda.ts @@ -0,0 +1,78 @@ +/// !cdk-integ * + +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; + +/* + * Creates an Appsync GraphQL API and schema in a code-first approach. + * + * Stack verification steps: + * Deploy stack, get api key and endpoinScalarType. Check if schema connects to data source. + * + * -- bash verify.integ.appsync-lambda.sh --start -- start -- + * -- aws appsync list-graphql-apis -- obtain apiId & endpoint -- + * -- aws appsync list-api-keys --api-id [apiId] -- obtain api key -- + * -- bash verify.integ.appsync-lambda.sh --check [apiKey] [url] -- check if success -- + * -- bash verify.integ.appsync-lambda.sh --clean -- clean -- + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'stack'); + +const api = new appsync.GraphqlApi(stack, 'LambdaAPI', { + name: 'LambdaAPI', + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.lambda.graphql')), +}); + +const func = new lambda.Function(stack, 'func', { + code: lambda.Code.fromAsset(path.join(__dirname, 'verify/lambda-tutorial')), + handler: 'lambda-tutorial.handler', + runtime: lambda.Runtime.NODEJS_12_X, +}); + +const lambdaDS = api.addLambdaDataSource('LambdaDS', func); + +const requestPayload = (field: string, { withArgs = false, withSource = false }) => { + const _field = `"field": "${field}"`; + const _args = '"arguments": $utils.toJson($context.arguments)'; + const _source = '"source": $utils.toJson($context.source)'; + + const _payload = [_field]; + if (withArgs) _payload.push(_args); + if (withSource) _payload.push(_source); + + return _payload.reduce((acc, v) => `${acc} ${v},`, '{').slice(0, -1) + '}'; +}; +const responseMappingTemplate = appsync.MappingTemplate.lambdaResult(); + +lambdaDS.createResolver({ + typeName: 'Query', + fieldName: 'getPost', + requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(requestPayload('getPost', { withArgs: true })), + responseMappingTemplate, +}); + +lambdaDS.createResolver({ + typeName: 'Query', + fieldName: 'allPosts', + requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(requestPayload('allPosts', {})), + responseMappingTemplate, +}); + +lambdaDS.createResolver({ + typeName: 'Mutation', + fieldName: 'addPost', + requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(requestPayload('addPost', { withArgs: true })), + responseMappingTemplate, +}); +lambdaDS.createResolver({ + typeName: 'Post', + fieldName: 'relatedPosts', + requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(requestPayload('relatedPosts', { withSource: true }), 'BatchInvoke'), + responseMappingTemplate, +}); + + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts index 185fc8ef3e729..04ab82fe3a491 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-iam.ts @@ -95,14 +95,14 @@ api.grant(lambdaIAM, IamResource.ofType('test'), 'appsync:GraphQL'); api.grantMutation(lambdaIAM, 'addTest'); new Function(stack, 'testQuery', { - code: Code.fromAsset('verify'), + code: Code.fromAsset(join(__dirname, 'verify/iam-query')), handler: 'iam-query.handler', runtime: Runtime.NODEJS_12_X, environment: { APPSYNC_ENDPOINT: api.graphqlUrl }, role: lambdaIAM, }); new Function(stack, 'testFail', { - code: Code.fromAsset('verify'), + code: Code.fromAsset(join(__dirname, 'verify/iam-query')), handler: 'iam-query.handler', runtime: Runtime.NODEJS_12_X, environment: { APPSYNC_ENDPOINT: api.graphqlUrl }, diff --git a/packages/@aws-cdk/aws-appsync/test/verify.integ.appsync-lambda.sh b/packages/@aws-cdk/aws-appsync/test/verify.integ.appsync-lambda.sh new file mode 100644 index 0000000000000..15fe8fc5e4b93 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/verify.integ.appsync-lambda.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +function error { + printf "\e[91;5;81m$@\e[0m\n" +} + +function usage { + echo "##################################################################################" + echo "# run 'verify.integ.appsync-lambda.sh --start' to deploy #" + echo "# run 'verify.integ.appsync-lambda.sh --check [APIKEY] [ENDPOINT]' to run check #" + echo "# run 'verify.integ.appsync-lambda.sh --clean' to clean up stack #" + echo "##################################################################################" +} + +if [[ "$1" == "--start" ]]; then + cdk deploy --app "node integ.appsync-lambda.js" +elif [[ "$1" == "--check" ]]; then + if [[ -z $2 || -z $3 ]]; then + error "Error: --check flag requires [APIKEY] [ENDPOINT]" + usage + exit 1 + fi + echo THIS TEST SHOULD PRODUCE A LIST OF BOOKS + curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:$2" -d '{ "query": "query { allPosts { id author title relatedPosts { id title } } }" }" }' $3 | json_pp + echo "" +elif [[ "$1" == "--clean" ]];then + cdk destroy --app "node integ.appsync-lambda.js" +else + error "Error: use flags --start, --check, --clean" + usage + exit 1 +fi \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/verify/iam-query.js b/packages/@aws-cdk/aws-appsync/test/verify/iam-query/iam-query.js similarity index 100% rename from packages/@aws-cdk/aws-appsync/test/verify/iam-query.js rename to packages/@aws-cdk/aws-appsync/test/verify/iam-query/iam-query.js diff --git a/packages/@aws-cdk/aws-appsync/test/verify/lambda-tutorial/lambda-tutorial.js b/packages/@aws-cdk/aws-appsync/test/verify/lambda-tutorial/lambda-tutorial.js new file mode 100644 index 0000000000000..15f072857fc7e --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/verify/lambda-tutorial/lambda-tutorial.js @@ -0,0 +1,69 @@ +exports.handler = (event, context, callback) => { + console.log("Received event {}", JSON.stringify(event, 3)); + var posts = { + "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, + "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, + "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, + "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, + "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; + + var relatedPosts = { + "1": [posts['4']], + "2": [posts['3'], posts['5']], + "3": [posts['2'], posts['1']], + "4": [posts['2'], posts['1']], + "5": [] + }; + const isBatch = Array.isArray(event); + if (isBatch) { + console.log("Got an BatchInvoke Request. The payload has %d items to resolve.", event.length); + const field = event[0].field; + switch(field) { + case "relatedPosts": + var results = []; + // the response MUST contain the same number + // of entries as the payload array + for (var i=0; i< event.length; i++) { + console.log("post {}", JSON.stringify(event[i].source)); + results.push(relatedPosts[event[i].source.id]); + } + console.log("results {}", JSON.stringify(results)); + callback(null, results); + break; + default: + callback("Unknown field, unable to resolve" + field, null); + break; + } + } + else { + console.log("Got an Invoke Request."); + switch(event.field) { + case "getPost": + var id = event.arguments.id; + callback(null, posts[id]); + break; + case "allPosts": + var values = []; + for(var d in posts){ + values.push(posts[d]); + } + callback(null, values); + break; + case "addPost": + // return the arguments back + callback(null, event.arguments); + break; + case "addPostErrorWithData": + var id = event.arguments.id; + var result = posts[id]; + // attached additional error information to the post + result.errorMessage = 'Error with the mutation, data has changed'; + result.errorType = 'MUTATION_ERROR'; + callback(null, result); + break; + default: + callback("Unknown field, unable to resolve" + event.field, null); + break; + } + } +}; \ No newline at end of file From f7c628948e7f71df7a95cb00cdc2746e2e46dc03 Mon Sep 17 00:00:00 2001 From: Calvin Combs <87042526+calvin-cc@users.noreply.github.com> Date: Mon, 19 Jul 2021 16:39:10 -0700 Subject: [PATCH 076/105] feat(rds): allow setting copyTagsToSnapshot on Clusters (#15553) Adds support for the `copyTagsToSnapshot` property. Closes #15521 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/README.md | 1 + packages/@aws-cdk/aws-rds/lib/cluster.ts | 8 +++ .../@aws-cdk/aws-rds/test/cluster.test.ts | 60 +++++++++++++++++++ .../integ.cluster-rotation.lit.expected.json | 1 + .../test/integ.cluster-s3.expected.json | 1 + .../aws-rds/test/integ.cluster.expected.json | 1 + 6 files changed, 72 insertions(+) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index c336f5c7c240e..c604a2ad5bce8 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -157,6 +157,7 @@ new rds.DatabaseCluster(stack, 'DatabaseCluster', { subnetType: ec2.SubnetType.PRIVATE, }, publiclyAccessible: true, + copyTagsToSnapshot: true, // whether to save the cluster tags when creating the snapshot. Default is 'true' }, }); ``` diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index faedab239f82f..ad5a2201c759c 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -460,6 +460,13 @@ export interface DatabaseClusterProps extends DatabaseClusterBaseProps { * @default - if storageEncrypted is true then the default master key, no key otherwise */ readonly storageEncryptionKey?: kms.IKey; + + /** + * Whether to copy tags to the snapshot when a snapshot is created. + * + * @default: true + */ + readonly copyTagsToSnapshot?: boolean; } /** @@ -513,6 +520,7 @@ export class DatabaseCluster extends DatabaseClusterNew { // Encryption kmsKeyId: props.storageEncryptionKey?.keyArn, storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, + copyTagsToSnapshot: props.copyTagsToSnapshot ?? true, }); this.clusterIdentifier = cluster.ref; diff --git a/packages/@aws-cdk/aws-rds/test/cluster.test.ts b/packages/@aws-cdk/aws-rds/test/cluster.test.ts index 9c743b811c818..4e9de20a9fa5d 100644 --- a/packages/@aws-cdk/aws-rds/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-rds/test/cluster.test.ts @@ -42,6 +42,7 @@ describe('cluster', () => { MasterUserPassword: 'tooshort', VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], EnableIAMDatabaseAuthentication: true, + CopyTagsToSnapshot: true, }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', @@ -1948,6 +1949,65 @@ describe('cluster', () => { DBClusterIdentifier: clusterIdentifier, }); }); + + test('cluster with copyTagsToSnapshot default', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA, + instanceProps: { + vpc, + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + CopyTagsToSnapshot: true, + }); + }); + + test('cluster with copyTagsToSnapshot disabled', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA, + instanceProps: { + vpc, + }, + copyTagsToSnapshot: false, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + CopyTagsToSnapshot: false, + }); + }); + + test('cluster with copyTagsToSnapshot enabled', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA, + copyTagsToSnapshot: true, + instanceProps: { + vpc, + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + CopyTagsToSnapshot: true, + }); + }); }); test.each([ diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json index 6d8c335221156..91b189023f587 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json @@ -670,6 +670,7 @@ "Type": "AWS::RDS::DBCluster", "Properties": { "Engine": "aurora", + "CopyTagsToSnapshot": true, "DBSubnetGroupName": { "Ref": "DatabaseSubnets56F17B9A" }, diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json index 3ee9358bebc33..a2631bd5991c8 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json @@ -610,6 +610,7 @@ "Type": "AWS::RDS::DBCluster", "Properties": { "Engine": "aurora", + "CopyTagsToSnapshot": true, "AssociatedRoles": [ { "RoleArn": { diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json index 04a2157aba0a8..553bcf398af92 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json @@ -460,6 +460,7 @@ "Type": "AWS::RDS::DBCluster", "Properties": { "Engine": "aurora", + "CopyTagsToSnapshot": true, "DBClusterParameterGroupName": { "Ref": "ParamsA8366201" }, From b934976f057cd395de660dc4099e2303415cdc78 Mon Sep 17 00:00:00 2001 From: haimlit Date: Tue, 20 Jul 2021 09:22:23 +0300 Subject: [PATCH 077/105] feat(lambda-nodejs): source map mode (#15621) Addresses #14857 by adding source map configuration options. Current implementation preferred preserving backwards compatibility with `sourceMap` boolean flag. See issue discussion for an alternative. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda-nodejs/README.md | 1 + .../aws-lambda-nodejs/lib/bundling.ts | 12 ++- .../@aws-cdk/aws-lambda-nodejs/lib/types.ts | 34 +++++++ .../aws-lambda-nodejs/test/bundling.test.ts | 89 ++++++++++++++++++- .../test/integ.function.expected.json | 54 +++++------ .../aws-lambda-nodejs/test/integ.function.ts | 6 +- 6 files changed, 165 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index b792f125c2c10..22347b28b1dd9 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -172,6 +172,7 @@ new lambda.NodejsFunction(this, 'my-handler', { bundling: { minify: true, // minify code, defaults to false sourceMap: true, // include source map, defaults to false + sourceMapMode: SourceMapMode.INLINE, // defaults to SourceMapMode.DEFAULT target: 'es2020', // target environment for the generated JavaScript code loader: { // Use the 'dataurl' loader for '.png' files '.png': 'dataurl', diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index ba447bab17e29..3c33ad74f2471 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -4,7 +4,7 @@ import { AssetCode, Code, Runtime } from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import { EsbuildInstallation } from './esbuild-installation'; import { PackageManager } from './package-manager'; -import { BundlingOptions } from './types'; +import { BundlingOptions, SourceMapMode } from './types'; import { exec, extractDependencies, findUp } from './util'; const ESBUILD_MAJOR_VERSION = '0'; @@ -126,6 +126,14 @@ export class Bundling implements cdk.BundlingOptions { const loaders = Object.entries(this.props.loader ?? {}); const defines = Object.entries(this.props.define ?? {}); + if (this.props.sourceMap === false && this.props.sourceMapMode) { + throw new Error('sourceMapMode cannot be used when sourceMap is false'); + } + // eslint-disable-next-line no-console + const sourceMapEnabled = this.props.sourceMapMode ?? this.props.sourceMap; + const sourceMapMode = this.props.sourceMapMode ?? SourceMapMode.DEFAULT; + const sourceMapValue = sourceMapMode === SourceMapMode.DEFAULT ? '' : `=${this.props.sourceMapMode}`; + const esbuildCommand: string[] = [ options.esbuildRunner, '--bundle', `"${pathJoin(options.inputDir, this.relativeEntryPath)}"`, @@ -133,7 +141,7 @@ export class Bundling implements cdk.BundlingOptions { '--platform=node', `--outfile="${pathJoin(options.outputDir, 'index.js')}"`, ...this.props.minify ? ['--minify'] : [], - ...this.props.sourceMap ? ['--sourcemap'] : [], + ...sourceMapEnabled ? [`--sourcemap${sourceMapValue}`] : [], ...this.externals.map(external => `--external:${external}`), ...loaders.map(([ext, name]) => `--loader:${ext}=${name}`), ...defines.map(([key, value]) => `--define:${key}=${JSON.stringify(value)}`), diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts index 6752d8ca725a1..8d0d263fe64e6 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts @@ -18,6 +18,14 @@ export interface BundlingOptions { */ readonly sourceMap?: boolean; + /** + * Source map mode to be used when bundling. + * @see https://esbuild.github.io/api/#sourcemap + * + * @default SourceMapMode.DEFAULT + */ + readonly sourceMapMode?: SourceMapMode; + /** * Target environment for the generated JavaScript code. * @@ -266,3 +274,29 @@ export enum LogLevel { /** Show nothing */ SILENT = 'silent', } + + +/** + * SourceMap mode for esbuild + * @see https://esbuild.github.io/api/#sourcemap + */ +export enum SourceMapMode { + /** + * Default sourceMap mode - will generate a .js.map file alongside any generated .js file and add a special //# sourceMappingURL= + * comment to the bottom of the .js file pointing to the .js.map file + */ + DEFAULT = 'default', + /** + * External sourceMap mode - If you want to omit the special //# sourceMappingURL= comment from the generated .js file but you still + * want to generate the .js.map files + */ + EXTERNAL = 'external', + /** + * Inline sourceMap mode - If you want to insert the entire source map into the .js file instead of generating a separate .js.map file + */ + INLINE = 'inline', + /** + * Both sourceMap mode - If you want to have the effect of both inline and external simultaneously + */ + BOTH = 'both' +} diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index 3cd9f5c5084b4..a38a6fa08d5bc 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -6,7 +6,7 @@ import { AssetHashType, DockerImage } from '@aws-cdk/core'; import { version as delayVersion } from 'delay/package.json'; import { Bundling } from '../lib/bundling'; import { EsbuildInstallation } from '../lib/esbuild-installation'; -import { LogLevel } from '../lib/types'; +import { LogLevel, SourceMapMode } from '../lib/types'; import * as util from '../lib/util'; jest.mock('@aws-cdk/aws-lambda'); @@ -218,6 +218,93 @@ test('esbuild bundling with esbuild options', () => { expect(bundleProcess.stdout.toString()).toMatchSnapshot(); }); +test('esbuild bundling source map default', () => { + Bundling.bundle({ + entry, + projectRoot, + depsLockFilePath, + runtime: Runtime.NODEJS_14_X, + sourceMap: true, + sourceMapMode: SourceMapMode.DEFAULT, + }); + + // Correctly bundles with esbuild + expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(depsLockFilePath), { + assetHashType: AssetHashType.OUTPUT, + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + [ + 'esbuild --bundle "/asset-input/lib/handler.ts" --target=node14 --platform=node --outfile="/asset-output/index.js"', + '--sourcemap --external:aws-sdk', + ].join(' '), + ], + }), + }); +}); + +test('esbuild bundling source map inline', () => { + Bundling.bundle({ + entry, + projectRoot, + depsLockFilePath, + runtime: Runtime.NODEJS_14_X, + sourceMap: true, + sourceMapMode: SourceMapMode.INLINE, + }); + + // Correctly bundles with esbuild + expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(depsLockFilePath), { + assetHashType: AssetHashType.OUTPUT, + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + [ + 'esbuild --bundle "/asset-input/lib/handler.ts" --target=node14 --platform=node --outfile="/asset-output/index.js"', + '--sourcemap=inline --external:aws-sdk', + ].join(' '), + ], + }), + }); +}); + +test('esbuild bundling source map enabled when only source map mode exists', () => { + Bundling.bundle({ + entry, + projectRoot, + depsLockFilePath, + runtime: Runtime.NODEJS_14_X, + sourceMapMode: SourceMapMode.INLINE, + }); + + // Correctly bundles with esbuild + expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(depsLockFilePath), { + assetHashType: AssetHashType.OUTPUT, + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + [ + 'esbuild --bundle "/asset-input/lib/handler.ts" --target=node14 --platform=node --outfile="/asset-output/index.js"', + '--sourcemap=inline --external:aws-sdk', + ].join(' '), + ], + }), + }); +}); + +test('esbuild bundling throws when sourceMapMode used with false sourceMap', () => { + expect(() => { + Bundling.bundle({ + entry, + projectRoot, + depsLockFilePath, + runtime: Runtime.NODEJS_14_X, + sourceMap: false, + sourceMapMode: SourceMapMode.INLINE, + }); + }).toThrow('sourceMapMode cannot be used when sourceMap is false'); +}); + test('Detects yarn.lock', () => { const yarnLock = path.join(__dirname, '..', 'yarn.lock'); Bundling.bundle({ diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/integ.function.expected.json b/packages/@aws-cdk/aws-lambda-nodejs/test/integ.function.expected.json index 061ad0bc939a7..f976b83648db4 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/integ.function.expected.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/integ.function.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterse693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031S3Bucket29060E55" + "Ref": "AssetParameters790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1S3Bucket1B1D9794" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031S3VersionKeyE68A6B82" + "Ref": "AssetParameters790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1S3VersionKey720EECDB" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031S3VersionKeyE68A6B82" + "Ref": "AssetParameters790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1S3VersionKey720EECDB" } ] } @@ -126,7 +126,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39S3Bucket4A9B4410" + "Ref": "AssetParameters55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6S3Bucket95EC2A4C" }, "S3Key": { "Fn::Join": [ @@ -139,7 +139,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39S3VersionKeyA27DDFEA" + "Ref": "AssetParameters55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6S3VersionKey0EEB0B14" } ] } @@ -152,7 +152,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39S3VersionKeyA27DDFEA" + "Ref": "AssetParameters55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6S3VersionKey0EEB0B14" } ] } @@ -758,7 +758,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736cS3BucketA6D7D091" + "Ref": "AssetParameters39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfeS3Bucket6796DF76" }, "S3Key": { "Fn::Join": [ @@ -771,7 +771,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736cS3VersionKeyD716694C" + "Ref": "AssetParameters39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfeS3VersionKeyE83502D3" } ] } @@ -784,7 +784,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736cS3VersionKeyD716694C" + "Ref": "AssetParameters39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfeS3VersionKeyE83502D3" } ] } @@ -835,41 +835,41 @@ } }, "Parameters": { - "AssetParameterse693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031S3Bucket29060E55": { + "AssetParameters790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1S3Bucket1B1D9794": { "Type": "String", - "Description": "S3 bucket for asset \"e693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031\"" + "Description": "S3 bucket for asset \"790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1\"" }, - "AssetParameterse693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031S3VersionKeyE68A6B82": { + "AssetParameters790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1S3VersionKey720EECDB": { "Type": "String", - "Description": "S3 key for asset version \"e693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031\"" + "Description": "S3 key for asset version \"790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1\"" }, - "AssetParameterse693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031ArtifactHash0218547C": { + "AssetParameters790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1ArtifactHashA9293830": { "Type": "String", - "Description": "Artifact hash for asset \"e693e416c0e5591cb0eaa424f7526a449f788de8aa8a89f06f27671feaba8031\"" + "Description": "Artifact hash for asset \"790877879aeb907c349904efa092342fdc774820821fe50f7b6bf9201c2cfdf1\"" }, - "AssetParameters8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39S3Bucket4A9B4410": { + "AssetParameters55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6S3Bucket95EC2A4C": { "Type": "String", - "Description": "S3 bucket for asset \"8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39\"" + "Description": "S3 bucket for asset \"55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6\"" }, - "AssetParameters8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39S3VersionKeyA27DDFEA": { + "AssetParameters55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6S3VersionKey0EEB0B14": { "Type": "String", - "Description": "S3 key for asset version \"8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39\"" + "Description": "S3 key for asset version \"55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6\"" }, - "AssetParameters8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39ArtifactHash13E6F6BF": { + "AssetParameters55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6ArtifactHashE6098BA4": { "Type": "String", - "Description": "Artifact hash for asset \"8bda5a67feb4905ef8b67b45ee665d3e466be7357e6c361ad2aa773e5867db39\"" + "Description": "Artifact hash for asset \"55ec667368ab6d681cbdada49e45f9f8a8dd2d610a1e6c9d6b4f342adb77f3d6\"" }, - "AssetParameters9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736cS3BucketA6D7D091": { + "AssetParameters39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfeS3Bucket6796DF76": { "Type": "String", - "Description": "S3 bucket for asset \"9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736c\"" + "Description": "S3 bucket for asset \"39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfe\"" }, - "AssetParameters9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736cS3VersionKeyD716694C": { + "AssetParameters39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfeS3VersionKeyE83502D3": { "Type": "String", - "Description": "S3 key for asset version \"9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736c\"" + "Description": "S3 key for asset version \"39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfe\"" }, - "AssetParameters9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736cArtifactHashF03B1BE8": { + "AssetParameters39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfeArtifactHashB3080084": { "Type": "String", - "Description": "Artifact hash for asset \"9aa4dd3191867438d7cf78d5509ee4ffc26b9f3954f6d9a2977c478b7728736c\"" + "Description": "Artifact hash for asset \"39132cdcc42d93606e39f295123475dee67fc9051b50231400eff004dac11dfe\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/integ.function.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/integ.function.ts index f08a041178e5e..f511cf21a3d3a 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/integ.function.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/integ.function.ts @@ -13,7 +13,11 @@ class TestStack extends Stack { new lambda.NodejsFunction(this, 'ts-handler', { entry: path.join(__dirname, 'integ-handlers/ts-handler.ts'), runtime: Runtime.NODEJS_12_X, - bundling: { minify: true }, + bundling: { + minify: true, + sourceMap: true, + sourceMapMode: lambda.SourceMapMode.BOTH, + }, }); new lambda.NodejsFunction(this, 'js-handler', { From 5e3cf2b0558401fab25f75da319fac587df1bcfb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jul 2021 17:37:02 +0200 Subject: [PATCH 078/105] fix(pipelines): permissions check in legacy API does not work (#15660) We were still passing a stack name instead of a stage path, causing the check to not work correctly. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/pipelines/lib/legacy/stage.ts | 4 +- .../test/compliance/security-check.test.ts | 47 ++++++++++++++++++- .../integ.pipeline-security.expected.json | 8 ++-- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts index ee4b860848503..8f1d0e205eefc 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts @@ -5,7 +5,7 @@ import { CodeBuildAction } from '@aws-cdk/aws-codepipeline-actions'; import * as sns from '@aws-cdk/aws-sns'; import { Stage, Aspects } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { Construct } from 'constructs'; +import { Construct, Node } from 'constructs'; import { AssetType } from '../blueprint/asset-type'; import { ApplicationSecurityCheck } from '../private/application-security-check'; import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; @@ -311,7 +311,7 @@ export class CdkStage extends CoreConstruct { variablesNamespace: `${appStageName}SecurityCheck`, environmentVariables: { STAGE_PATH: { - value: this.pipelineStage.pipeline.stack.stackName, + value: Node.of(appStage).path, type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, }, STAGE_NAME: { diff --git a/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts b/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts index e698f355b8609..7367930e6618a 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts @@ -1,4 +1,4 @@ -import { arrayWith, objectLike, stringLike } from '@aws-cdk/assert-internal'; +import { anything, arrayWith, encodedJson, objectLike, stringLike } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; import { Topic } from '@aws-cdk/aws-sns'; import { Stack } from '@aws-cdk/core'; @@ -55,6 +55,51 @@ behavior('security check option generates lambda/codebuild at pipeline scope', ( } }); +behavior('security check option passes correct environment variables to check project', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(pipelineStack, 'App'), { confirmBroadeningPermissions: true }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const stage = new OneStackApp(pipelineStack, 'App'); + pipeline.addStage(stage, { + pre: [ + new cdkp.ConfirmPermissionsBroadening('Check', { + stage, + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith( + { + Name: 'App', + Actions: arrayWith( + objectLike({ + Name: stringLike('*Check'), + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { name: 'STAGE_PATH', type: 'PLAINTEXT', value: 'PipelineSecurityStack/App' }, + { name: 'STAGE_NAME', type: 'PLAINTEXT', value: 'App' }, + { name: 'ACTION_NAME', type: 'PLAINTEXT', value: anything() }, + ]), + }), + }), + ), + }, + ), + }); + } +}); + behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid permissions', (suite) => { suite.legacy(() => { const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json index 3495d780cecdd..82ad5c418fb70 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json @@ -431,7 +431,7 @@ "Fn::Join": [ "", [ - "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"UnattachedStage\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"SingleStageManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack/SingleStage\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"UnattachedStage\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"SingleStageManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", { "Ref": "SecurityChangesTopic9762A9B3" }, @@ -568,7 +568,7 @@ "Fn::Join": [ "", [ - "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProduction\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProductionManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack/PreProduction\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProduction\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProductionManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", { "Ref": "SecurityChangesTopic9762A9B3" }, @@ -627,7 +627,7 @@ "Fn::Join": [ "", [ - "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProduction\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"SafeProductionManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack/SafeProduction\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProduction\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"SafeProductionManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", { "Ref": "SecurityChangesTopic9762A9B3" }, @@ -906,7 +906,7 @@ "ProjectName": { "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" }, - "EnvironmentVariables": "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"NoSecurityCheck\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"EnableSecurityCheckManualApproval\"}]" + "EnvironmentVariables": "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack/EnableSecurityCheck\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"NoSecurityCheck\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"EnableSecurityCheckManualApproval\"}]" }, "InputArtifacts": [ { From 9532e3c44ba22dab9cf6519bc80fa6cd92e77a10 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jul 2021 18:15:34 +0200 Subject: [PATCH 079/105] docs(pipelines): describe context lookups (#15663) Describe recommended way to do context lookups, and an alternative approach we don't recommend but that everyone is asking for. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 61 +++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 9760e382e2155..b38bdef546c2b 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -840,6 +840,67 @@ and orphan the old bucket. You should manually delete the orphaned bucket after you are sure you have redeployed all CDK applications and there are no more references to the old asset bucket. +## Context Lookups + +You might be using CDK constructs that need to look up [runtime +context](https://docs.aws.amazon.com/cdk/latest/guide/context.html#context_methods), +which is information from the target AWS Account and Region the CDK needs to +synthesize CloudFormation templates appropriate for that environment. Examples +of this kind of context lookups are the number of Availability Zones available +to you, a Route53 Hosted Zone ID, or the ID of an AMI in a given region. This +information is automatically looked up when you run `cdk synth`. + +By default, a `cdk synth` performed in a pipeline will not have permissions +to perform these lookups, and the lookups will fail. This is by design. + +**Our recommended way of using lookups** is by running `cdk synth` on the +developer workstation and checking in the `cdk.context.json` file, which +contains the results of the context lookups. This will make sure your +synthesized infrastructure is consistent and repeatable. If you do not commit +`cdk.context.json`, the results of the lookups may suddenly be different in +unexpected ways, and even produce results that cannot be deployed or will cause +data loss. To give an account permissions to perform lookups against an +environment, without being able to deploy to it and make changes, run +`cdk bootstrap --trust-for-lookup=`. + +If you want to use lookups directly from the pipeline, you either need to accept +the risk of nondeterminism, or make sure you save and load the +`cdk.context.json` file somewhere between synth runs. Finally, you should +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... + commands: [ + // Commands to load cdk.context.json from somewhere here + '...', + 'npm ci', + 'npm run build', + 'npx cdk synth', + // Commands to store cdk.context.json back here + '...', + ], + rolePolicyStatements: [ + new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: ['*'], + conditions: { + StringEquals: { + 'iam:ResourceTag/aws-cdk:bootstrap-role': 'deploy', + }, + }, + }), + ], + }), +}); +``` + +The above example requires that the target environments have all +been bootstrapped with bootstrap stack version `8`, released with +CDK CLI `1.114.0`. + ## Security Considerations It's important to stay safe while employing Continuous Delivery. The CDK Pipelines From 309b9b4cf554474c87fe3d833a5205498e200ecf Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jul 2021 18:55:02 +0200 Subject: [PATCH 080/105] fix(pipelines): new pipeline stages aren't validated (#15665) We do validation-as-part-of-synth for legacy pipelines; also need to do the same for modern pipelines otherwise failing context lookups are too hard to diagnose. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/blueprint/stage-deployment.ts | 3 +- .../@aws-cdk/pipelines/lib/legacy/stage.ts | 3 +- .../lib/private/construct-internals.ts | 4 ++ .../pipelines/test/compliance/synths.test.ts | 46 ++++++++++++++++++- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts b/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts index 499d324dfb25f..b8e3f0ea77536 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts @@ -1,6 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; import { isStackArtifact } from '../private/cloud-assembly-internals'; +import { pipelineSynth } from '../private/construct-internals'; import { StackDeployment } from './stack-deployment'; import { Step } from './step'; @@ -44,7 +45,7 @@ export class StageDeployment { * in dependency order. */ public static fromStage(stage: cdk.Stage, props: StageDeploymentProps = {}) { - const assembly = stage.synth(); + const assembly = pipelineSynth(stage); if (assembly.stacks.length === 0) { // If we don't check here, a more puzzling "stage contains no actions" // error will be thrown come deployment time. diff --git a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts index 8f1d0e205eefc..3b4140fdba5ce 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts @@ -16,6 +16,7 @@ import { CdkPipeline } from './pipeline'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; +import { pipelineSynth } from '../private/construct-internals'; /** * Construction properties for a CdkStage @@ -113,7 +114,7 @@ export class CdkStage extends CoreConstruct { * publishing stage. */ public addApplication(appStage: Stage, options: AddStageOptions = {}) { - const asm = appStage.synth({ validateOnSynthesis: true }); + const asm = pipelineSynth(appStage); const extraRunOrderSpace = options.extraRunOrderSpace ?? 0; if (options.confirmBroadeningPermissions ?? this.confirmBroadeningPermissions) { diff --git a/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts b/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts index fe2ddf1953f64..34d12b84e1d8c 100644 --- a/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts +++ b/packages/@aws-cdk/pipelines/lib/private/construct-internals.ts @@ -23,6 +23,10 @@ export function assemblyBuilderOf(stage: Stage): cxapi.CloudAssemblyBuilder { return (stage as any)._assemblyBuilder; } +export function pipelineSynth(stage: Stage) { + return stage.synth({ validateOnSynthesis: true }); +} + /** * Return the relative path from the app assembly to the scope's (nested) assembly */ diff --git a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts index 70e012aad8f1d..7c47600b66155 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts @@ -9,7 +9,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; import * as cdkp from '../../lib'; import { CodeBuildStep } from '../../lib'; -import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, ModernTestGitHubNpmPipelineProps } from '../testhelpers'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, ModernTestGitHubNpmPipelineProps, OneStackApp } from '../testhelpers'; let app: TestApp; let pipelineStack: Stack; @@ -1104,4 +1104,48 @@ behavior('can provide custom BuildSpec that is merged with generated one', (suit }, }); } +}); + +behavior('stacks synthesized for pipeline will be checked during synth', (suite) => { + let stage: OneStackApp; + beforeEach(() => { + stage = new OneStackApp(pipelineStack, 'MyApp'); + }); + + suite.legacy(() => { + // WHEN + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: new cdkp.SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + installCommands: ['install1', 'install2'], + buildCommands: ['build1', 'build2'], + testCommands: ['test1', 'test2'], + synthCommand: 'cdk synth', + }), + }); + pipeline.addApplicationStage(stage); + + THEN(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + installCommands: ['install1', 'install2'], + commands: ['build1', 'build2', 'test1', 'test2', 'cdk synth'], + }); + pipeline.addStage(stage); + + THEN(); + }); + + function THEN() { + // All stacks in the ASM have been synthesized with 'validateOnSynth: true' + const asm = stage.synth(); + for (const stack of asm.stacks) { + expect(stack.validateOnSynth).toEqual(true); + } + } }); \ No newline at end of file From 645376958a699ee89e3cc805546c293c93d99613 Mon Sep 17 00:00:00 2001 From: Daniel Lindberg Date: Tue, 20 Jul 2021 19:33:58 +0200 Subject: [PATCH 081/105] feat(apigatewayv2-integrations): http private integrations - tls config (#15469) Adds the option to specify tlsConfig for `AWS::ApiGatewayV2::Integration` resources, to enable use of HTTPS when configuring private integrations. closes #14036 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/http/alb.ts | 1 + .../lib/http/base-types.ts | 10 +++++- .../lib/http/nlb.ts | 1 + .../lib/http/service-discovery.ts | 1 + .../test/http/alb.test.ts | 27 ++++++++++++++++ .../test/http/nlb.test.ts | 27 ++++++++++++++++ .../test/http/service-discovery.test.ts | 31 +++++++++++++++++++ .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 1 + .../aws-apigatewayv2/lib/http/integration.ts | 22 +++++++++++++ .../aws-apigatewayv2/test/http/route.test.ts | 5 +++ 10 files changed, 125 insertions(+), 1 deletion(-) 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 b6afd1cc76450..656e0a550408f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts @@ -43,6 +43,7 @@ export class HttpAlbIntegration extends HttpPrivateIntegration { connectionType: this.connectionType, connectionId: vpcLink.vpcLinkId, uri: this.props.listener.listenerArn, + secureServerName: this.props.secureServerName, }; } } 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 5000dfb63a751..db14e50f7fc54 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 @@ -16,4 +16,12 @@ export interface HttpPrivateIntegrationOptions { * @default HttpMethod.ANY */ readonly method?: HttpMethod; -} \ No newline at end of file + + /** + * Specifies the server name to verified by HTTPS when calling the backend integration + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-integration-tlsconfig.html + * @default undefined private integration traffic will use HTTP protocol + */ + + readonly secureServerName?: string; +} 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 85e3f3773d1c4..1c405b51b3bfd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts @@ -43,6 +43,7 @@ export class HttpNlbIntegration extends HttpPrivateIntegration { connectionType: this.connectionType, connectionId: vpcLink.vpcLinkId, uri: this.props.listener.listenerArn, + secureServerName: this.props.secureServerName, }; } } 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 44e8b148754dd..f9f204b6eba3e 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 @@ -33,6 +33,7 @@ export class HttpServiceDiscoveryIntegration extends HttpPrivateIntegration { connectionType: this.connectionType, connectionId: this.props.vpcLink.vpcLinkId, uri: this.props.service.serviceArn, + secureServerName: this.props.secureServerName, }; } } 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 cf852751692f7..95ece74cf7e93 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 @@ -116,4 +116,31 @@ describe('HttpAlbIntegration', () => { routeKey: HttpRouteKey.with('/pets'), })).toThrow(/vpcLink property must be specified/); }); + + test('tlsConfig 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, + secureServerName: 'name-to-verify', + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + TlsConfig: { + ServerNameToVerify: 'name-to-verify', + }, + }); + }); }); 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 176e511214e77..2b7120ea33582 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 @@ -113,4 +113,31 @@ describe('HttpNlbIntegration', () => { routeKey: HttpRouteKey.with('/pets'), })).toThrow(/vpcLink property must be specified/); }); + + test('tlsConfig 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, + secureServerName: 'name-to-verify', + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + TlsConfig: { + ServerNameToVerify: 'name-to-verify', + }, + }); + }); }); 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 43294f35fb12f..83d2b47252598 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 @@ -94,4 +94,35 @@ describe('HttpServiceDiscoveryIntegration', () => { routeKey: HttpRouteKey.with('/pets'), })).toThrow(/vpcLink property is mandatory/); }); + + test('tlsConfig 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, + secureServerName: 'name-to-verify', + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + TlsConfig: { + ServerNameToVerify: 'name-to-verify', + }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index f650d62bd289b..e4b6ce929eb2a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -304,6 +304,7 @@ abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that th connectionId: config.connectionId, connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, + secureServerName: config.secureServerName, }); 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 836b831550fb7..f832b5b7e3b21 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -1,3 +1,4 @@ +/* eslint-disable quotes */ import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; @@ -120,6 +121,13 @@ export interface HttpIntegrationProps { * @default - defaults to latest in the case of HttpIntegrationType.LAMBDA_PROXY`, irrelevant otherwise. */ readonly payloadFormatVersion?: PayloadFormatVersion; + + /** + * Specifies the TLS configuration for a private integration + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-integration-tlsconfig.html + * @default undefined private integration traffic will use HTTP protocol + */ + readonly secureServerName?: string; } /** @@ -142,6 +150,13 @@ export class HttpIntegration extends Resource implements IHttpIntegration { connectionType: props.connectionType, payloadFormatVersion: props.payloadFormatVersion?.version, }); + + if (props.secureServerName) { + integ.tlsConfig = { + serverNameToVerify: props.secureServerName, + }; + } + this.integrationId = integ.ref; this.httpApi = props.httpApi; } @@ -215,4 +230,11 @@ export interface HttpRouteIntegrationConfig { * @default - undefined */ readonly payloadFormatVersion: PayloadFormatVersion; + + /** + * Specifies the server name to verified by HTTPS when calling the backend integration + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-integration-tlsconfig.html + * @default undefined private integration traffic will use HTTP protocol + */ + readonly secureServerName?: string; } 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 018b23ba1e10d..748ec8cb68d41 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -173,6 +173,7 @@ describe('HttpRoute', () => { connectionId: 'some-connection-id', connectionType: HttpConnectionType.VPC_LINK, uri: 'some-target-arn', + secureServerName: 'some-server-name', }; } } @@ -192,7 +193,11 @@ describe('HttpRoute', () => { IntegrationMethod: 'ANY', IntegrationUri: 'some-target-arn', PayloadFormatVersion: '1.0', + TlsConfig: { + ServerNameToVerify: 'some-server-name', + }, }); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::VpcLink'); }); From a2d936999cf6deb0c1355a5c8551ede1268d786e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jul 2021 20:24:09 +0200 Subject: [PATCH 082/105] docs(pipeline): clarify that `ShellStep` needs to be added to the pipeline (#15669) There was confusion on Slack on why the `ShellStep` of this example did not appear in the pipeline. Make the example code more complete. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index b38bdef546c2b..9753faeb38b29 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -474,10 +474,14 @@ step and runs tests from there: const synth = new ShellStep('Synth', { /* ... */ }); const pipeline = new CodePipeline(this, 'Pipeline', { synth }); -new ShellStep('Approve', { - // Use the contents of the 'integ' directory from the synth step as the input - input: synth.addOutputDirectory('integ'), - commands: ['cd integ && ./run.sh'], +pipeline.addStage(/* ... */, { + post: [ + new ShellStep('Approve', { + // Use the contents of the 'integ' directory from the synth step as the input + input: synth.addOutputDirectory('integ'), + commands: ['cd integ && ./run.sh'], + }), + ], }); ``` From 4e40db3e5577c575aa5e25093e1b82eae6e7cb31 Mon Sep 17 00:00:00 2001 From: arcrank Date: Tue, 20 Jul 2021 15:25:52 -0400 Subject: [PATCH 083/105] feat(servicecatalog): Add stack event notification constraint (#15610) Add stack event notification constraint. Allows users to subscribe AWS `SNS` topics to stack updates on their products. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* Co-authored-by: Dillon Ponzo --- .../@aws-cdk/aws-servicecatalog/README.md | 19 +++++++ .../aws-servicecatalog/lib/portfolio.ts | 14 +++++- .../lib/private/association-manager.ts | 27 ++++++++-- .../@aws-cdk/aws-servicecatalog/package.json | 2 + .../test/integ.portfolio.expected.json | 46 +++++++++++++++++ .../test/integ.portfolio.ts | 11 +++++ .../aws-servicecatalog/test/portfolio.test.ts | 49 +++++++++++++++++++ 7 files changed, 164 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index cbdf1d0224998..d2cd893efb4e8 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -33,6 +33,7 @@ enables organizations to create and manage catalogs of products for their end us - [Adding a product to a portfolio](#adding-a-product-to-a-portfolio) - [TagOptions](#tag-options) - [Constraints](#constraints) + - [Notify on stack events](#notify-on-stack-events) - [Tag update constraint](#tag-update-constraint) The `@aws-cdk/aws-servicecatalog` package contains resources that enable users to automate governance and management of their AWS resources at scale. @@ -185,6 +186,24 @@ If a misconfigured constraint is added, `synth` will fail with an error message. Read more at [Service Catalog Constraints](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/constraints.html). +### Notify on stack events + +Allows users to subscribe an AWS `SNS` topic to the stack events of the product. +When an end user provisions a product it creates a product stack that notifies the subscribed topic on creation, edit, and delete events. +An individual `SNS` topic may only be subscribed once to a portfolio-product association. + +```ts fixture=portfolio-product +import * as sns from '@aws-cdk/aws-sns'; + +const topic1 = new sns.Topic(this, 'MyTopic1'); +portfolio.notifyOnStackEvents(product, topic1); + +const topic2 = new sns.Topic(this, 'MyTopic2'); +portfolio.notifyOnStackEvents(product, topic2, { + description: 'description for this topic2', // description is an optional field. +}); +``` + ### Tag update constraint Tag update constraints allow or disallow end users to update tags on resources associated with an AWS Service Catalog product upon provisioning. diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts index 7db7998b34c83..ba8bc6c301d12 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts @@ -1,7 +1,8 @@ import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import { MessageLanguage } from './common'; -import { TagUpdateConstraintOptions } from './constraints'; +import { CommonConstraintOptions, TagUpdateConstraintOptions } from './constraints'; import { AssociationManager } from './private/association-manager'; import { hashValues } from './private/util'; import { InputValidator } from './private/validation'; @@ -87,6 +88,13 @@ export interface IPortfolio extends cdk.IResource { */ associateTagOptions(tagOptions: TagOptions): void; + /** + * Add notifications for supplied topics on the provisioned product. + * @param product A service catalog product. + * @param topic A SNS Topic to receive notifications on events related to the provisioned product. + */ + notifyOnStackEvents(product: IProduct, topic: sns.ITopic, options?: CommonConstraintOptions): void; + /** * Add a Resource Update Constraint. */ @@ -128,6 +136,10 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio { AssociationManager.associateTagOptions(this, tagOptions); } + public notifyOnStackEvents(product: IProduct, topic: sns.ITopic, options: CommonConstraintOptions = {}): void { + AssociationManager.notifyOnStackEvents(this, product, topic, options); + } + public constrainTagUpdates(product: IProduct, options: TagUpdateConstraintOptions = {}): void { AssociationManager.constrainTagUpdates(this, product, options); } diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts index 6ba7e6ebb8eb0..519679e3df5a0 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts @@ -1,8 +1,9 @@ +import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; -import { TagUpdateConstraintOptions } from '../constraints'; +import { CommonConstraintOptions, TagUpdateConstraintOptions } from '../constraints'; import { IPortfolio } from '../portfolio'; import { IProduct } from '../product'; -import { CfnPortfolioProductAssociation, CfnResourceUpdateConstraint, CfnTagOption, CfnTagOptionAssociation } from '../servicecatalog.generated'; +import { CfnLaunchNotificationConstraint, CfnPortfolioProductAssociation, CfnResourceUpdateConstraint, CfnTagOption, CfnTagOptionAssociation } from '../servicecatalog.generated'; import { TagOptions } from '../tag-options'; import { hashValues } from './util'; import { InputValidator } from './validation'; @@ -27,6 +28,26 @@ export class AssociationManager { }; } + public static notifyOnStackEvents(portfolio: IPortfolio, product: IProduct, topic: sns.ITopic, options: CommonConstraintOptions): void { + InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options.description); + const association = this.associateProductWithPortfolio(portfolio, product); + const constructId = `LaunchNotificationConstraint${hashValues(topic.node.addr, topic.stack.node.addr, association.associationKey)}`; + + if (!portfolio.node.tryFindChild(constructId)) { + const constraint = new CfnLaunchNotificationConstraint(portfolio as unknown as cdk.Resource, constructId, { + acceptLanguage: options.messageLanguage, + description: options.description, + portfolioId: portfolio.portfolioId, + productId: product.productId, + notificationArns: [topic.topicArn], + }); + + // Add dependsOn to force proper order in deployment. + constraint.addDependsOn(association.cfnPortfolioProductAssociation); + } else { + throw new Error(`Topic ${topic.node.path} is already subscribed to association ${this.prettyPrintAssociation(portfolio, product)}`); + } + } public static constrainTagUpdates(portfolio: IPortfolio, product: IProduct, options: TagUpdateConstraintOptions): void { InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options.description); @@ -80,4 +101,4 @@ export class AssociationManager { private static prettyPrintAssociation(portfolio: IPortfolio, product: IProduct): string { return `- Portfolio: ${portfolio.node.path} | Product: ${product.node.path}`; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-servicecatalog/package.json b/packages/@aws-cdk/aws-servicecatalog/package.json index e448e5aa5b559..442fdbe8eb5c8 100644 --- a/packages/@aws-cdk/aws-servicecatalog/package.json +++ b/packages/@aws-cdk/aws-servicecatalog/package.json @@ -83,6 +83,7 @@ "dependencies": { "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -90,6 +91,7 @@ "peerDependencies": { "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json index 1de1c2a264218..d7f4510a7b01a 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json @@ -142,6 +142,46 @@ "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" ] }, + "TestPortfolioLaunchNotificationConstraintf2572fdcecadB6DD8541": { + "Type": "AWS::ServiceCatalog::LaunchNotificationConstraint", + "Properties": { + "NotificationArns": [ + { + "Ref": "Topic198E71B3E" + } + ], + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "ProductId": { + "Ref": "TestProduct7606930B" + } + }, + "DependsOn": [ + "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" + ] + }, + "TestPortfolioLaunchNotificationConstrainta9675fc4d6aa995BF1B9": { + "Type": "AWS::ServiceCatalog::LaunchNotificationConstraint", + "Properties": { + "NotificationArns": [ + { + "Ref": "specialTopic7664DE4C" + } + ], + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "ProductId": { + "Ref": "TestProduct7606930B" + }, + "AcceptLanguage": "en", + "Description": "special topic description" + }, + "DependsOn": [ + "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" + ] + }, "TagOptionc0d88a3c4b8b": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { @@ -180,6 +220,12 @@ } ] } + }, + "Topic198E71B3E": { + "Type": "AWS::SNS::Topic" + }, + "specialTopic7664DE4C": { + "Type": "AWS::SNS::Topic" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts index 31a7cff042551..ae45f171a8c47 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts @@ -1,4 +1,5 @@ import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; import { App, Stack } from '@aws-cdk/core'; import * as servicecatalog from '../lib'; @@ -45,4 +46,14 @@ portfolio.addProduct(product); portfolio.constrainTagUpdates(product); +const topic = new sns.Topic(stack, 'Topic1'); + +const specialTopic = new sns.Topic(stack, 'specialTopic'); + +portfolio.notifyOnStackEvents(product, topic); +portfolio.notifyOnStackEvents(product, specialTopic, { + description: 'special topic description', + messageLanguage: servicecatalog.MessageLanguage.EN, +}); + app.synth(); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts index e21ace272c4b8..a5f97dc1e7470 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts @@ -1,5 +1,6 @@ import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as servicecatalog from '../lib'; @@ -408,5 +409,53 @@ describe('portfolio associations and product constraints', () => { description: 'another test constraint description', }); }).toThrowError(/Cannot have multiple tag update constraints for association/); + }), + + test('add event notification constraint', () => { + portfolio.addProduct(product); + + const topic = new sns.Topic(stack, 'Topic'); + const description = 'event notification constraint description'; + + portfolio.notifyOnStackEvents(product, topic, { + description: description, + }); + + expect(stack).toHaveResource('AWS::ServiceCatalog::LaunchNotificationConstraint', { + NotificationArns: [{ Ref: 'TopicBFC7AF6E' }], + Description: description, + PortfolioId: { Ref: 'MyPortfolio59CCA9C9' }, + ProductId: { Ref: 'MyProduct49A3C587' }, + }); + }), + + test('event notification constraint will still add without explicit association', () => { + const topic = new sns.Topic(stack, 'Topic1'); + + portfolio.notifyOnStackEvents(product, topic); + + expect(stack).toCountResources('AWS::ServiceCatalog::LaunchNotificationConstraint', 1); + }), + + test('can add multiple notifications', () => { + const topic1 = new sns.Topic(stack, 'Topic1'); + const topic2 = new sns.Topic(stack, 'Topic2'); + const topic3 = new sns.Topic(stack, 'Topic3'); + + portfolio.notifyOnStackEvents(product, topic1); + portfolio.notifyOnStackEvents(product, topic2); + portfolio.notifyOnStackEvents(product, topic3); + + expect(stack).toCountResources('AWS::ServiceCatalog::LaunchNotificationConstraint', 3); + }), + + test('fails to add same topic multiple times in event notification constraint', () => { + const topic = new sns.Topic(stack, 'Topic1'); + + portfolio.notifyOnStackEvents(product, topic); + + expect(() => { + portfolio.notifyOnStackEvents(product, topic); + }).toThrowError(`Topic ${topic} is already subscribed to association`); }); }); \ No newline at end of file From 06d58325db7884d8d9e57122eff1e06435a8e5ed Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Tue, 20 Jul 2021 17:04:39 -0400 Subject: [PATCH 084/105] chore(lambda): fix typo (#15459) fo -> of ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda/lib/function-base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index e8935fc2cdb6b..746cb23438c51 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -21,7 +21,7 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable { readonly functionName: string; /** - * The ARN fo the function. + * The ARN of the function. * * @attribute */ From 6bc086b23df7a58ebae98c454141ebfd901ebf50 Mon Sep 17 00:00:00 2001 From: Jerry Kindall <52084730+Jerry-AWS@users.noreply.github.com> Date: Tue, 20 Jul 2021 14:43:42 -0700 Subject: [PATCH 085/105] docs(ecs): Specify units for DNS TTL (#15656) Customer reports that it's not clear here what the units of the default "60" are, and furthermore that this appears as "1" (again without units) in the console. Clarify by writing this as `Duration.minutes(1)` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecs/lib/base/base-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index c33718905c5bb..ce5097e9ebccf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -945,7 +945,7 @@ export interface CloudMapOptions { /** * The amount of time that you want DNS resolvers to cache the settings for this record. * - * @default 60 + * @default Duration.minutes(1) */ readonly dnsTtl?: Duration; From 464cdc0baa1fc5b0634e428a9c7a15de4ca93d50 Mon Sep 17 00:00:00 2001 From: David Michael <1davidmichael@users.noreply.github.com> Date: Tue, 20 Jul 2021 17:22:29 -0500 Subject: [PATCH 086/105] chore(readme): Correct CDK Construction Zone S1E5 Twitch Link (#15646) This change corrects the episode link, both within a browser and with the youtube-dl tool it shows as a non-existent video with the current link. youtube-dl output ```bash youtube-dl https://www.twitch.tv/aws/video/977551207 [twitch:vod] 977551207: Downloading stream metadata GraphQL ERROR: Video 977551207 does not exist ``` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50d417529d979..480d3ea597e85 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ this capability, please see the * [S1E2](https://www.twitch.tv/videos/925801382): Triggers (part 2); **Participants:** @NetaNir, @eladb, @iliapolo * [S1E3](https://www.twitch.tv/videos/944565768): Triggers (part 3); **Participants:** @NetaNir, @eladb, @iliapolo, @RomainMuller * [S1E4](https://www.twitch.tv/aws/video/960287598): [Tokens](https://docs.aws.amazon.com/cdk/latest/guide/tokens.html) Deep Dive; **Participants:** @NetaNir,@rix0rrr, @iliapolo, @RomainMuller - * [S1E5](https://www.twitch.tv/aws/video/977551207): [Assets](https://docs.aws.amazon.com/cdk/latest/guide/assets.html) Deep Dive; **Participants:** @NetaNir, @eladb, @jogold + * [S1E5](https://www.twitch.tv/videos/981481112): [Assets](https://docs.aws.amazon.com/cdk/latest/guide/assets.html) Deep Dive; **Participants:** @NetaNir, @eladb, @jogold * [S1E6](https://www.twitch.tv/aws/video/1005334364): [Best Practices](https://aws.amazon.com/blogs/devops/best-practices-for-developing-cloud-applications-with-aws-cdk/); **Participants:** @skinny85, @eladb, @rix0rrr, @alexpulver * [S1E7](https://www.twitch.tv/videos/1019059654): Tips and Tricks From The CDK Team; **Participants:** All the CDK team! * [Examples](https://github.com/aws-samples/aws-cdk-examples) From c488cbf76d06add7b1e842bc70de4e1c0c3c2170 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 21 Jul 2021 11:08:59 +0200 Subject: [PATCH 087/105] docs(pipelines): stress implications of `--trust` again (#15685) Requested by AppSec to double-stress that access to the deploy role (implied by `--trust`) is dangerous and should be explicitly called out in the documentation. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 28 ++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 9753faeb38b29..83e53348a04e2 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -739,9 +739,9 @@ to it, it's important that you don't delete the stack or change its *Qualifier*, or future deployments to this environment will fail. If you want to upgrade the bootstrap stack to a newer version, do that by updating it in-place. -> This library requires a newer version of the bootstrapping stack which has -> been updated specifically to support cross-account continuous delivery. In the future, -> this new bootstrapping stack will become the default, but for now it is still +> This library requires the *modern* bootstrapping stack which has +> been updated specifically to support cross-account continuous delivery. Starting, +> in CDK v2 this new bootstrapping stack will become the default, but for now it is still > opt-in. > > The commands below assume you are running `cdk bootstrap` in a directory @@ -792,18 +792,28 @@ These command lines explained: flag out if either the AWS default credentials or the `AWS_*` environment variables confer these permissions. * `--cloudformation-execution-policies`: ARN of the managed policy that future CDK - deployments should execute with. You can tailor this to the needs of your organization - and give more constrained permissions than `AdministratorAccess`. + deployments should execute with. By default this is `AdministratorAccess`, but + if you also specify the `--trust` flag to give another Account permissions to + deploy into the current account, you must specify a value here. * `--trust`: indicates which other account(s) should have permissions to deploy CDK applications into this account. In this case we indicate the Pipeline's account, but you could also use this for developer accounts (don't do that for production application accounts though!). -* `--trust-for-lookup`: similar to `--trust`, but gives a more limited set of permissions to the - trusted account, allowing it to only look up values, such as availability zones, EC2 images and - VPCs. Note that if you provide an account using `--trust`, that account can also do lookups. - So you only need to pass `--trust-for-lookup` if you need to use a different account. +* `--trust-for-lookup`: gives a more limited set of permissions to the + trusted account, only allowing it to look up values such as availability zones, EC2 images and + VPCs. `--trust-for-lookup` does not give permissions to modify anything in the account. + Note that `--trust` implies `--trust-for-lookup`, so you don't need to specify + the same acocunt twice. * `aws://222222222222/us-east-2`: the account and region we're bootstrapping. +> Be aware that anyone who has access to the trusted Accounts **effectively has all +> permissions conferred by the configured CloudFormation execution policies**, +> allowing them to do things like read arbitrary S3 buckets and create arbitrary +> infrastructure in the bootstrapped account. Restrict the list of `--trust`ed Accounts, +> or restrict the policies configured by `--cloudformation-execution-policies`. + +
+ > **Security tip**: we recommend that you use administrative credentials to an > account only to bootstrap it and provision the initial pipeline. Otherwise, > access to administrative credentials should be dropped as soon as possible. From 1f2a06bd34f39624f628ab5779fb82e6b001f2c8 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Wed, 21 Jul 2021 14:26:45 +0000 Subject: [PATCH 088/105] chore(release): 1.115.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2c236e6eb4e..20b7301ca3863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.115.0](https://github.com/aws/aws-cdk/compare/v1.114.0...v1.115.0) (2021-07-21) + + +### Features + +* **apigatewayv2:** websocket - callback url ([#15227](https://github.com/aws/aws-cdk/issues/15227)) ([349de7c](https://github.com/aws/aws-cdk/commit/349de7c2abff97e10a6e76dd6b7856ecbfd0c441)), closes [#14836](https://github.com/aws/aws-cdk/issues/14836) +* **apigatewayv2-integrations:** http private integrations - tls config ([#15469](https://github.com/aws/aws-cdk/issues/15469)) ([6453769](https://github.com/aws/aws-cdk/commit/645376958a699ee89e3cc805546c293c93d99613)), closes [#14036](https://github.com/aws/aws-cdk/issues/14036) +* **appsync:** optional operation parameter for lambdaRequest mapping template ([#15283](https://github.com/aws/aws-cdk/issues/15283)) ([efd2e68](https://github.com/aws/aws-cdk/commit/efd2e68d9799f0827c542a562f3c17a5a0dbdee1)), closes [#15274](https://github.com/aws/aws-cdk/issues/15274) [#14079](https://github.com/aws/aws-cdk/issues/14079) +* **aws-efs:** grant support on FileSystem ([#14999](https://github.com/aws/aws-cdk/issues/14999)) ([09591c6](https://github.com/aws/aws-cdk/commit/09591c6268d0e03937741e4f7cad9b97e21b131b)), closes [#14998](https://github.com/aws/aws-cdk/issues/14998) +* **cli:** add ability to specify an external id for the deploy-role ([#15604](https://github.com/aws/aws-cdk/issues/15604)) ([2647cf3](https://github.com/aws/aws-cdk/commit/2647cf300ae0f9053104e3a545e2fd94dd7249e1)) +* **lambda-nodejs:** source map mode ([#15621](https://github.com/aws/aws-cdk/issues/15621)) ([b934976](https://github.com/aws/aws-cdk/commit/b934976f057cd395de660dc4099e2303415cdc78)), closes [#14857](https://github.com/aws/aws-cdk/issues/14857) +* **pipelines:** confirm IAM changes before starting the deployment ([#15441](https://github.com/aws/aws-cdk/issues/15441)) ([ebba618](https://github.com/aws/aws-cdk/commit/ebba61830ea7ee73e168099d1cd8e8f4003d595c)), closes [#12748](https://github.com/aws/aws-cdk/issues/12748) +* **rds:** allow setting copyTagsToSnapshot on Clusters ([#15553](https://github.com/aws/aws-cdk/issues/15553)) ([f7c6289](https://github.com/aws/aws-cdk/commit/f7c628948e7f71df7a95cb00cdc2746e2e46dc03)), closes [#15521](https://github.com/aws/aws-cdk/issues/15521) +* **servicecatalog:** Add stack event notification constraint ([#15610](https://github.com/aws/aws-cdk/issues/15610)) ([4e40db3](https://github.com/aws/aws-cdk/commit/4e40db3e5577c575aa5e25093e1b82eae6e7cb31)) +* **servicecatalog:** Add TagOptions for portfolio ([#15612](https://github.com/aws/aws-cdk/issues/15612)) ([e7760ee](https://github.com/aws/aws-cdk/commit/e7760ee9da19d7d006cdf1836ce6f71bf9f31327)) + + +### Bug Fixes + +* **appsync:** update timestamp for apikey test ([#15624](https://github.com/aws/aws-cdk/issues/15624)) ([9c4e51c](https://github.com/aws/aws-cdk/commit/9c4e51ca1719b89bcdd9d4032f50063876fac69a)), closes [#15623](https://github.com/aws/aws-cdk/issues/15623) +* **cfnspec:** make EndpointConfiguration of AWS::Serverless::Api a union type ([#15526](https://github.com/aws/aws-cdk/issues/15526)) ([dd38eff](https://github.com/aws/aws-cdk/commit/dd38eff318c31bf2c5308f6b3daae0fd433b4370)) +* **cli:** `cdk deploy` is listing deprecated ids ([#15603](https://github.com/aws/aws-cdk/issues/15603)) ([22f2499](https://github.com/aws/aws-cdk/commit/22f2499508bccd3f44733705bbfa3c4e2b0b0d63)) +* **iam:** `PrincipalWithConditions.addCondition` does not work ([#15414](https://github.com/aws/aws-cdk/issues/15414)) ([fdce08c](https://github.com/aws/aws-cdk/commit/fdce08cee6f0eb58aad93572641a1dd4b59e8d37)) +* **pipelines:** `CodeBuildStep.partialBuildSpec` not used, buildspec control for legacy API ([#15625](https://github.com/aws/aws-cdk/issues/15625)) ([d8dc818](https://github.com/aws/aws-cdk/commit/d8dc8185203e73172786024eea90eeb60153ce0e)), closes [#15169](https://github.com/aws/aws-cdk/issues/15169) +* **pipelines:** new pipeline stages aren't validated ([#15665](https://github.com/aws/aws-cdk/issues/15665)) ([309b9b4](https://github.com/aws/aws-cdk/commit/309b9b4cf554474c87fe3d833a5205498e200ecf)) +* **pipelines:** permissions check in legacy API does not work ([#15660](https://github.com/aws/aws-cdk/issues/15660)) ([5e3cf2b](https://github.com/aws/aws-cdk/commit/5e3cf2b0558401fab25f75da319fac587df1bcfb)) +* **pipelines:** unresolved source names aren't handled properly ([#15600](https://github.com/aws/aws-cdk/issues/15600)) ([4b7116d](https://github.com/aws/aws-cdk/commit/4b7116d8a252a6768ae50d736d5cab0f0cef22f4)), closes [#15592](https://github.com/aws/aws-cdk/issues/15592) + ## [1.114.0](https://github.com/aws/aws-cdk/compare/v1.113.0...v1.114.0) (2021-07-15) diff --git a/version.v1.json b/version.v1.json index b2aee66739e48..4af54a1130ffd 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.114.0" + "version": "1.115.0" } From 52331c8e38645fbf804fe1c9e528a8faf9185c0c Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Wed, 21 Jul 2021 17:29:54 +0300 Subject: [PATCH 089/105] chore: npm-check-updates && yarn upgrade (#15693) Ran npm-check-updates and yarn upgrade to keep the `yarn.lock` file up-to-date. --- package.json | 2 +- .../aws-apigatewayv2-authorizers/package.json | 2 +- .../package.json | 4 +- .../@aws-cdk/aws-cloudformation/package.json | 2 +- .../aws-global-table-coordinator/package.json | 2 +- packages/@aws-cdk/aws-dynamodb/package.json | 2 +- packages/@aws-cdk/aws-ec2/package.json | 2 +- packages/@aws-cdk/aws-eks/package.json | 2 +- packages/@aws-cdk/aws-iam/package.json | 2 +- packages/@aws-cdk/aws-lambda/package.json | 2 +- packages/@aws-cdk/aws-logs/package.json | 2 +- packages/@aws-cdk/aws-route53/package.json | 2 +- packages/@aws-cdk/aws-s3/package.json | 2 +- packages/@aws-cdk/aws-ses/package.json | 2 +- packages/@aws-cdk/core/package.json | 2 +- .../@aws-cdk/custom-resources/package.json | 2 +- packages/@aws-cdk/pipelines/package.json | 2 +- packages/aws-cdk/package.json | 2 +- packages/awslint/package.json | 8 +- tools/cdk-build-tools/package.json | 8 +- tools/eslint-plugin-cdk/package.json | 4 +- tools/pkglint/package.json | 8 +- yarn.lock | 316 +++++++++--------- 23 files changed, 191 insertions(+), 191 deletions(-) diff --git a/package.json b/package.json index 96281e2f0dd36..66025c149aa84 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "jsii-rosetta": "^1.31.0", "lerna": "^4.0.0", "patch-package": "^6.4.7", - "standard-version": "^9.3.0", + "standard-version": "^9.3.1", "typescript": "~3.9.10" }, "tap-mocha-reporter-resolutions-comment": "should be removed or reviewed when nodeunit dependency is dropped or adjusted", diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json index 23690776093aa..748efa7077158 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json @@ -74,7 +74,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/jest": "^26.0.24", - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json index e3ab68c8a7dd6..63784c4cf30ce 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json @@ -29,12 +29,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/sinon": "^9.0.11", "cdk-build-tools": "0.0.0", "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.2.1", - "eslint": "^7.30.0", + "eslint": "^7.31.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index f0d492aace535..b3d32b8636b09 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -74,7 +74,7 @@ "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json index 5df277b6064ca..130fed06d99c7 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json @@ -31,7 +31,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.2.1", - "eslint": "^7.30.0", + "eslint": "^7.31.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index a9ffc922c528e..e6d4627e3455e 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/jest": "^26.0.24", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 52dede95bb318..985422e5da1fb 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/jest": "^26.0.24", "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index bf23bff54c899..290e23df60d2c 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/sinon": "^9.0.11", "@types/nodeunit": "^0.0.32", "@types/yaml": "1.9.6", diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index 2cbf971197ef3..ef766c83c1a25 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/jest": "^26.0.24", "@types/sinon": "^9.0.11", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index da9f2ff159fd6..e3a1ec3289d46 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/jest": "^26.0.24", - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/lodash": "^4.14.171", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index a7838115e50db..ebaa38cd41ad5 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -73,7 +73,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/nodeunit": "^0.0.32", - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", "aws-sdk-mock": "^5.2.1", diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 4d72b2ed05616..90608982f090b 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/jest": "^26.0.24", "@types/nodeunit": "^0.0.32", "aws-sdk": "^2.848.0", diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 06e4248c943e7..da9bdd3d05df4 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -73,7 +73,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ses/package.json b/packages/@aws-cdk/aws-ses/package.json index 36c7fca9442e3..5b56def33afd4 100644 --- a/packages/@aws-cdk/aws-ses/package.json +++ b/packages/@aws-cdk/aws-ses/package.json @@ -72,7 +72,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 80033471fbdc5..f4bfcb141730e 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -169,7 +169,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/fs-extra": "^8.1.2", "@types/jest": "^26.0.24", "@types/lodash": "^4.14.171", diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 67f181d8844f6..d81b9a11edc0c 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -77,7 +77,7 @@ "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", - "@types/aws-lambda": "^8.10.78", + "@types/aws-lambda": "^8.10.79", "@types/fs-extra": "^8.1.2", "@types/sinon": "^9.0.11", "aws-sdk": "^2.848.0", diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 45252fe965ac7..25c9099e64074 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -37,7 +37,7 @@ "@aws-cdk/aws-ecr-assets": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", - "@types/jest": "^26.0.23", + "@types/jest": "^26.0.24", "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 71d15e6442fdf..4485c964c105e 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -41,7 +41,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/core": "0.0.0", - "@octokit/rest": "^18.6.7", + "@octokit/rest": "^18.6.8", "@types/archiver": "^5.3.0", "@types/fs-extra": "^8.1.2", "@types/glob": "^7.1.4", diff --git a/packages/awslint/package.json b/packages/awslint/package.json index da5906149f506..6edbaaf52399e 100644 --- a/packages/awslint/package.json +++ b/packages/awslint/package.json @@ -31,14 +31,14 @@ "@types/yargs": "^15.0.14", "pkglint": "0.0.0", "typescript": "~3.9.10", - "@typescript-eslint/eslint-plugin": "^4.28.3", - "@typescript-eslint/parser": "^4.28.3", - "eslint": "^7.30.0", + "@typescript-eslint/eslint-plugin": "^4.28.4", + "@typescript-eslint/parser": "^4.28.4", + "eslint": "^7.31.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", "eslint-plugin-import": "^2.23.4", - "eslint-plugin-jest": "^24.3.6", + "eslint-plugin-jest": "^24.3.7", "jest": "^26.6.3" }, "repository": { diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 3443058c4ea6b..c4c3df70fc154 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -42,16 +42,16 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^4.28.3", - "@typescript-eslint/parser": "^4.28.3", + "@typescript-eslint/eslint-plugin": "^4.28.4", + "@typescript-eslint/parser": "^4.28.4", "awslint": "0.0.0", "colors": "^1.4.0", - "eslint": "^7.30.0", + "eslint": "^7.31.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", "eslint-plugin-import": "^2.23.4", - "eslint-plugin-jest": "^24.3.6", + "eslint-plugin-jest": "^24.3.7", "fs-extra": "^9.1.0", "jest": "^26.6.3", "jest-junit": "^11.1.0", diff --git a/tools/eslint-plugin-cdk/package.json b/tools/eslint-plugin-cdk/package.json index b075f070208d0..4095ff1d19297 100644 --- a/tools/eslint-plugin-cdk/package.json +++ b/tools/eslint-plugin-cdk/package.json @@ -24,8 +24,8 @@ "typescript": "~3.9.10" }, "dependencies": { - "@typescript-eslint/parser": "^4.28.3", - "eslint": "^7.30.0", + "@typescript-eslint/parser": "^4.28.4", + "eslint": "^7.31.0", "fs-extra": "^9.1.0" }, "jest": { diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index 9f6f3446c31b3..a0a988e330e6c 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -42,14 +42,14 @@ "@types/jest": "^26.0.24", "@types/semver": "^7.3.7", "@types/yargs": "^15.0.14", - "@typescript-eslint/eslint-plugin": "^4.28.3", - "@typescript-eslint/parser": "^4.28.3", - "eslint": "^7.30.0", + "@typescript-eslint/eslint-plugin": "^4.28.4", + "@typescript-eslint/parser": "^4.28.4", + "eslint": "^7.31.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", "eslint-plugin-import": "^2.23.4", - "eslint-plugin-jest": "^24.3.6", + "eslint-plugin-jest": "^24.3.7", "jest": "^26.6.3", "typescript": "~3.9.10" }, diff --git a/yarn.lock b/yarn.lock index 51786b613db78..7e67161d74f5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,19 +43,19 @@ integrity sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw== "@babel/core@^7.1.0", "@babel/core@^7.7.5": - version "7.14.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.6.tgz#e0814ec1a950032ff16c13a2721de39a8416fcab" - integrity sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA== + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.8.tgz#20cdf7c84b5d86d83fac8710a8bc605a7ba3f010" + integrity sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q== dependencies: "@babel/code-frame" "^7.14.5" - "@babel/generator" "^7.14.5" + "@babel/generator" "^7.14.8" "@babel/helper-compilation-targets" "^7.14.5" - "@babel/helper-module-transforms" "^7.14.5" - "@babel/helpers" "^7.14.6" - "@babel/parser" "^7.14.6" + "@babel/helper-module-transforms" "^7.14.8" + "@babel/helpers" "^7.14.8" + "@babel/parser" "^7.14.8" "@babel/template" "^7.14.5" - "@babel/traverse" "^7.14.5" - "@babel/types" "^7.14.5" + "@babel/traverse" "^7.14.8" + "@babel/types" "^7.14.8" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -63,12 +63,12 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.14.5", "@babel/generator@^7.4.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.5.tgz#848d7b9f031caca9d0cd0af01b063f226f52d785" - integrity sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA== +"@babel/generator@^7.14.8", "@babel/generator@^7.4.0": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.8.tgz#bf86fd6af96cf3b74395a8ca409515f89423e070" + integrity sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg== dependencies: - "@babel/types" "^7.14.5" + "@babel/types" "^7.14.8" jsesc "^2.5.1" source-map "^0.5.0" @@ -119,19 +119,19 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-module-transforms@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz#7de42f10d789b423eb902ebd24031ca77cb1e10e" - integrity sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA== +"@babel/helper-module-transforms@^7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz#d4279f7e3fd5f4d5d342d833af36d4dd87d7dc49" + integrity sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA== dependencies: "@babel/helper-module-imports" "^7.14.5" "@babel/helper-replace-supers" "^7.14.5" - "@babel/helper-simple-access" "^7.14.5" + "@babel/helper-simple-access" "^7.14.8" "@babel/helper-split-export-declaration" "^7.14.5" - "@babel/helper-validator-identifier" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.8" "@babel/template" "^7.14.5" - "@babel/traverse" "^7.14.5" - "@babel/types" "^7.14.5" + "@babel/traverse" "^7.14.8" + "@babel/types" "^7.14.8" "@babel/helper-optimise-call-expression@^7.14.5": version "7.14.5" @@ -155,12 +155,12 @@ "@babel/traverse" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/helper-simple-access@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz#66ea85cf53ba0b4e588ba77fc813f53abcaa41c4" - integrity sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw== +"@babel/helper-simple-access@^7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924" + integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg== dependencies: - "@babel/types" "^7.14.5" + "@babel/types" "^7.14.8" "@babel/helper-split-export-declaration@^7.14.5": version "7.14.5" @@ -169,24 +169,24 @@ dependencies: "@babel/types" "^7.14.5" -"@babel/helper-validator-identifier@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" - integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== +"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz#32be33a756f29e278a0d644fa08a2c9e0f88a34c" + integrity sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow== "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== -"@babel/helpers@^7.14.6": - version "7.14.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.6.tgz#5b58306b95f1b47e2a0199434fa8658fa6c21635" - integrity sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA== +"@babel/helpers@^7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.8.tgz#839f88f463025886cff7f85a35297007e2da1b77" + integrity sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw== dependencies: "@babel/template" "^7.14.5" - "@babel/traverse" "^7.14.5" - "@babel/types" "^7.14.5" + "@babel/traverse" "^7.14.8" + "@babel/types" "^7.14.8" "@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5": version "7.14.5" @@ -197,10 +197,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.5", "@babel/parser@^7.14.6", "@babel/parser@^7.14.7", "@babel/parser@^7.4.3": - version "7.14.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.7.tgz#6099720c8839ca865a2637e6c85852ead0bdb595" - integrity sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.5", "@babel/parser@^7.14.8", "@babel/parser@^7.4.3": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.8.tgz#66fd41666b2d7b840bd5ace7f7416d5ac60208d4" + integrity sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -295,27 +295,27 @@ "@babel/parser" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.4.3": - version "7.14.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.7.tgz#64007c9774cfdc3abd23b0780bc18a3ce3631753" - integrity sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.14.8", "@babel/traverse@^7.4.3": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.8.tgz#c0253f02677c5de1a8ff9df6b0aacbec7da1a8ce" + integrity sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg== dependencies: "@babel/code-frame" "^7.14.5" - "@babel/generator" "^7.14.5" + "@babel/generator" "^7.14.8" "@babel/helper-function-name" "^7.14.5" "@babel/helper-hoist-variables" "^7.14.5" "@babel/helper-split-export-declaration" "^7.14.5" - "@babel/parser" "^7.14.7" - "@babel/types" "^7.14.5" + "@babel/parser" "^7.14.8" + "@babel/types" "^7.14.8" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.14.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff" - integrity sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg== +"@babel/types@^7.0.0", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.8.tgz#38109de8fcadc06415fbd9b74df0065d4d41c728" + integrity sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q== dependencies: - "@babel/helper-validator-identifier" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.8" to-fast-properties "^2.0.0" "@balena/dockerignore@^1.0.2": @@ -336,10 +336,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@eslint/eslintrc@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.2.tgz#f63d0ef06f5c0c57d76c4ab5f63d3835c51b0179" - integrity sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg== +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -1352,10 +1352,10 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^8.3.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-8.3.0.tgz#8bc912edae8c03e002882cf1e29b595b7da9b441" - integrity sha512-ZFyQ30tNpoATI7o+Z9MWFUzUgWisB8yduhcky7S4UYsRijgIGSnwUKzPBDGzf/Xkx1DuvUtqzvmuFlDSqPJqmQ== +"@octokit/openapi-types@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.0.0.tgz#05d33f999326785445c915d25167d68bd5eddb24" + integrity sha512-GSpv5VUFqarOXZl6uWPsDnjChkKCxnaMALmQhzvCWGiMxONQxX7ZwlomCMS+wB1KqxLPCA5n6gYt016oEMkHmQ== "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" @@ -1389,12 +1389,12 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.4.1.tgz#540ec90bb753dcaa682ee9f2cd6efdde9132fa90" - integrity sha512-Nx0g7I5ayAYghsLJP4Q1Ch2W9jYYM0FlWWWZocUro8rNxVwuZXGfFd7Rcqi9XDWepSXjg1WByiNJnZza2hIOvQ== +"@octokit/plugin-rest-endpoint-methods@5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.4.2.tgz#d090e93ee68ec09985e1ff0a1d2d28581cc883a5" + integrity sha512-imNDDvUMy9YzECcP6zTcKNjwutSwqCYGMZjLPnBHF0kdb3V9URrHWmalD0ZvNEYjwbpm2zw8RPewj3ebCpMBRw== dependencies: - "@octokit/types" "^6.18.1" + "@octokit/types" "^6.19.1" deprecation "^2.3.1" "@octokit/request-error@^1.0.2": @@ -1449,15 +1449,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^18.1.0", "@octokit/rest@^18.6.7": - version "18.6.7" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.6.7.tgz#89b8ecd13edd9603f00453640d1fb0b4175d4b31" - integrity sha512-Kn6WrI2ZvmAztdx+HEaf88RuJn+LK72S8g6OpciE4kbZddAN84fu4fiPGxcEu052WmqKVnA/cnQsbNlrYC6rqQ== +"@octokit/rest@^18.1.0", "@octokit/rest@^18.6.8": + version "18.6.8" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.6.8.tgz#f73ef3b59686df18206183551c2a835d1db1424f" + integrity sha512-n2aT0mJL9N/idCPmnBynCino1qNScfRHvr8OeskQdBNhUYAMc7cxoc8KLlv1DMWxlZUNhed+5kVdu7majVdVag== dependencies: "@octokit/core" "^3.5.0" "@octokit/plugin-paginate-rest" "^2.6.2" "@octokit/plugin-request-log" "^1.0.2" - "@octokit/plugin-rest-endpoint-methods" "5.4.1" + "@octokit/plugin-rest-endpoint-methods" "5.4.2" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -1466,12 +1466,12 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.18.0", "@octokit/types@^6.18.1": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.19.0.tgz#e2b6fedb10c8b53cf4574aa5d1a8a5611295297a" - integrity sha512-9wdZFiJfonDyU6DjIgDHxAIn92vdSUBOwAXbO2F9rOFt6DJwuAkyGLu1CvdJPphCbPBoV9iSDMX7y4fu0v6AtA== +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.18.0", "@octokit/types@^6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.19.1.tgz#6ea5f759d8d37e892e59c0a65f10892789b84a25" + integrity sha512-hMI2EokQzMG8ABWcnvcrabqQFuFHqUdN0HUOG4DPTaOtnf/jqhzhK1SHOGu5vDlI/x+hWJ60e28VxB7QhOP0CQ== dependencies: - "@octokit/openapi-types" "^8.3.0" + "@octokit/openapi-types" "^9.0.0" "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1", "@sinonjs/commons@^1.8.3": version "1.8.3" @@ -1529,10 +1529,10 @@ dependencies: "@types/glob" "*" -"@types/aws-lambda@^8.10.78": - version "8.10.78" - resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.78.tgz#dbb509837b6082962d6e7bc19f814e067ac9f5a2" - integrity sha512-+lZ8NuHT0qKEEpiZR4bF1G24SLrLwzdu0i9Cjdc3BGq6XJU6gBBYS5I0RJ8RdDCtgqgGdW8sOwsiZGHrC6mp0Q== +"@types/aws-lambda@^8.10.79": + version "8.10.79" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.79.tgz#c87fcc10500d0524f583b7fd7828025d8aa834fd" + integrity sha512-YgpllvHcDrPfzyf8/a2+ScRoEtJQjAP0pSgIneWcVpfqHGmiAaS2oMO3e4y3InJMgrMyuxGPde72CBZS/X8vJA== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.15" @@ -1688,9 +1688,9 @@ integrity sha512-uv53RrNdhbkV/3VmVCtfImfYCWC3GTTRn3R11Whni3EJ+gb178tkZBVNj2edLY5CMrB749dQi+SJkg87jsN8UQ== "@types/node@*", "@types/node@>= 8": - version "16.3.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.3.2.tgz#655432817f83b51ac869c2d51dd8305fb8342e16" - integrity sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw== + version "16.4.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.0.tgz#2c219eaa3b8d1e4d04f4dd6e40bc68c7467d5272" + integrity sha512-HrJuE7Mlqcjj+00JqMWpZ3tY8w7EUd+S0U3L1+PQSWiXZbOgyQDvi+ogoUxaHApPJq5diKxYBQwA3iIlNcPqOg== "@types/node@^10.17.60": version "10.17.60" @@ -1816,73 +1816,73 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.5.tgz#9639020e1fb65120a2f4387db8f1e8b63efdf229" integrity sha512-8NYnGOctzsI4W0ApsP/BIHD/LnxpJ6XaGf2AZmz4EyDYJMxtprN4279dLNI1CPZcwC9H18qYcaFv4bXi0wmokg== -"@typescript-eslint/eslint-plugin@^4.28.3": - version "4.28.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.3.tgz#36cdcd9ca6f9e5cb49b9f61b970b1976708d084b" - integrity sha512-jW8sEFu1ZeaV8xzwsfi6Vgtty2jf7/lJmQmDkDruBjYAbx5DA8JtbcMnP0rNPUG+oH5GoQBTSp+9613BzuIpYg== +"@typescript-eslint/eslint-plugin@^4.28.4": + version "4.28.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.4.tgz#e73c8cabbf3f08dee0e1bda65ed4e622ae8f8921" + integrity sha512-s1oY4RmYDlWMlcV0kKPBaADn46JirZzvvH7c2CtAqxCY96S538JRBAzt83RrfkDheV/+G/vWNK0zek+8TB3Gmw== dependencies: - "@typescript-eslint/experimental-utils" "4.28.3" - "@typescript-eslint/scope-manager" "4.28.3" + "@typescript-eslint/experimental-utils" "4.28.4" + "@typescript-eslint/scope-manager" "4.28.4" debug "^4.3.1" functional-red-black-tree "^1.0.1" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.28.3", "@typescript-eslint/experimental-utils@^4.0.1": - version "4.28.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.3.tgz#976f8c1191b37105fd06658ed57ddfee4be361ca" - integrity sha512-zZYl9TnrxwEPi3FbyeX0ZnE8Hp7j3OCR+ELoUfbwGHGxWnHg9+OqSmkw2MoCVpZksPCZYpQzC559Ee9pJNHTQw== +"@typescript-eslint/experimental-utils@4.28.4", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.28.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.4.tgz#9c70c35ebed087a5c70fb0ecd90979547b7fec96" + integrity sha512-OglKWOQRWTCoqMSy6pm/kpinEIgdcXYceIcH3EKWUl4S8xhFtN34GQRaAvTIZB9DD94rW7d/U7tUg3SYeDFNHA== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.28.3" - "@typescript-eslint/types" "4.28.3" - "@typescript-eslint/typescript-estree" "4.28.3" + "@typescript-eslint/scope-manager" "4.28.4" + "@typescript-eslint/types" "4.28.4" + "@typescript-eslint/typescript-estree" "4.28.4" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^4.28.3": - version "4.28.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.3.tgz#95f1d475c08268edffdcb2779993c488b6434b44" - integrity sha512-ZyWEn34bJexn/JNYvLQab0Mo5e+qqQNhknxmc8azgNd4XqspVYR5oHq9O11fLwdZMRcj4by15ghSlIEq+H5ltQ== +"@typescript-eslint/parser@^4.28.4": + version "4.28.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.4.tgz#bc462dc2779afeefdcf49082516afdc3e7b96fab" + integrity sha512-4i0jq3C6n+og7/uCHiE6q5ssw87zVdpUj1k6VlVYMonE3ILdFApEzTWgppSRG4kVNB/5jxnH+gTeKLMNfUelQA== dependencies: - "@typescript-eslint/scope-manager" "4.28.3" - "@typescript-eslint/types" "4.28.3" - "@typescript-eslint/typescript-estree" "4.28.3" + "@typescript-eslint/scope-manager" "4.28.4" + "@typescript-eslint/types" "4.28.4" + "@typescript-eslint/typescript-estree" "4.28.4" debug "^4.3.1" -"@typescript-eslint/scope-manager@4.28.3": - version "4.28.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.3.tgz#c32ad4491b3726db1ba34030b59ea922c214e371" - integrity sha512-/8lMisZ5NGIzGtJB+QizQ5eX4Xd8uxedFfMBXOKuJGP0oaBBVEMbJVddQKDXyyB0bPlmt8i6bHV89KbwOelJiQ== +"@typescript-eslint/scope-manager@4.28.4": + version "4.28.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.4.tgz#bdbce9b6a644e34f767bd68bc17bb14353b9fe7f" + integrity sha512-ZJBNs4usViOmlyFMt9X9l+X0WAFcDH7EdSArGqpldXu7aeZxDAuAzHiMAeI+JpSefY2INHrXeqnha39FVqXb8w== dependencies: - "@typescript-eslint/types" "4.28.3" - "@typescript-eslint/visitor-keys" "4.28.3" + "@typescript-eslint/types" "4.28.4" + "@typescript-eslint/visitor-keys" "4.28.4" -"@typescript-eslint/types@4.28.3": - version "4.28.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.3.tgz#8fffd436a3bada422c2c1da56060a0566a9506c7" - integrity sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA== +"@typescript-eslint/types@4.28.4": + version "4.28.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.4.tgz#41acbd79b5816b7c0dd7530a43d97d020d3aeb42" + integrity sha512-3eap4QWxGqkYuEmVebUGULMskR6Cuoc/Wii0oSOddleP4EGx1tjLnZQ0ZP33YRoMDCs5O3j56RBV4g14T4jvww== -"@typescript-eslint/typescript-estree@4.28.3": - version "4.28.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz#253d7088100b2a38aefe3c8dd7bd1f8232ec46fb" - integrity sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w== +"@typescript-eslint/typescript-estree@4.28.4": + version "4.28.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.4.tgz#252e6863278dc0727244be9e371eb35241c46d00" + integrity sha512-z7d8HK8XvCRyN2SNp+OXC2iZaF+O2BTquGhEYLKLx5k6p0r05ureUtgEfo5f6anLkhCxdHtCf6rPM1p4efHYDQ== dependencies: - "@typescript-eslint/types" "4.28.3" - "@typescript-eslint/visitor-keys" "4.28.3" + "@typescript-eslint/types" "4.28.4" + "@typescript-eslint/visitor-keys" "4.28.4" debug "^4.3.1" globby "^11.0.3" is-glob "^4.0.1" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.28.3": - version "4.28.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz#26ac91e84b23529968361045829da80a4e5251c4" - integrity sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg== +"@typescript-eslint/visitor-keys@4.28.4": + version "4.28.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.4.tgz#92dacfefccd6751cbb0a964f06683bfd72d0c4d3" + integrity sha512-NIAXAdbz1XdOuzqkJHjNKXKj8QQ4cv5cxR/g0uQhCYf/6//XrmfpaYsM7PnBcNbfvTDLUkqQ5TPNm1sozDdTWg== dependencies: - "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/types" "4.28.4" eslint-visitor-keys "^2.0.0" "@yarnpkg/lockfile@^1.1.0": @@ -1976,9 +1976,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: uri-js "^4.2.2" ajv@^8.0.1: - version "8.6.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.1.tgz#ae65764bf1edde8cd861281cda5057852364a295" - integrity sha512-42VLtQUOLefAvKFAQIxIZDaThq6om/PrfP0CYk3/vn+y4BMNkKnbli8ON2QCiHov4KkzOSJ/xSoBJdayiiYvVQ== + version "8.6.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" + integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -2269,9 +2269,9 @@ aws-sdk-mock@^5.2.1: traverse "^0.6.6" aws-sdk@^2.596.0, aws-sdk@^2.848.0, aws-sdk@^2.928.0: - version "2.945.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.945.0.tgz#ebd90832a664a192b12edf755af31be70dc18909" - integrity sha512-tkcoFAUol7c+9ZBnXsBTKfsj9bNckJ7uzj7FdD/a8AMt/6/18LlEISCiuHFl9qr8MItcON7UgnphJdFCTV7zBw== + version "2.950.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.950.0.tgz#cffb65590c50de9479c87ed04df57d355d1d8a22" + integrity sha512-iFC5fKLuFLEV27xeKmxDHDZzIDj4upm5+Ts3NpYYRbwPlOG0nE0gZzf9fRYkLkLgTr0TQq26CbKorgeo+6ailw== dependencies: buffer "4.9.2" events "1.1.1" @@ -2619,9 +2619,9 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001219: - version "1.0.30001245" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz#45b941bbd833cb0fa53861ff2bae746b3c6ca5d4" - integrity sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA== + version "1.0.30001246" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001246.tgz#fe17d9919f87124d6bb416ef7b325356d69dc76c" + integrity sha512-Tc+ff0Co/nFNbLOrziBXmMVtpt9S2c2Y+Z9Nk9Khj09J+0zR9ejvIW5qkZAErCbOrVODCx/MN+GpB5FNBs5GFA== capture-exit@^2.0.0: version "2.0.0" @@ -2958,9 +2958,9 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= constructs@^3.3.69: - version "3.3.97" - resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.97.tgz#751cc8955ee29381da9ee05f39141f02d6164aee" - integrity sha512-KDemmmUBgTDd2OKVOZkVEJM1LwP/bzm+cs2l/v1UYctIUl2X4LW+MrK7Ajd8blKkS5Vp6edkQSTSHUJnR/413w== + version "3.3.99" + resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.99.tgz#e614595cb0a1f7986f72597a4d9b98572a4ada8d" + integrity sha512-uX3bZtp6Zn53Utyurp4DrKolIDUuiDddHVTgsQ39KhVRkQ8TRMtl0nyXllysMtu78t8zLo9QygeyQ0QOBy3LHw== conventional-changelog-angular@^5.0.12: version "5.0.12" @@ -3644,9 +3644,9 @@ ejs@^2.5.2: integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== electron-to-chromium@^1.3.723: - version "1.3.775" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz#046517d1f2cea753e06fff549995b9dc45e20082" - integrity sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q== + version "1.3.782" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.782.tgz#522740fe6b4b5255ca754c68d9c406a17b0998e2" + integrity sha512-6AI2se1NqWA1SBf/tlD6tQD/6ZOt+yAhqmrTlh4XZw4/g0Mt3p6JhTQPZxRPxPZiOg0o7ss1EBP/CpYejfnoIA== emittery@^0.7.1: version "0.7.2" @@ -3876,10 +3876,10 @@ eslint-plugin-import@^2.23.4: resolve "^1.20.0" tsconfig-paths "^3.9.0" -eslint-plugin-jest@^24.3.6: - version "24.3.6" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.3.6.tgz#5f0ca019183c3188c5ad3af8e80b41de6c8e9173" - integrity sha512-WOVH4TIaBLIeCX576rLcOgjNXqP+jNlCiEmRgFTfQtJ52DpwnIQKAVGlGPAN7CZ33bW6eNfHD6s8ZbEUTQubJg== +eslint-plugin-jest@^24.3.7: + version "24.3.7" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.3.7.tgz#a4deaa9e88182b92533a9c25cc4f3c369d7f33eb" + integrity sha512-pXED2NA4q2M/5mxlN6GyuUXAFJndT0uosOkQCHaUED9pqgBPd89ZzpcZEU6c5HtZNahC00M36FkwLdDHMDqaHw== dependencies: "@typescript-eslint/experimental-utils" "^4.0.1" @@ -3942,13 +3942,13 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint@^7.30.0: - version "7.30.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.30.0.tgz#6d34ab51aaa56112fd97166226c9a97f505474f8" - integrity sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg== +eslint@^7.31.0: + version "7.31.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca" + integrity sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA== dependencies: "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.2" + "@eslint/eslintrc" "^0.4.3" "@humanwhocodes/config-array" "^0.5.0" ajv "^6.10.0" chalk "^4.0.0" @@ -6554,9 +6554,9 @@ make-fetch-happen@^8.0.9: ssri "^8.0.0" make-fetch-happen@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.0.3.tgz#57bbfb5b859807cd28005ca85aa6a72568675e24" - integrity sha512-uZ/9Cf2vKqsSWZyXhZ9wHHyckBrkntgbnqV68Bfe8zZenlf7D6yuGMXvHZQ+jSnzPkjosuNP1HGasj1J4h8OlQ== + version "9.0.4" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.0.4.tgz#ceaa100e60e0ef9e8d1ede94614bb2ba83c8bb24" + integrity sha512-sQWNKMYqSmbAGXqJg2jZ+PmHh5JAybvwu0xM8mZR/bsTjGiTASj3ldXJV7KFHy1k/IJIBkjxQFoWIVsv9+PQMg== dependencies: agentkeepalive "^4.1.3" cacache "^15.2.0" @@ -6808,9 +6808,9 @@ minipass-collect@^1.0.2: minipass "^3.0.0" minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" - integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== + version "1.3.4" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.4.tgz#63f5af868a38746ca7b33b03393ddf8c291244fe" + integrity sha512-TielGogIzbUEtd1LsjZFs47RWuHHfhl6TiCx1InVxApBAmQ8bL0dL5ilkLGcRvuyW/A9nE+Lvn855Ewz8S0PnQ== dependencies: minipass "^3.1.0" minipass-sized "^1.0.3" @@ -8911,10 +8911,10 @@ stack-utils@^2.0.2: dependencies: escape-string-regexp "^2.0.0" -standard-version@^9.3.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.3.0.tgz#2e6ff439aa49b2ea8952262f30ae6b70c02467d3" - integrity sha512-cYxxKXhYfI3S9+CA84HmrJa9B88H56V5FQ302iFF2TNwJukJCNoU8FgWt+11YtwKFXRkQQFpepC2QOF7aDq2Ow== +standard-version@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.3.1.tgz#786c16c318847f58a31a2434f97e8db33a635853" + integrity sha512-5qMxXw/FxLouC5nANyx/5RY1kiorJx9BppUso8gN07MG64q2uLRmrPb4KfXp3Ql4s/gxjZwZ89e0FwxeLubGww== dependencies: chalk "^2.4.2" conventional-changelog "3.1.24" From 524e532a7bc525346ea510d237130964e1f883e7 Mon Sep 17 00:00:00 2001 From: Jordie Bodlay Date: Thu, 22 Jul 2021 02:10:01 +1000 Subject: [PATCH 090/105] chore(rds): add AuroraPostgresEngineVersion 12.6 (#15686) Version 12.6 of the Aurora PostgreSQL cluster engine has been released as per https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Updates.20180305.html Screen Shot 2021-07-21 at 6 39 16 pm Can also be seen via aws CLI: ![Screen Shot 2021-07-21 at 6 38 17 pm](https://user-images.githubusercontent.com/712360/126459517-176804ac-5d72-47e6-8de7-e669e359650c.png) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/lib/cluster-engine.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index 96346bcb9e03f..2a9c3b378e1fd 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -459,6 +459,8 @@ export class AuroraPostgresEngineVersion { public static readonly VER_11_9 = AuroraPostgresEngineVersion.of('11.9', '11', { s3Import: true, s3Export: true }); /** Version "12.4". */ public static readonly VER_12_4 = AuroraPostgresEngineVersion.of('12.4', '12', { s3Import: true, s3Export: true }); + /** Version "12.6". */ + public static readonly VER_12_6 = AuroraPostgresEngineVersion.of('12.6', '12', { s3Import: true, s3Export: true }); /** * Create a new AuroraPostgresEngineVersion with an arbitrary version. From 56f3275af26b6bfbcbb65241151d8b9d092d1ce2 Mon Sep 17 00:00:00 2001 From: Jerry Kindall <52084730+Jerry-AWS@users.noreply.github.com> Date: Wed, 21 Jul 2021 15:18:08 -0700 Subject: [PATCH 091/105] docs(appmesh): Suggest improvements for App Mesh readme (#15506) Did a quick review of this README and made some generally minor suggestions. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 129 +++++++++++------------- 1 file changed, 61 insertions(+), 68 deletions(-) diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 6354289442d16..1b04f9dd93cb9 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -17,7 +17,7 @@ App Mesh gives you consistent visibility and network traffic controls for every App Mesh supports microservice applications that use service discovery naming for their components. To use App Mesh, you must have an existing application running on AWS Fargate, Amazon ECS, Amazon EKS, Kubernetes on AWS, or Amazon EC2. -For further information on **AWS AppMesh** visit the [AWS Docs for AppMesh](https://docs.aws.amazon.com/app-mesh/index.html). +For further information on **AWS App Mesh**, visit the [AWS App Mesh Documentation](https://docs.aws.amazon.com/app-mesh/index.html). ## Create the App and Stack @@ -32,7 +32,7 @@ A service mesh is a logical boundary for network traffic between the services th After you create your service mesh, you can create virtual services, virtual nodes, virtual routers, and routes to distribute traffic between the applications in your mesh. -The following example creates the `AppMesh` service mesh with the default filter of `DROP_ALL`, see [docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appmesh-mesh-egressfilter.html) here for more info on egress filters. +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', { @@ -40,7 +40,7 @@ const mesh = new Mesh(stack, 'AppMesh', { }); ``` -The mesh can also be created with the "ALLOW_ALL" egress filter by overwritting the property. +The mesh can instead be created with the `ALLOW_ALL` egress filter by providing the `egressFilter` property. ```ts const mesh = new Mesh(stack, 'AppMesh', { @@ -51,7 +51,7 @@ const mesh = new Mesh(stack, 'AppMesh', { ## Adding VirtualRouters -The _Mesh_ needs _VirtualRouters_ as logical units to route requests to _VirtualNodes_. +A _mesh_ uses _virtual routers_ as logical units to route requests to _virtual nodes_. 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. @@ -62,12 +62,10 @@ const router = mesh.addVirtualRouter('router', { }); ``` -Note that creating the router using the `addVirtualRouter()` method places it in the same Stack that the mesh belongs to -(which might be different from the current Stack). -The router can also be created using the constructor of `VirtualRouter` and passing in the mesh instead of calling the `addVirtualRouter()` method. -This is particularly useful when splitting your resources between many Stacks, -like creating the `mesh` as part of an infrastructure stack, -but the other resources, such as routers, in the application stack: +Note that creating the router using the `addVirtualRouter()` method places it in the same stack as the mesh +(which might be different from the current stack). +The router can also be created using the `VirtualRouter` constructor (passing in the mesh) instead of calling the `addVirtualRouter()` method. +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', { @@ -83,23 +81,23 @@ const router = new VirtualRouter(appStack, 'router', { }); ``` -The same is true for other `add*()` methods in the AppMesh library. +The same is true for other `add*()` methods in the App Mesh construct library. -The _VirtualRouterListener_ class provides an easy interface for defining new protocol specific listeners. -The `http()`, `http2()`, `grpc()` and `tcp()` methods are available for use. -They accept a single port parameter, that is used to define what port to match requests on. -The port parameter can be omitted, and it will default to port 8080. +The `VirtualRouterListener` class lets you define protocol-specific listeners. +The `http()`, `http2()`, `grpc()` and `tcp()` methods create listeners for the named protocols. +They accept a single parameter that defines the port to on which requests will be matched. +The port parameter defaults to 8080 if omitted. -## Adding VirtualService +## Adding a VirtualService -A virtual service is an abstraction of a real service that is provided by a virtual node directly or indirectly by means of a virtual router. Dependent services call your virtual service by its virtualServiceName, and those requests are routed to the virtual node or virtual router that is specified as the provider for the virtual service. +A _virtual service_ is an abstraction of a real service that is provided by a virtual node directly, or indirectly by means of a virtual router. Dependent services call your virtual service by its `virtualServiceName`, and those requests are routed to the virtual node or virtual router specified as the provider for the virtual service. We recommend that you use the service discovery name of the real service that you're targeting (such as `my-service.default.svc.cluster.local`). When creating a virtual service: -- If you want the virtual service to spread traffic across multiple virtual nodes, specify a Virtual router. -- If you want the virtual service to reach a virtual node directly, without a virtual router, specify a Virtual node. +- If you want the virtual service to spread traffic across multiple virtual nodes, specify a virtual router. +- If you want the virtual service to reach a virtual node directly, without a virtual router, specify a virtual node. Adding a virtual router as the provider: @@ -121,14 +119,14 @@ new appmesh.VirtualService('virtual-service', { ## Adding a VirtualNode -A `virtual node` acts as a logical pointer to a particular task group, such as an Amazon ECS service or a Kubernetes deployment. +A _virtual node_ acts as a logical pointer to a particular task group, such as an Amazon ECS service or a Kubernetes deployment. -When you create a `virtual node`, any inbound traffic that your `virtual node` expects should be specified as a listener. Any outbound traffic that your `virtual node` expects to reach should be specified as a backend. +When you create a virtual node, accept inbound traffic by specifying a *listener*. Outbound traffic that your virtual node expects to send should be specified as a *back end*. -The response metadata for your new `virtual node` contains the Amazon Resource Name (ARN) that is associated with the `virtual node`. Set this value (either the full ARN or the truncated resource name) as the APPMESH_VIRTUAL_NODE_NAME environment variable for your task group's Envoy proxy container in your task definition or pod spec. For example, the value could be mesh/default/virtualNode/simpleapp. This is then mapped to the node.id and node.cluster Envoy parameters. +The response metadata for your new virtual node contains the Amazon Resource Name (ARN) that is associated with the virtual node. Set this value (either the full ARN or the truncated resource name) as the `APPMESH_VIRTUAL_NODE_NAME` environment variable for your task group's Envoy proxy container in your task definition or pod spec. For example, the value could be `mesh/default/virtualNode/simpleapp`. This is then mapped to the `node.id` and `node.cluster` Envoy parameters. -> Note -> 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. +> **Note** +> 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'); @@ -216,22 +214,22 @@ const virtualService = new appmesh.VirtualService(stack, 'service-1', { node.addBackend(appmesh.Backend.virtualService(virtualService)); ``` -The `listeners` property can be left blank and added later with the `node.addListener()` method. The `healthcheck` and `timeout` properties are optional but if specifying a listener, the `port` must be added. +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. -The `backends` property can be added with `node.addBackend()`. We define a virtual service and add it to the virtual node to allow egress traffic to other node. +The `backends` property can be added with `node.addBackend()`. In the example, we define a virtual service and add it to the virtual node to allow egress traffic to other nodes. -The `backendDefaults` property are added to the node while creating the virtual node. These are virtual node's default settings for all backends. +The `backendDefaults` property is added to the node while creating the virtual node. These are the virtual node's default settings for all backends. ## Adding TLS to a listener -The `tls` property can be provided when creating a Virtual Node listener, or a Virtual Gateway listener to add TLS configuration. -App Mesh allows you to provide the TLS certificate to the proxy in the following ways: +The `tls` property specifies TLS configuration when creating a listener for a virtual node or a virtual gateway. +Provide the TLS certificate to the proxy in one of the following ways: -- A certificate from AWS Certificate Manager (ACM) can be used. +- A certificate from AWS Certificate Manager (ACM). -- A customer provided certificate can be specified with a `certificateChain` path file and a `privateKey` file path. +- A customer-provided certificate (specify a `certificateChain` path file and a `privateKey` file path). -- A certificate provided by a Secrets Discovery Service (SDS) endpoint over local Unix Domain Socket can be specified with its `secretName`. +- 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'; @@ -281,16 +279,16 @@ const gateway2 = new appmesh.VirtualGateway(this, 'gateway2', { ## Adding mutual TLS authentication Mutual TLS authentication is an optional component of TLS that offers two-way peer authentication. -To enable mutual TLS authentication, -add `mutualTlsCertificate` property to TLS Client Policy and/or `mutualTlsValidation` property to TLS Listener. +To enable mutual TLS authentication, add the `mutualTlsCertificate` property to TLS client policy and/or the `mutualTlsValidation` property to your TLS listener. `tls.mutualTlsValidation` and `tlsClientPolicy.mutualTlsCertificate` can be sourced from either: -- A customer provided certificate can be specified with a `certificateChain` path file and a `privateKey` file path. +- A customer-provided certificate (specify a `certificateChain` path file and a `privateKey` file path). -- A certificate provided by a Secrets Discovery Service (SDS) endpoint over local Unix Domain Socket can be specified with its `secretName`. +- A certificate provided by a Secrets Discovery Service (SDS) endpoint over local Unix Domain Socket (specify its `secretName`). -**Note**: Currently, a certificate from AWS Certificate Manager (ACM) cannot be sourced for above two properties. +> **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'; @@ -331,8 +329,8 @@ const node2 = new appmesh.VirtualNode(stack, 'node2', { ## Adding outlier detection to a Virtual Node listener -The `outlierDetection` property can be added to a Virtual Node listener to add outlier detection. The 4 parameters -(`baseEjectionDuration`, `interval`, `maxEjectionPercent`, `maxServerErrors`) are required. +The `outlierDetection` property adds outlier detection to a Virtual Node listener. The properties +`baseEjectionDuration`, `interval`, `maxEjectionPercent`, and `maxServerErrors` are required. ```typescript // Cloud Map service discovery is currently required for host ejection by outlier detection @@ -356,8 +354,7 @@ const node = mesh.addVirtualNode('virtual-node', { ## Adding a connection pool to a listener -The `connectionPool` property can be added to a Virtual Node listener or Virtual Gateway listener to add a request connection pool. There are different -connection pool properties per listener protocol types. +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 // A Virtual Node with a gRPC listener with a connection pool set @@ -392,19 +389,17 @@ const gateway = new appmesh.VirtualGateway(this, 'gateway', { ## Adding a Route -A `route` is associated with a virtual router, and it's used to match requests for a virtual router and distribute traffic accordingly to its associated virtual nodes. +A _route_ matches requests with an associated virtual router and distributes traffic to its associated virtual nodes. +The route distributes matching requests to one or more target virtual nodes with relative weighting. -If your `route` matches a request, you can distribute traffic to one or more target virtual nodes with relative weighting. +The `RouteSpec` class lets you define protocol-specific route specifications. +The `tcp()`, `http()`, `http2()`, and `grpc()` methods create a specification for the named protocols. -The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs. -The `tcp()`, `http()`, `http2()`, and `grpc()` methods provide the spec necessary to define a protocol specific spec. +For HTTP-based routes, the match field can match on path (prefix, exact, or regex), HTTP method, scheme, +HTTP headers, and query parameters. By default, HTTP-based routes match all requests. -For HTTP based routes, the match field can be used to match on -path (prefix, exact, or regex), HTTP method, scheme, HTTP headers, and query parameters. -By default, an HTTP based route will match all requests. - -For gRPC based routes, the match field can be used to match on service name, method name, and metadata. -When specifying the method name, service name must also be specified. +For gRPC-based routes, the match field can match on service name, method name, and metadata. +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: @@ -452,7 +447,7 @@ router.addRoute('route-http2', { }); ``` -Add a single route with multiple targets and split traffic 50/50 +Add a single route with two targets and split traffic 50/50: ```ts router.addRoute('route-http', { @@ -538,7 +533,7 @@ router.addRoute('route-grpc-retry', { }); ``` -Add a gRPC route with time out: +Add a gRPC route with timeout: ```ts router.addRoute('route-http', { @@ -561,12 +556,12 @@ router.addRoute('route-http', { ## Adding a Virtual Gateway -A _virtual gateway_ allows resources outside your mesh to communicate to resources that are inside your mesh. +A _virtual gateway_ allows resources outside your mesh to communicate with resources inside your mesh. The virtual gateway represents an Envoy proxy running in an Amazon ECS task, in a Kubernetes service, or on an Amazon EC2 instance. -Unlike a virtual node, which represents an Envoy running with an application, a virtual gateway represents Envoy deployed by itself. +Unlike a virtual node, which represents Envoy running with an application, a virtual gateway represents Envoy deployed by itself. A virtual gateway is similar to a virtual node in that it has a listener that accepts traffic for a particular port and protocol (HTTP, HTTP2, GRPC). -The traffic that the virtual gateway receives, is directed to other services in your mesh, +Traffic received by the virtual gateway is directed to other services in your mesh using rules defined in gateway routes which can be added to your virtual gateway. Create a virtual gateway with the constructor: @@ -611,19 +606,18 @@ const gateway = mesh.addVirtualGateway('gateway', { }); ``` -The listeners field can be omitted which will default to an HTTP Listener on port 8080. +The `listeners` field defaults to an HTTP Listener on port 8080 if omitted. A gateway route can be added using the `gateway.addGatewayRoute()` method. -The `backendDefaults` property is added to the node while creating the virtual gateway. These are virtual gateway's default settings for all backends. +The `backendDefaults` property, provided when creating the virtual gateway, specifies the virtual gateway's default settings for all backends. ## Adding a Gateway Route -A _gateway route_ is attached to a virtual gateway and routes traffic to an existing virtual service. -If a route matches a request, it can distribute traffic to a target virtual service. +A _gateway route_ is attached to a virtual gateway and routes matching traffic to an existing virtual service. -For HTTP based gateway routes, the match field can be used to match on +For HTTP-based gateway routes, the `match` field can be used to match on path (prefix, exact, or regex), HTTP method, host name, HTTP headers, and query parameters. -By default, an HTTP based route will match all requests. +By default, HTTP-based gateway routes match all requests. ```ts gateway.addGatewayRoute('gateway-route-http', { @@ -636,7 +630,7 @@ 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. +For gRPC-based gateway routes, the `match` field can be used to match on service name, host name, and metadata. ```ts gateway.addGatewayRoute('gateway-route-grpc', { @@ -709,9 +703,8 @@ gateway.addGatewayRoute('gateway-route-grpc', { ## Importing Resources -Each mesh resource comes with two static methods for importing a reference to an existing App Mesh resource. -These imported resources can be used as references for other resources in your mesh. -There are two static methods, `fromArn` and `fromAttributes` where the `` is replaced with the resource name. +Each App Mesh resource class comes with two static methods, `fromArn` and `fromAttributes` (where `` is replaced with the resource name, such as `VirtualNode`) for importing a reference to an existing App Mesh resource. +These imported resources can be used with other resources in your mesh as if they were defined directly in your CDK application. ```ts const arn = "arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh/virtualNode/testNode"; @@ -725,7 +718,7 @@ appmesh.VirtualNode.fromVirtualNodeAttributes(stack, 'imported-virtual-node', { }); ``` -To import a mesh, there are two static methods, `fromMeshArn` and `fromMeshName`. +To import a mesh, again there are two static methods, `fromMeshArn` and `fromMeshName`. ```ts const arn = 'arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh'; @@ -738,7 +731,7 @@ appmesh.Mesh.fromMeshName(stack, 'imported-mesh', 'abc'); ## IAM Grants -Virtual Node and Virtual Gateway implement `grantStreamAggregatedResources` that will grant identities that are running +`VirtualNode` and `VirtualGateway` provide `grantStreamAggregatedResources` methods that grant identities that are running Envoy access to stream generated config from App Mesh. ```ts From 2e4cfaeb8612179c79e293ba52a8afcdcfd6ef52 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 22 Jul 2021 11:58:19 +0200 Subject: [PATCH 092/105] feat(pipelines): CDK Pipelines is now Generally Available (#15667) Switch the CDK Pipelines API to stable and GA. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 8 +------- packages/@aws-cdk/pipelines/package.json | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 83e53348a04e2..8eb49268b2a24 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -3,13 +3,7 @@ --- -![cdk-constructs: Developer Preview](https://img.shields.io/badge/cdk--constructs-developer--preview-informational.svg?style=for-the-badge) - -> The APIs of higher level constructs in this module are in **developer preview** before they -> become stable. We will only make breaking changes to address unforeseen API issues. Therefore, -> these APIs are not subject to [Semantic Versioning](https://semver.org/), and breaking changes -> will be announced in release notes. This means that while you may use them, you may need to -> update your source code when upgrading to a newer version of this package. +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) --- diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 25c9099e64074..5edf5a75d7f85 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -95,8 +95,8 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "license": "Apache-2.0", - "stability": "experimental", - "maturity": "developer-preview", + "stability": "stable", + "maturity": "stable", "cdk-build": { "jest": true, "env": { From 9022d6f6ebb1335e67747dfb4034dce0c81b5d27 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 22 Jul 2021 12:37:34 +0200 Subject: [PATCH 093/105] docs(pipelines): add migration guide (#15696) Add a migration guide for original to modern API. [Rendered version](https://github.com/aws/aws-cdk/blob/huijbers/pipelines-migration-guide/packages/@aws-cdk/pipelines/ORIGINAL_API.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/ORIGINAL_API.md | 183 +++++++++++++++++++- packages/@aws-cdk/pipelines/README.md | 72 +++++++- 2 files changed, 245 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/pipelines/ORIGINAL_API.md b/packages/@aws-cdk/pipelines/ORIGINAL_API.md index d46acb44af989..14152b5b7f6d6 100644 --- a/packages/@aws-cdk/pipelines/ORIGINAL_API.md +++ b/packages/@aws-cdk/pipelines/ORIGINAL_API.md @@ -2,8 +2,185 @@ This document describes the API the CDK Pipelines library originally went into Developer Preview with. The API has since been reworked, but the original one -left in place because of popular uptake. The original API still works and is -still supported, but the revised one is preferred for future projects. +left in place because of popular adoption. The original API still works and is +still supported, but the revised one is preferred for future projects as it +is more flexible and abstracts more unnecessary details from the user. + +## Migrating from the original to the modern API + +It's possible to migrate a pipeline in-place from the original to the modern API. +The changes necessary are the following: + +### The Pipeline + +Replace `new CdkPipeline` with `new CodePipeline`. Some +configuration properties have been changed: + +| Old API | New API | +|--------------------------------+------------------------------------------------------------------------------------------------| +| `cloudAssemblyArtifact` | removed | +| `sourceAction` | removed | +| `synthAction` | `synth` | +| `crossAccountKeys` | new default is `false`; specify `crossAccountKeys: true` if you need cross-account deployments | +| `cdkCliVersion` | `cliVersion` | +| `selfMutating` | `selfMutation` | +| `vpc`, `subnetSelection` | `codeBuildDefaults.vpc`, codeBuildDefaults.subnetSelection` | +| `selfMutationBuildSpec` | `selfMutationCodeBuildDefaults.partialBuildSpec` | +| `assetBuildSpec` | `assetPublishingCodeBuildDefaults.partialBuildSpec` | +| `assetPreinstallCommands` | use `assetPublishingCodeBuildDefaults.partialBuildSpec` instead | +| `singlePublisherPerType: true` | `publishAssetsInParallel: false` | +| `supportDockerAssets` | `dockerEnabledForSelfMutation` | + +### The synth + +As the argument to `synth`, use `new ShellStep` or `new CodeBuildStep`, +depending on whether or not you want to customize the AWS CodeBuild Project that gets generated. + +Contrary to `SimpleSynthAction.standardNpmSynth`, you need to specify +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({ + sourceArtifact, + cloudAssemblyArtifact, + + // Use this if you need a build step (if you're not using ts-node + // or if you have TypeScript Lambdas that need to be compiled). + buildCommand: 'npm run build', +}), +``` + +Becomes: + +```ts +new ShellStep('Synth', { + input: /* source */, + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], +}); +``` + +Instead of specifying the pipeline source with the `sourceAction` property to +the pipeline, specify it as the `input` property to the `ShellStep` instead. +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({ + actionName: 'GitHub', + output: sourceArtifact, + // Replace these with your actual GitHub project name + owner: 'OWNER', + repo: 'REPO', + branch: 'main', // default: 'master' +}), +``` + +Translates into: + +```ts +input: CodePipelineSource.gitHub('OWNER/REPO', 'main', { + authentication: SecretValue.secretsManager('GITHUB_TOKEN_NAME'), +}), +``` + +### Deployments + +Adding CDK Stages to deploy is done by calling `addStage()`, or +potentially `addWave().addStage()`. All stages inside a wave are +deployed in parallel, which was not a capability of the original API. + +| Old API | New API | +|-------------------------------+-------------------------------------------------------------------------------------------------------------------------------| +| `addApplicationStage()` | `addStage()` | +| `addStage().addApplication()` | `addStage()`. Adding multiple CDK Stages into a single Pipeline stage is not supported, add multiple Pipeline stages instead. | + +### Approvals + +Approvals are added by adding `pre` and `post` options to `addStage()`, with +steps to execute before and after the deployments, respectively. We recommend +putting manual approvals in `pre` steps, and automated approvals in `post` steps. + +#### Manual approvals + +For example, specifying a manual approval on a stage deployment in old API: + +```ts +const stage = pipeline.addApplicationStage(...); +stage.addAction(new ManualApprovalAction({ + actionName: 'ManualApproval', + runOrder: testingStage.nextSequentialRunOrder(), +})); +``` + +Becomes: + +```ts +pipeline.addStage(..., { + pre: [ + new ManualApprovalStep('ManualApproval'), + ], +}); +``` + +Note that this we've used `pre` to put the manual approval *before* a Stage +deployment (this was not possible in the old API). Be sure to put the manual +approval in the `pre` steps list of the *next* Stage to keep +it in the same location in the pipeline. + +#### Automated approvals + +For example, specifying an automated approval after a stage is deployed in the following old API: + +```ts +const stage = pipeline.addApplicationStage(...); +stage.addActions(new ShellScriptAction({ + actionName: 'MyValidation', + commands: ['curl -Ssf $VAR'], + useOutputs: { + VAR: pipeline.stackOutput(stage.cfnOutput), + }, + // Optionally specify a BuildEnvironment + environment: { ... }, +})); +``` + +Becomes: + +```ts +const stage = new MyStage(...); +pipeline.addStage(stage, { + post: [ + new CodeBuildStep('MyValidation', { + commands: ['curl -Ssf $VAR'], + envFromCfnOutput: { + VAR: stage.cfnOutput, + }, + // Optionally specify a BuildEnvironment + buildEnvironment: { ... }, + }), + ], +}); +``` + +You can also use `ShellStep` if you don't need any of the CodeBuild Project +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. + +### Custom CodePipeline Actions + +See the section [**Arbitrary CodePipeline actions** in the +main `README`](https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/pipelines/README.md#arbitrary-codepipeline-actions) for an example of how to inject arbitrary +CodeBuild Actions. ## Definining the pipeline @@ -551,4 +728,4 @@ const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), { ``` **Note**: Manual Approvals notifications only apply when an application has security -check enabled. \ No newline at end of file +check enabled. diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 8eb49268b2a24..103715704ec62 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -22,7 +22,7 @@ to the new version if possible. > allows more control of CodeBuild project generation; supports deployment > engines other than CodePipeline. > -> The README for the original API can be found in [our GitHub repository](https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/pipelines/ORIGINAL_API.md). +> The README for the original API, as well as a migration guide, can be found in [our GitHub repository](https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/pipelines/ORIGINAL_API.md). ## At a glance @@ -198,10 +198,10 @@ expected to produce the CDK Cloud Assembly as its single output (the contents of the `cdk.out` directory after running `cdk synth`). "Steps" are arbitrary actions in the pipeline, typically used to run scripts or commands. -For the synth, use a `ShellStep` and specify the commands necessary to build -your project and run `cdk synth`; the specific commands required will depend on -the programming language you are using. For a typical NPM-based project, the synth -will look like this: +For the synth, use a `ShellStep` and specify the commands necessary to install +dependencies, the CDK CLI, build your project and run `cdk synth`; the specific +commands required will depend on the programming language you are using. For a +typical NPM-based project, the synth will look like this: ```ts const source = /* the repository source */; @@ -244,6 +244,63 @@ earlier by calling `pipeline.buildPipeline()`. After you've called that method, you can inspect the constructs that were produced by accessing the properties of the `pipeline` object. +#### Commands for other languages and package managers + +The commands you pass to `new ShellStep` will be very similar to the commands +you run on your own workstation to install dependencies and synth your CDK +project. Here are some (non-exhaustive) examples for what those commands might +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', { + input: source, + commands: [ + 'yarn install --frozen-lockfile', + 'yarn build', + 'npx cdk synth', + ], + }) +}); +``` + +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', { + input: source, + commands: [ + 'pip install -r requirements.txt', + 'npm install -g aws-cdk', + 'cdk synth', + ], + }) +}); +``` + +For Java projects, remember to install the CDK CLI globally (as +there is no `package.json` to automatically install it for you), +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', { + input: source, + commands: [ + 'npm install -g aws-cdk', + 'cdk synth', + ], + }) +}); +``` + +You can adapt these examples to your own situation. + #### CodePipeline Sources In CodePipeline, *Sources* define where the source of your application lives. @@ -399,7 +456,8 @@ const pipeline = new CodePipeline(this, 'Pipeline', { Every `addStage()` and `addWave()` command takes additional options. As part of these options, you can specify `pre` and `post` steps, which are arbitrary steps that run before or after the contents of the stage or wave, respectively. You can use these to add validations like -manual or automated gates to your pipeline. +manual or automated gates to your pipeline. We recommend putting manual approval gates in the set of `pre` steps, and automated approval gates in +the set of `post` steps. The following example shows both an automated approval in the form of a `ShellStep`, and a manual approvel in the form of a `ManualApprovalStep` added to the pipeline. Both must @@ -566,7 +624,7 @@ If you want to add a type of CodePipeline action to the CDK Pipeline that doesn't have a matching class yet, you can define your own step class that extends `Step` and implements `ICodePipelineActionFactory`. -Here's a simple example that adds a Jenkins step: +Here's an example that adds a Jenkins step: ```ts class MyJenkinsStep extends Step implements ICodePipelineActionFactory { From 2c4ef0131893e77d373c52b41c62d31847023446 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Thu, 22 Jul 2021 20:32:13 +0300 Subject: [PATCH 094/105] feat(cfnspec): cloudformation spec v39.7.0 (#15719) Co-authored-by: AWS CDK Team --- .../aws-lookoutequipment/.eslintrc.js | 3 + .../@aws-cdk/aws-lookoutequipment/.gitignore | 19 + .../@aws-cdk/aws-lookoutequipment/.npmignore | 29 ++ .../@aws-cdk/aws-lookoutequipment/LICENSE | 201 +++++++++ packages/@aws-cdk/aws-lookoutequipment/NOTICE | 2 + .../@aws-cdk/aws-lookoutequipment/README.md | 20 + .../aws-lookoutequipment/jest.config.js | 2 + .../aws-lookoutequipment/lib/index.ts | 2 + .../aws-lookoutequipment/package.json | 103 +++++ .../test/lookoutequipment.test.ts | 6 + packages/@aws-cdk/cfnspec/CHANGELOG.md | 78 ++++ packages/@aws-cdk/cfnspec/cfn.version | 2 +- ...0_CloudFormationResourceSpecification.json | 388 ++++++++++++++++-- .../cfn-lint/StatefulResources/000.json | 1 + .../cloudformation-include/package.json | 2 + packages/aws-cdk-lib/package.json | 1 + packages/decdk/package.json | 1 + packages/monocdk/package.json | 1 + .../rosetta/portfolio-product.ts-fixture | 28 ++ 19 files changed, 860 insertions(+), 29 deletions(-) create mode 100644 packages/@aws-cdk/aws-lookoutequipment/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-lookoutequipment/.gitignore create mode 100644 packages/@aws-cdk/aws-lookoutequipment/.npmignore create mode 100644 packages/@aws-cdk/aws-lookoutequipment/LICENSE create mode 100644 packages/@aws-cdk/aws-lookoutequipment/NOTICE create mode 100644 packages/@aws-cdk/aws-lookoutequipment/README.md create mode 100644 packages/@aws-cdk/aws-lookoutequipment/jest.config.js create mode 100644 packages/@aws-cdk/aws-lookoutequipment/lib/index.ts create mode 100644 packages/@aws-cdk/aws-lookoutequipment/package.json create mode 100644 packages/@aws-cdk/aws-lookoutequipment/test/lookoutequipment.test.ts create mode 100644 packages/monocdk/rosetta/portfolio-product.ts-fixture diff --git a/packages/@aws-cdk/aws-lookoutequipment/.eslintrc.js b/packages/@aws-cdk/aws-lookoutequipment/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lookoutequipment/.gitignore b/packages/@aws-cdk/aws-lookoutequipment/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-lookoutequipment/.npmignore b/packages/@aws-cdk/aws-lookoutequipment/.npmignore new file mode 100644 index 0000000000000..f931fede67c44 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/.npmignore @@ -0,0 +1,29 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts diff --git a/packages/@aws-cdk/aws-lookoutequipment/LICENSE b/packages/@aws-cdk/aws-lookoutequipment/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-lookoutequipment/NOTICE b/packages/@aws-cdk/aws-lookoutequipment/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-lookoutequipment/README.md b/packages/@aws-cdk/aws-lookoutequipment/README.md new file mode 100644 index 0000000000000..9985449bd1ea8 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/README.md @@ -0,0 +1,20 @@ +# AWS::LookoutEquipment Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import lookoutequipment = require('@aws-cdk/aws-lookoutequipment'); +``` diff --git a/packages/@aws-cdk/aws-lookoutequipment/jest.config.js b/packages/@aws-cdk/aws-lookoutequipment/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lookoutequipment/lib/index.ts b/packages/@aws-cdk/aws-lookoutequipment/lib/index.ts new file mode 100644 index 0000000000000..c90623c23ff71 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::LookoutEquipment CloudFormation Resources: +export * from './lookoutequipment.generated'; diff --git a/packages/@aws-cdk/aws-lookoutequipment/package.json b/packages/@aws-cdk/aws-lookoutequipment/package.json new file mode 100644 index 0000000000000..e34187a2c7e57 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/package.json @@ -0,0 +1,103 @@ +{ + "name": "@aws-cdk/aws-lookoutequipment", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::LookoutEquipment", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.LookoutEquipment", + "packageId": "Amazon.CDK.AWS.LookoutEquipment", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.lookoutequipment", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "lookoutequipment" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-lookoutequipment", + "module": "aws_cdk.aws_lookoutequipment" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-lookoutequipment" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "cloudformation": "AWS::LookoutEquipment", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::LookoutEquipment", + "aws-lookoutequipment" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^26.0.22", + "@aws-cdk/assert-internal": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-lookoutequipment/test/lookoutequipment.test.ts b/packages/@aws-cdk/aws-lookoutequipment/test/lookoutequipment.test.ts new file mode 100644 index 0000000000000..c4505ad966984 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutequipment/test/lookoutequipment.test.ts @@ -0,0 +1,6 @@ +import '@aws-cdk/assert-internal/jest'; +import {} from '../lib'; + +test('No tests are specified for this package', () => { + expect(true).toBe(true); +}); diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 794305b7dd323..2e7ba5e22bf56 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,81 @@ +# CloudFormation Resource Specification v39.7.0 + +## New Resource Types + +* AWS::Logs::ResourcePolicy +* AWS::LookoutEquipment::InferenceScheduler + +## Attribute Changes + +* AWS::Amplify::Domain AutoSubDomainCreationPatterns.DuplicatesAllowed (__added__) +* AWS::S3::AccessPoint Arn (__added__) +* AWS::S3::AccessPoint Name (__added__) + +## Property Changes + +* AWS::Amplify::Domain AutoSubDomainCreationPatterns.DuplicatesAllowed (__added__) +* AWS::Amplify::Domain SubDomainSettings.DuplicatesAllowed (__added__) +* AWS::Cassandra::Table EncryptionSpecification (__added__) +* AWS::EC2::TransitGateway AssociationDefaultRouteTableId (__added__) +* AWS::EC2::TransitGateway PropagationDefaultRouteTableId (__added__) +* AWS::EC2::TransitGateway TransitGatewayCidrBlocks (__added__) +* AWS::EC2::TransitGateway AutoAcceptSharedAttachments.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EC2::TransitGateway DefaultRouteTableAssociation.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EC2::TransitGateway DefaultRouteTablePropagation.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EC2::TransitGateway Description.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EC2::TransitGateway DnsSupport.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EC2::TransitGateway Tags.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EC2::TransitGateway VpnEcmpSupport.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::EC2::VPCCidrBlock Ipv6CidrBlock (__added__) +* AWS::EC2::VPCCidrBlock Ipv6Pool (__added__) +* AWS::Glue::Crawler RecrawlPolicy (__added__) +* AWS::ImageBuilder::ContainerRecipe InstanceConfiguration.PrimitiveType (__deleted__) +* AWS::ImageBuilder::ImageRecipe AdditionalInstanceConfiguration (__added__) +* AWS::LookoutMetrics::Alert Action.PrimitiveType (__deleted__) +* AWS::LookoutMetrics::Alert Action.Type (__added__) +* AWS::LookoutMetrics::AnomalyDetector AnomalyDetectorConfig.PrimitiveType (__deleted__) +* AWS::LookoutMetrics::AnomalyDetector AnomalyDetectorConfig.Type (__added__) +* AWS::S3::AccessPoint Name (__deleted__) +* AWS::S3::AccessPoint PolicyStatus (__added__) +* AWS::SSMIncidents::ReplicationSet Regions.DuplicatesAllowed (__added__) + +## Property Type Changes + +* AWS::Cassandra::Table.EncryptionSpecification (__added__) +* AWS::Glue::Crawler.RecrawlPolicy (__added__) +* AWS::Glue::Database.DataLakePrincipal (__added__) +* AWS::Glue::Database.PrincipalPrivileges (__added__) +* AWS::ImageBuilder::ImageRecipe.AdditionalInstanceConfiguration (__added__) +* AWS::ImageBuilder::ImageRecipe.ComponentParameter (__added__) +* AWS::ImageBuilder::ImageRecipe.SystemsManagerAgent (__added__) +* AWS::LookoutMetrics::Alert.Action (__added__) +* AWS::LookoutMetrics::Alert.LambdaConfiguration (__added__) +* AWS::LookoutMetrics::Alert.SNSConfiguration (__added__) +* AWS::LookoutMetrics::AnomalyDetector.AnomalyDetectorConfig (__added__) +* AWS::CloudWatch::Alarm.MetricDataQuery AccountId (__added__) +* AWS::Glue::Database.DatabaseInput CreateTableDefaultPermissions (__added__) +* AWS::Glue::Partition.SchemaReference SchameVersionId (__deleted__) +* AWS::Glue::Partition.SchemaReference SchemaVersionId (__added__) +* AWS::Glue::Table.SchemaReference SchameVersionId (__deleted__) +* AWS::Glue::Table.SchemaReference SchemaVersionId (__added__) +* AWS::ImageBuilder::ImageRecipe.ComponentConfiguration Parameters (__added__) +* AWS::QuickSight::DataSet.RowLevelPermissionDataSet FormatVersion (__added__) + + # CloudFormation Resource Specification v39.5.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 21aa1374d7809..ac8cf4b235806 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -39.5.0 +39.7.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 5986caf0ad273..7ea26213ba6ce 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -10315,6 +10315,23 @@ } } }, + "AWS::Cassandra::Table.EncryptionSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cassandra-table-encryptionspecification.html", + "Properties": { + "EncryptionType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cassandra-table-encryptionspecification.html#cfn-cassandra-table-encryptionspecification-encryptiontype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "KmsKeyIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cassandra-table-encryptionspecification.html#cfn-cassandra-table-encryptionspecification-kmskeyidentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Cassandra::Table.ProvisionedThroughput": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cassandra-table-provisionedthroughput.html", "Properties": { @@ -11987,6 +12004,12 @@ "AWS::CloudWatch::Alarm.MetricDataQuery": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-alarm-metricdataquery.html", "Properties": { + "AccountId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-alarm-metricdataquery.html#cfn-cloudwatch-alarm-metricdataquery-accountid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Expression": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-alarm-metricdataquery.html#cfn-cloudwatch-alarm-metricdataquery-expression", "PrimitiveType": "String", @@ -28580,6 +28603,17 @@ } } }, + "AWS::Glue::Crawler.RecrawlPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-recrawlpolicy.html", + "Properties": { + "RecrawlBehavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-recrawlpolicy.html#cfn-glue-crawler-recrawlpolicy-recrawlbehavior", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Glue::Crawler.S3Target": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-crawler-s3target.html", "Properties": { @@ -28716,6 +28750,17 @@ } } }, + "AWS::Glue::Database.DataLakePrincipal": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-datalakeprincipal.html", + "Properties": { + "DataLakePrincipalIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-datalakeprincipal.html#cfn-glue-database-datalakeprincipal-datalakeprincipalidentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Glue::Database.DatabaseIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-databaseidentifier.html", "Properties": { @@ -28736,6 +28781,13 @@ "AWS::Glue::Database.DatabaseInput": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-databaseinput.html", "Properties": { + "CreateTableDefaultPermissions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-databaseinput.html#cfn-glue-database-databaseinput-createtabledefaultpermissions", + "ItemType": "PrincipalPrivileges", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-databaseinput.html#cfn-glue-database-databaseinput-description", "PrimitiveType": "String", @@ -28768,6 +28820,24 @@ } } }, + "AWS::Glue::Database.PrincipalPrivileges": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-principalprivileges.html", + "Properties": { + "Permissions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-principalprivileges.html#cfn-glue-database-principalprivileges-permissions", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Principal": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-database-principalprivileges.html#cfn-glue-database-principalprivileges-principal", + "Required": false, + "Type": "DataLakePrincipal", + "UpdateType": "Mutable" + } + } + }, "AWS::Glue::Job.ConnectionsList": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-job-connectionslist.html", "Properties": { @@ -29036,18 +29106,18 @@ "AWS::Glue::Partition.SchemaReference": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-partition-schemareference.html", "Properties": { - "SchameVersionId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-partition-schemareference.html#cfn-glue-partition-schemareference-schameversionid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "SchemaId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-partition-schemareference.html#cfn-glue-partition-schemareference-schemaid", "Required": false, "Type": "SchemaId", "UpdateType": "Mutable" }, + "SchemaVersionId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-partition-schemareference.html#cfn-glue-partition-schemareference-schemaversionid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "SchemaVersionNumber": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-partition-schemareference.html#cfn-glue-partition-schemareference-schemaversionnumber", "PrimitiveType": "Integer", @@ -29394,18 +29464,18 @@ "AWS::Glue::Table.SchemaReference": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-table-schemareference.html", "Properties": { - "SchameVersionId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-table-schemareference.html#cfn-glue-table-schemareference-schameversionid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "SchemaId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-table-schemareference.html#cfn-glue-table-schemareference-schemaid", "Required": false, "Type": "SchemaId", "UpdateType": "Mutable" }, + "SchemaVersionId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-table-schemareference.html#cfn-glue-table-schemareference-schemaversionid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "SchemaVersionNumber": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-table-schemareference.html#cfn-glue-table-schemareference-schemaversionnumber", "PrimitiveType": "Integer", @@ -31943,6 +32013,23 @@ } } }, + "AWS::ImageBuilder::ImageRecipe.AdditionalInstanceConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-additionalinstanceconfiguration.html", + "Properties": { + "SystemsManagerAgent": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-additionalinstanceconfiguration.html#cfn-imagebuilder-imagerecipe-additionalinstanceconfiguration-systemsmanageragent", + "Required": false, + "Type": "SystemsManagerAgent", + "UpdateType": "Mutable" + }, + "UserDataOverride": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-additionalinstanceconfiguration.html#cfn-imagebuilder-imagerecipe-additionalinstanceconfiguration-userdataoverride", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::ImageBuilder::ImageRecipe.ComponentConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentconfiguration.html", "Properties": { @@ -31951,6 +32038,31 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" + }, + "Parameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentconfiguration.html#cfn-imagebuilder-imagerecipe-componentconfiguration-parameters", + "ItemType": "ComponentParameter", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe.ComponentParameter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentparameter.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentparameter.html#cfn-imagebuilder-imagerecipe-componentparameter-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentparameter.html#cfn-imagebuilder-imagerecipe-componentparameter-value", + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" } } }, @@ -32030,6 +32142,17 @@ } } }, + "AWS::ImageBuilder::ImageRecipe.SystemsManagerAgent": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-systemsmanageragent.html", + "Properties": { + "UninstallAfterBuild": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-systemsmanageragent.html#cfn-imagebuilder-imagerecipe-systemsmanageragent-uninstallafterbuild", + "PrimitiveType": "Boolean", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::ImageBuilder::InfrastructureConfiguration.Logging": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-logging.html", "Properties": { @@ -40479,6 +40602,68 @@ } } }, + "AWS::LookoutMetrics::Alert.Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-action.html", + "Properties": { + "LambdaConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-action.html#cfn-lookoutmetrics-alert-action-lambdaconfiguration", + "Required": false, + "Type": "LambdaConfiguration", + "UpdateType": "Immutable" + }, + "SNSConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-action.html#cfn-lookoutmetrics-alert-action-snsconfiguration", + "Required": false, + "Type": "SNSConfiguration", + "UpdateType": "Immutable" + } + } + }, + "AWS::LookoutMetrics::Alert.LambdaConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-lambdaconfiguration.html", + "Properties": { + "LambdaArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-lambdaconfiguration.html#cfn-lookoutmetrics-alert-lambdaconfiguration-lambdaarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-lambdaconfiguration.html#cfn-lookoutmetrics-alert-lambdaconfiguration-rolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::LookoutMetrics::Alert.SNSConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-snsconfiguration.html", + "Properties": { + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-snsconfiguration.html#cfn-lookoutmetrics-alert-snsconfiguration-rolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SnsTopicArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-alert-snsconfiguration.html#cfn-lookoutmetrics-alert-snsconfiguration-snstopicarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::LookoutMetrics::AnomalyDetector.AnomalyDetectorConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-anomalydetector-anomalydetectorconfig.html", + "Properties": { + "AnomalyDetectorFrequency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-anomalydetector-anomalydetectorconfig.html#cfn-lookoutmetrics-anomalydetector-anomalydetectorconfig-anomalydetectorfrequency", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::LookoutMetrics::AnomalyDetector.AppFlowConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lookoutmetrics-anomalydetector-appflowconfig.html", "Properties": { @@ -50845,6 +51030,12 @@ "Required": true, "UpdateType": "Mutable" }, + "FormatVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dataset-rowlevelpermissiondataset.html#cfn-quicksight-dataset-rowlevelpermissiondataset-formatversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Namespace": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dataset-rowlevelpermissiondataset.html#cfn-quicksight-dataset-rowlevelpermissiondataset-namespace", "PrimitiveType": "String", @@ -60932,7 +61123,7 @@ } } }, - "ResourceSpecificationVersion": "39.5.0", + "ResourceSpecificationVersion": "39.7.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -61554,6 +61745,7 @@ "PrimitiveType": "String" }, "AutoSubDomainCreationPatterns": { + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Type": "List" }, @@ -61586,6 +61778,7 @@ }, "AutoSubDomainCreationPatterns": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-domain.html#cfn-amplify-domain-autosubdomaincreationpatterns", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -61611,6 +61804,7 @@ }, "SubDomainSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amplify-domain.html#cfn-amplify-domain-subdomainsettings", + "DuplicatesAllowed": true, "ItemType": "SubDomainSetting", "Required": true, "Type": "List", @@ -66353,6 +66547,12 @@ "Type": "List", "UpdateType": "Immutable" }, + "EncryptionSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cassandra-table.html#cfn-cassandra-table-encryptionspecification", + "Required": false, + "Type": "EncryptionSpecification", + "UpdateType": "Mutable" + }, "KeyspaceName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cassandra-table.html#cfn-cassandra-table-keyspacename", "PrimitiveType": "String", @@ -74251,35 +74451,41 @@ "Required": false, "UpdateType": "Immutable" }, + "AssociationDefaultRouteTableId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-associationdefaultroutetableid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "AutoAcceptSharedAttachments": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-autoacceptsharedattachments", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "DefaultRouteTableAssociation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-defaultroutetableassociation", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "DefaultRouteTablePropagation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-defaultroutetablepropagation", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-description", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "DnsSupport": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-dnssupport", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "MulticastSupport": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-multicastsupport", @@ -74287,19 +74493,32 @@ "Required": false, "UpdateType": "Immutable" }, + "PropagationDefaultRouteTableId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-propagationdefaultroutetableid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-tags", "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" + }, + "TransitGatewayCidrBlocks": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-transitgatewaycidrblocks", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" }, "VpnEcmpSupport": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgateway.html#cfn-ec2-transitgateway-vpnecmpsupport", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -74741,6 +74960,18 @@ "Required": false, "UpdateType": "Immutable" }, + "Ipv6CidrBlock": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-ipv6cidrblock", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Ipv6Pool": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-ipv6pool", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "VpcId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpccidrblock.html#cfn-ec2-vpccidrblock-vpcid", "PrimitiveType": "String", @@ -79855,6 +80086,12 @@ "Required": false, "UpdateType": "Immutable" }, + "RecrawlPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-glue-crawler.html#cfn-glue-crawler-recrawlpolicy", + "Required": false, + "Type": "RecrawlPolicy", + "UpdateType": "Mutable" + }, "Role": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-glue-crawler.html#cfn-glue-crawler-role", "PrimitiveType": "String", @@ -82165,7 +82402,6 @@ }, "InstanceConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-containerrecipe.html#cfn-imagebuilder-containerrecipe-instanceconfiguration", - "PrimitiveType": "Json", "Required": false, "Type": "InstanceConfiguration", "UpdateType": "Immutable" @@ -82410,6 +82646,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html", "Properties": { + "AdditionalInstanceConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-additionalinstanceconfiguration", + "Required": false, + "Type": "AdditionalInstanceConfiguration", + "UpdateType": "Mutable" + }, "BlockDeviceMappings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-blockdevicemappings", "ItemType": "InstanceBlockDeviceMapping", @@ -86127,6 +86369,23 @@ } } }, + "AWS::Logs::ResourcePolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html", + "Properties": { + "PolicyDocument": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policydocument", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "PolicyName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html#cfn-logs-resourcepolicy-policyname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::Logs::SubscriptionFilter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-subscriptionfilter.html", "Properties": { @@ -86156,6 +86415,72 @@ } } }, + "AWS::LookoutEquipment::InferenceScheduler": { + "Attributes": { + "InferenceSchedulerArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html", + "Properties": { + "DataDelayOffsetInMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-datadelayoffsetinminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "DataInputConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-datainputconfiguration", + "PrimitiveType": "Json", + "Required": true, + "UpdateType": "Mutable" + }, + "DataOutputConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-dataoutputconfiguration", + "PrimitiveType": "Json", + "Required": true, + "UpdateType": "Mutable" + }, + "DataUploadFrequency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-datauploadfrequency", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "InferenceSchedulerName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-inferenceschedulername", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "ModelName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-modelname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-rolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ServerSideKmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-serversidekmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutequipment-inferencescheduler.html#cfn-lookoutequipment-inferencescheduler-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::LookoutMetrics::Alert": { "Attributes": { "Arn": { @@ -86166,8 +86491,8 @@ "Properties": { "Action": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutmetrics-alert.html#cfn-lookoutmetrics-alert-action", - "PrimitiveType": "Json", "Required": true, + "Type": "Action", "UpdateType": "Immutable" }, "AlertDescription": { @@ -86206,8 +86531,8 @@ "Properties": { "AnomalyDetectorConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutmetrics-anomalydetector.html#cfn-lookoutmetrics-anomalydetector-anomalydetectorconfig", - "PrimitiveType": "Json", "Required": true, + "Type": "AnomalyDetectorConfig", "UpdateType": "Mutable" }, "AnomalyDetectorDescription": { @@ -93291,6 +93616,12 @@ }, "AWS::S3::AccessPoint": { "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "Name": { + "PrimitiveType": "String" + }, "NetworkOrigin": { "PrimitiveType": "String" } @@ -93303,14 +93634,14 @@ "Required": true, "UpdateType": "Immutable" }, - "Name": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html#cfn-s3-accesspoint-name", - "PrimitiveType": "String", + "Policy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html#cfn-s3-accesspoint-policy", + "PrimitiveType": "Json", "Required": false, "UpdateType": "Mutable" }, - "Policy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html#cfn-s3-accesspoint-policy", + "PolicyStatus": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html#cfn-s3-accesspoint-policystatus", "PrimitiveType": "Json", "Required": false, "UpdateType": "Mutable" @@ -94753,6 +95084,7 @@ }, "Regions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssmincidents-replicationset.html#cfn-ssmincidents-replicationset-regions", + "DuplicatesAllowed": false, "ItemType": "ReplicationRegion", "Required": true, "Type": "List", diff --git a/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json b/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json index 36a17855e22b7..7037f14238497 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json +++ b/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json @@ -5,6 +5,7 @@ "AWS::Cognito::UserPool" : {}, "AWS::DocDB::DBCluster" : {}, "AWS::DocDB::DBInstance" : {}, + "AWS::DynamoDB::GlobalTable" : {}, "AWS::DynamoDB::Table" : {}, "AWS::EC2::Volume" : {}, "AWS::EFS::FileSystem" : {}, diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 94c805c24cafd..f8c09b12fc8ea 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -173,6 +173,7 @@ "@aws-cdk/aws-licensemanager": "0.0.0", "@aws-cdk/aws-location": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-lookoutequipment": "0.0.0", "@aws-cdk/aws-lookoutmetrics": "0.0.0", "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", @@ -340,6 +341,7 @@ "@aws-cdk/aws-licensemanager": "0.0.0", "@aws-cdk/aws-location": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-lookoutequipment": "0.0.0", "@aws-cdk/aws-lookoutmetrics": "0.0.0", "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 1a609044fbbdb..a200e97145db6 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -251,6 +251,7 @@ "@aws-cdk/aws-location": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", + "@aws-cdk/aws-lookoutequipment": "0.0.0", "@aws-cdk/aws-lookoutmetrics": "0.0.0", "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index bb1a848f9c335..0c38c44ff0625 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -158,6 +158,7 @@ "@aws-cdk/aws-location": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", + "@aws-cdk/aws-lookoutequipment": "0.0.0", "@aws-cdk/aws-lookoutmetrics": "0.0.0", "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index dc27ea678f33c..33bbc89af3414 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -252,6 +252,7 @@ "@aws-cdk/aws-location": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", + "@aws-cdk/aws-lookoutequipment": "0.0.0", "@aws-cdk/aws-lookoutmetrics": "0.0.0", "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", diff --git a/packages/monocdk/rosetta/portfolio-product.ts-fixture b/packages/monocdk/rosetta/portfolio-product.ts-fixture new file mode 100644 index 0000000000000..20a1db30bf3ee --- /dev/null +++ b/packages/monocdk/rosetta/portfolio-product.ts-fixture @@ -0,0 +1,28 @@ +// Fixture with packages imported, but nothing else +import { Construct, Stack } from '@aws-cdk/core'; +import * as servicecatalog from '@aws-cdk/aws-servicecatalog'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const portfolio = new servicecatalog.Portfolio(this, "MyFirstPortfolio", { + displayName: "MyFirstPortfolio", + providerName: "MyTeam", + }); + + const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', { + productName: "My Product", + owner: "Product Owner", + productVersions: [ + { + productVersionName: "v1", + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl( + 'https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ServiceCatalog/Product.yaml'), + }, + ] + }); + + /// here + } +} From f59331193b5a2cc4a33d71d775f6650d66bb1bf8 Mon Sep 17 00:00:00 2001 From: Filip Piskor Date: Thu, 22 Jul 2021 19:31:13 +0100 Subject: [PATCH 095/105] fix(aws-cloudwatch): unable to use generic extended statistics for cloudwatch alarms (#15720) This change adds support for alarming on custom statistics and extends the testing done for metrics to ensure if a custom statistic is passed it preserves the case ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 3 +++ .../aws-cloudwatch/lib/private/statistic.ts | 4 ++-- .../aws-cloudwatch/test/alarm.test.ts | 20 +++++++++++++++++++ .../aws-cloudwatch/test/metrics.test.ts | 7 ++++--- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index ef97dc3d79c7f..6b102d567125f 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -392,7 +392,10 @@ function renderIfExtendedStatistic(statistic?: string): string | undefined { // Already percentile. Avoid parsing because we might get into // floating point rounding issues, return as-is but lowercase the p. return statistic.toLowerCase(); + } else if (parsed.type === 'generic') { + return statistic; } + return undefined; } diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts b/packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts index 8f1aae4ff99e6..a306c1d925817 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/private/statistic.ts @@ -53,13 +53,13 @@ export function parseStatistic(stat: string): SimpleStatistic | PercentileStatis return { type: 'generic', - statistic: lowerStat, + statistic: stat, }; } export function normalizeStatistic(stat: string): string { const parsed = parseStatistic(stat); - if (parsed.type === 'simple') { + if (parsed.type === 'simple' || parsed.type === 'generic') { return parsed.statistic; } else { // Already percentile. Avoid parsing because we might get into diff --git a/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts index c143e8baec65b..bfe3eca913b67 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts @@ -257,6 +257,26 @@ describe('Alarm', () => { }); + + test('can use a generic string for extended statistic to make alarm', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + testMetric.createAlarm(stack, 'Alarm', { + threshold: 1000, + evaluationPeriods: 2, + statistic: 'tm99.9999999999', + }); + + // THEN + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + Statistic: ABSENT, + ExtendedStatistic: 'tm99.9999999999', + }); + + }); + }); class TestAlarmAction implements IAlarmAction { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts index dc958860b6a4c..ff1620f91ed50 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts @@ -232,12 +232,13 @@ describe('Metrics', () => { }); test('metric accepts a variety of statistics', () => { - new Metric({ + const customStat = 'myCustomStatistic'; + const metric = new Metric({ namespace: 'Test', metricName: 'Metric', - statistic: 'myCustomStatistic', + statistic: customStat, }); - + expect(metric.statistic).toEqual(customStat); }); }); From c92548b2242478d22db030842014e7646715c2ef Mon Sep 17 00:00:00 2001 From: arcrank Date: Thu, 22 Jul 2021 15:13:56 -0400 Subject: [PATCH 096/105] feat(servicecatalog): add ability to set launch Role and deploy with StackSets (#15678) Adds 2 constraints, launch role and stackset. Users can specify a specific role users must assume when launching product. StackSets deployments allows you to deploy products using Cloudformation StackSets. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-servicecatalog/README.md | 72 +++++++-- .../aws-servicecatalog/lib/constraints.ts | 33 +++++ .../aws-servicecatalog/lib/portfolio.ts | 35 ++++- .../lib/private/association-manager.ts | 88 +++++++++-- .../test/integ.portfolio.expected.json | 120 +++++++++++++++ .../test/integ.portfolio.ts | 23 +++ .../aws-servicecatalog/test/portfolio.test.ts | 138 ++++++++++++++++++ 7 files changed, 484 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index d2cd893efb4e8..b90748a2125ad 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -33,8 +33,11 @@ enables organizations to create and manage catalogs of products for their end us - [Adding a product to a portfolio](#adding-a-product-to-a-portfolio) - [TagOptions](#tag-options) - [Constraints](#constraints) - - [Notify on stack events](#notify-on-stack-events) - [Tag update constraint](#tag-update-constraint) + - [Notify on stack events](#notify-on-stack-events) + - [Set launch role](#set-launch-role) + - [Deploy with StackSets](#deploy-with-stacksets) + The `@aws-cdk/aws-servicecatalog` package contains resources that enable users to automate governance and management of their AWS resources at scale. @@ -186,6 +189,27 @@ If a misconfigured constraint is added, `synth` will fail with an error message. Read more at [Service Catalog Constraints](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/constraints.html). +### Tag update constraint + +Tag update constraints allow or disallow end users to update tags on resources associated with an AWS Service Catalog product upon provisioning. +By default, tag updating is not permitted. +If tag updating is allowed, then new tags associated with the product or portfolio will be applied to provisioned resources during a provisioned product update. + +```ts fixture=portfolio-product +portfolio.addProduct(product); + +portfolio.constrainTagUpdates(product); +``` + +If you want to disable this feature later on, you can update it by setting the "allow" parameter to `false`: + +```ts fixture=portfolio-product +// to disable tag updates: +portfolio.constrainTagUpdates(product, { + allow: false, +}); +``` + ### Notify on stack events Allows users to subscribe an AWS `SNS` topic to the stack events of the product. @@ -204,23 +228,49 @@ portfolio.notifyOnStackEvents(product, topic2, { }); ``` -### Tag update constraint +### Set launch role -Tag update constraints allow or disallow end users to update tags on resources associated with an AWS Service Catalog product upon provisioning. -By default, tag updating is not permitted. -If tag updating is allowed, then new tags associated with the product or portfolio will be applied to provisioned resources during a provisioned product update. +Allows you to configure a specific AWS `IAM` role that a user must assume when launching a product. +By setting this launch role, you can control what policies and privileges end users can have. +The launch role must be assumed by the service catalog principal. +You can only have one launch role set for a portfolio-product association, and you cannot set a launch role if a StackSets deployment has been configured. ```ts fixture=portfolio-product -portfolio.addProduct(product); +import * as iam from '@aws-cdk/aws-iam'; -portfolio.constrainTagUpdates(product); +const launchRole = new iam.Role(this, 'LaunchRole', { + assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'), +}); + +portfolio.setLaunchRole(product, launchRole); ``` -If you want to disable this feature later on, you can update it by setting the "allow" parameter to `false`: +See [Launch Constraint](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/constraints-launch.html) documentation +to understand permissions roles need. + +### Deploy with StackSets + +A StackSets deployment constraint allows you to configure product deployment options using +[AWS CloudFormation StackSets](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/using-stacksets.html). +You can specify multiple accounts and regions for the product launch following StackSets conventions. +There is an additional field `allowStackSetInstanceOperations` that configures ability for end users to create, edit, or delete the stacks. +By default, this field is set to `false`. +End users can manage those accounts and determine where products deploy and the order of deployment. +You can only define one StackSets deployment configuration per portfolio-product association, +and you cannot both set a launch role and StackSets deployment configuration for an assocation. ```ts fixture=portfolio-product -// to disable tag updates: -portfolio.constrainTagUpdates(product, { - allow: false, +import * as iam from '@aws-cdk/aws-iam'; + +const adminRole = new iam.Role(this, 'AdminRole', { + assumedBy: new iam.AccountRootPrincipal(), +}); + +portfolio.deployWithStackSets(product, { + accounts: ['012345678901', '012345678902', '012345678903'], + regions: ['us-west-1', 'us-east-1', 'us-west-2', 'us-east-1'], + adminRole: adminRole, + executionRoleName: 'SCStackSetExecutionRole', // Name of role deployed in end users accounts. + allowStackSetInstanceOperations: true, }); ``` diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts b/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts index 54a6e40973c4f..50cb619aa0889 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts @@ -1,3 +1,4 @@ +import * as iam from '@aws-cdk/aws-iam'; import { MessageLanguage } from './common'; /** @@ -20,6 +21,38 @@ export interface CommonConstraintOptions { readonly description?: string; } +/** + * Properties for deploying with Stackset, which creates a StackSet constraint. + */ +export interface StackSetsConstraintOptions extends CommonConstraintOptions { + /** + * List of accounts to deploy stacks to. + */ + readonly accounts: string[]; + + /** + * List of regions to deploy stacks to. + */ + readonly regions: string[]; + + /** + * IAM role used to administer the StackSets configuration. + */ + readonly adminRole: iam.IRole; + + /** + * IAM role used to provision the products in the Stacks. + */ + readonly executionRoleName: string; + + /** + * Wether to allow end users to create, update, and delete stacks. + * + * @default false + */ + readonly allowStackSetInstanceOperations?: boolean; +} + /** * Properties for ResourceUpdateConstraint. */ diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts index ba8bc6c301d12..73d4452ce348b 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts @@ -2,7 +2,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import { MessageLanguage } from './common'; -import { CommonConstraintOptions, TagUpdateConstraintOptions } from './constraints'; +import { CommonConstraintOptions, StackSetsConstraintOptions, TagUpdateConstraintOptions } from './constraints'; import { AssociationManager } from './private/association-manager'; import { hashValues } from './private/util'; import { InputValidator } from './private/validation'; @@ -88,6 +88,11 @@ export interface IPortfolio extends cdk.IResource { */ associateTagOptions(tagOptions: TagOptions): void; + /** + * Add a Resource Update Constraint. + */ + constrainTagUpdates(product: IProduct, options?: TagUpdateConstraintOptions): void; + /** * Add notifications for supplied topics on the provisioned product. * @param product A service catalog product. @@ -96,9 +101,21 @@ export interface IPortfolio extends cdk.IResource { notifyOnStackEvents(product: IProduct, topic: sns.ITopic, options?: CommonConstraintOptions): void; /** - * Add a Resource Update Constraint. + * Force users to assume a certain role when launching a product. + * + * @param product A service catalog product. + * @param launchRole The IAM role a user must assume when provisioning the product. + * @param options options for the constraint. */ - constrainTagUpdates(product: IProduct, options?: TagUpdateConstraintOptions): void; + setLaunchRole(product: IProduct, launchRole: iam.IRole, options?: CommonConstraintOptions): void; + + /** + * Configure deployment options using AWS Cloudformaiton StackSets + * + * @param product A service catalog product. + * @param options Configuration options for the constraint. + */ + deployWithStackSets(product: IProduct, options: StackSetsConstraintOptions): void; } abstract class PortfolioBase extends cdk.Resource implements IPortfolio { @@ -136,12 +153,20 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio { AssociationManager.associateTagOptions(this, tagOptions); } + public constrainTagUpdates(product: IProduct, options: TagUpdateConstraintOptions = {}): void { + AssociationManager.constrainTagUpdates(this, product, options); + } + public notifyOnStackEvents(product: IProduct, topic: sns.ITopic, options: CommonConstraintOptions = {}): void { AssociationManager.notifyOnStackEvents(this, product, topic, options); } - public constrainTagUpdates(product: IProduct, options: TagUpdateConstraintOptions = {}): void { - AssociationManager.constrainTagUpdates(this, product, options); + public setLaunchRole(product: IProduct, launchRole: iam.IRole, options: CommonConstraintOptions = {}): void { + AssociationManager.setLaunchRole(this, product, launchRole, options); + } + + public deployWithStackSets(product: IProduct, options: StackSetsConstraintOptions) { + AssociationManager.deployWithStackSets(this, product, options); } /** diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts index 519679e3df5a0..e4e8326f2bb19 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts @@ -1,9 +1,13 @@ +import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; -import { CommonConstraintOptions, TagUpdateConstraintOptions } from '../constraints'; +import { CommonConstraintOptions, StackSetsConstraintOptions, TagUpdateConstraintOptions } from '../constraints'; import { IPortfolio } from '../portfolio'; import { IProduct } from '../product'; -import { CfnLaunchNotificationConstraint, CfnPortfolioProductAssociation, CfnResourceUpdateConstraint, CfnTagOption, CfnTagOptionAssociation } from '../servicecatalog.generated'; +import { + CfnLaunchNotificationConstraint, CfnLaunchRoleConstraint, CfnPortfolioProductAssociation, + CfnResourceUpdateConstraint, CfnStackSetConstraint, CfnTagOption, CfnTagOptionAssociation, +} from '../servicecatalog.generated'; import { TagOptions } from '../tag-options'; import { hashValues } from './util'; import { InputValidator } from './validation'; @@ -28,8 +32,29 @@ export class AssociationManager { }; } + public static constrainTagUpdates(portfolio: IPortfolio, product: IProduct, options: TagUpdateConstraintOptions): void { + this.validateCommonConstraintOptions(portfolio, product, options); + const association = this.associateProductWithPortfolio(portfolio, product); + const constructId = `ResourceUpdateConstraint${association.associationKey}`; + + if (!portfolio.node.tryFindChild(constructId)) { + const constraint = new CfnResourceUpdateConstraint(portfolio as unknown as cdk.Resource, constructId, { + acceptLanguage: options.messageLanguage, + description: options.description, + portfolioId: portfolio.portfolioId, + productId: product.productId, + tagUpdateOnProvisionedProduct: options.allow === false ? 'NOT_ALLOWED' : 'ALLOWED', + }); + + // Add dependsOn to force proper order in deployment. + constraint.addDependsOn(association.cfnPortfolioProductAssociation); + } else { + throw new Error(`Cannot have multiple tag update constraints for association ${this.prettyPrintAssociation(portfolio, product)}`); + } + } + public static notifyOnStackEvents(portfolio: IPortfolio, product: IProduct, topic: sns.ITopic, options: CommonConstraintOptions): void { - InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options.description); + this.validateCommonConstraintOptions(portfolio, product, options); const association = this.associateProductWithPortfolio(portfolio, product); const constructId = `LaunchNotificationConstraint${hashValues(topic.node.addr, topic.stack.node.addr, association.associationKey)}`; @@ -49,24 +74,57 @@ export class AssociationManager { } } - public static constrainTagUpdates(portfolio: IPortfolio, product: IProduct, options: TagUpdateConstraintOptions): void { - InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options.description); + public static setLaunchRole(portfolio: IPortfolio, product: IProduct, launchRole: iam.IRole, options: CommonConstraintOptions): void { + this.validateCommonConstraintOptions(portfolio, product, options); const association = this.associateProductWithPortfolio(portfolio, product); - const constructId = `ResourceUpdateConstraint${association.associationKey}`; + // Check if a stackset deployment constraint has already been configured. + if (portfolio.node.tryFindChild(this.stackSetConstraintLogicalId(association.associationKey))) { + throw new Error(`Cannot set launch role when a StackSet rule is already defined for association ${this.prettyPrintAssociation(portfolio, product)}`); + } + const constructId = this.launchRoleConstraintLogicalId(association.associationKey); if (!portfolio.node.tryFindChild(constructId)) { - const constraint = new CfnResourceUpdateConstraint(portfolio as unknown as cdk.Resource, constructId, { + const constraint = new CfnLaunchRoleConstraint(portfolio as unknown as cdk.Resource, constructId, { acceptLanguage: options.messageLanguage, description: options.description, portfolioId: portfolio.portfolioId, productId: product.productId, - tagUpdateOnProvisionedProduct: options.allow === false ? 'NOT_ALLOWED' : 'ALLOWED', + roleArn: launchRole.roleArn, }); // Add dependsOn to force proper order in deployment. constraint.addDependsOn(association.cfnPortfolioProductAssociation); } else { - throw new Error(`Cannot have multiple tag update constraints for association ${this.prettyPrintAssociation(portfolio, product)}`); + throw new Error(`Cannot set multiple launch roles for association ${this.prettyPrintAssociation(portfolio, product)}`); + } + } + + public static deployWithStackSets(portfolio: IPortfolio, product: IProduct, options: StackSetsConstraintOptions) { + this.validateCommonConstraintOptions(portfolio, product, options); + const association = this.associateProductWithPortfolio(portfolio, product); + // Check if a launch role has already been set. + if (portfolio.node.tryFindChild(this.launchRoleConstraintLogicalId(association.associationKey))) { + throw new Error(`Cannot configure StackSet deployment when a launch role is already defined for association ${this.prettyPrintAssociation(portfolio, product)}`); + } + + const constructId = this.stackSetConstraintLogicalId(association.associationKey); + if (!portfolio.node.tryFindChild(constructId)) { + const constraint = new CfnStackSetConstraint(portfolio as unknown as cdk.Resource, constructId, { + acceptLanguage: options.messageLanguage, + description: options.description ?? '', + portfolioId: portfolio.portfolioId, + productId: product.productId, + accountList: options.accounts, + regionList: options.regions, + adminRole: options.adminRole.roleArn, + executionRole: options.executionRoleName, + stackInstanceControl: options.allowStackSetInstanceOperations ? 'ALLOWED' : 'NOT_ALLOWED', + }); + + // Add dependsOn to force proper order in deployment. + constraint.addDependsOn(association.cfnPortfolioProductAssociation); + } else { + throw new Error(`Cannot configure multiple StackSet deployment constraints for association ${this.prettyPrintAssociation(portfolio, product)}`); } } @@ -98,7 +156,19 @@ export class AssociationManager { }; } + private static stackSetConstraintLogicalId(associationKey: string): string { + return `StackSetConstraint${associationKey}`; + } + + private static launchRoleConstraintLogicalId(associationKey:string): string { + return `LaunchRoleConstraint${associationKey}`; + } + private static prettyPrintAssociation(portfolio: IPortfolio, product: IProduct): string { return `- Portfolio: ${portfolio.node.path} | Product: ${product.node.path}`; } + + private static validateCommonConstraintOptions(portfolio: IPortfolio, product: IProduct, options: CommonConstraintOptions): void { + InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options.description); + } } diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json index d7f4510a7b01a..bc21e236fd3dd 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.expected.json @@ -182,6 +182,26 @@ "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" ] }, + "TestPortfolioLaunchRoleConstrainta0185761d2312183162C": { + "Type": "AWS::ServiceCatalog::LaunchRoleConstraint", + "Properties": { + "PortfolioId": { + "Ref": "TestPortfolio4AC794EB" + }, + "ProductId": { + "Ref": "TestProduct7606930B" + }, + "RoleArn": { + "Fn::GetAtt": [ + "LaunchRole2CFB2E44", + "Arn" + ] + } + }, + "DependsOn": [ + "TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7" + ] + }, "TagOptionc0d88a3c4b8b": { "Type": "AWS::ServiceCatalog::TagOption", "Properties": { @@ -226,6 +246,106 @@ }, "specialTopic7664DE4C": { "Type": "AWS::SNS::Topic" + }, + "LaunchRole2CFB2E44": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "servicecatalog.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "SecondTestPortfolio96C8F4BA": { + "Type": "AWS::ServiceCatalog::Portfolio", + "Properties": { + "DisplayName": "SecondTestPortfolio", + "ProviderName": "TestProvider" + } + }, + "SecondTestPortfolioPortfolioProductAssociationcda67a671c209FE862F2": { + "Type": "AWS::ServiceCatalog::PortfolioProductAssociation", + "Properties": { + "PortfolioId": { + "Ref": "SecondTestPortfolio96C8F4BA" + }, + "ProductId": { + "Ref": "TestProduct7606930B" + } + } + }, + "SecondTestPortfolioStackSetConstraintcda67a671c20A02367A6": { + "Type": "AWS::ServiceCatalog::StackSetConstraint", + "Properties": { + "AccountList": [ + "000000000000", + "111111111111", + "222222222222" + ], + "AdminRole": { + "Fn::GetAtt": [ + "AdminRole38563C57", + "Arn" + ] + }, + "Description": "", + "ExecutionRole": "StackSetExecutionRole", + "PortfolioId": { + "Ref": "SecondTestPortfolio96C8F4BA" + }, + "ProductId": { + "Ref": "TestProduct7606930B" + }, + "RegionList": [ + "us-east-1", + "us-west-2", + "eu-west-1" + ], + "StackInstanceControl": "ALLOWED" + }, + "DependsOn": [ + "SecondTestPortfolioPortfolioProductAssociationcda67a671c209FE862F2" + ] + }, + "AdminRole38563C57": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts index ae45f171a8c47..c9aa607880619 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts @@ -56,4 +56,27 @@ portfolio.notifyOnStackEvents(product, specialTopic, { messageLanguage: servicecatalog.MessageLanguage.EN, }); +const launchRole = new iam.Role(stack, 'LaunchRole', { + assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'), +}); + +portfolio.setLaunchRole(product, launchRole); + +const secondPortfolio = new servicecatalog.Portfolio(stack, 'SecondTestPortfolio', { + displayName: 'SecondTestPortfolio', + providerName: 'TestProvider', +}); + +const adminRole = new iam.Role(stack, 'AdminRole', { + assumedBy: new iam.AccountRootPrincipal(), +}); + +secondPortfolio.deployWithStackSets(product, { + accounts: ['000000000000', '111111111111', '222222222222'], + regions: ['us-east-1', 'us-west-2', 'eu-west-1'], + adminRole: adminRole, + executionRoleName: 'StackSetExecutionRole', + allowStackSetInstanceOperations: true, +}); + app.synth(); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts index a5f97dc1e7470..3c76619c03a1f 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts @@ -458,4 +458,142 @@ describe('portfolio associations and product constraints', () => { portfolio.notifyOnStackEvents(product, topic); }).toThrowError(`Topic ${topic} is already subscribed to association`); }); + + describe('portfolio constraints that have roles', () => { + let launchRole: iam.IRole, adminRole: iam.IRole; + beforeEach(() => { + adminRole = new iam.Role(stack, 'AdminRole', { + assumedBy: new iam.AccountRootPrincipal(), + }); + launchRole = new iam.Role(stack, 'LaunchRole', { + assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'), + }); + }), + + test('set a launch role constraint', () => { + portfolio.addProduct(product); + + portfolio.setLaunchRole(product, launchRole, { + description: 'set launch role description', + messageLanguage: servicecatalog.MessageLanguage.EN, + }); + + expect(stack).toHaveResourceLike('AWS::ServiceCatalog::LaunchRoleConstraint', { + PortfolioId: { Ref: 'MyPortfolio59CCA9C9' }, + ProductId: { Ref: 'MyProduct49A3C587' }, + Description: 'set launch role description', + AcceptLanguage: 'en', + RoleArn: { + 'Fn::GetAtt': ['LaunchRole2CFB2E44', 'Arn'], + }, + }); + }), + + test('set launch role constraint still adds without explicit association', () => { + portfolio.setLaunchRole(product, launchRole); + + expect(stack).toHaveResourceLike('AWS::ServiceCatalog::LaunchRoleConstraint'); + }), + + test('fails to add multiple set launch roles', () => { + const otherLaunchRole = new iam.Role(stack, 'otherLaunchRole', { + assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'), + }); + + portfolio.setLaunchRole(product, launchRole); + + expect(() => { + portfolio.setLaunchRole(product, otherLaunchRole); + }).toThrowError(/Cannot set multiple launch roles for association/); + }), + + test('fails to set launch role if stackset rule is already defined', () => { + portfolio.deployWithStackSets(product, { + accounts: ['012345678901', '012345678901'], + regions: ['us-east-1', 'us-west-2', 'eu-west-1'], + adminRole: adminRole, + executionRoleName: 'StackSetExecutionRole', + allowStackSetInstanceOperations: false, + }, + ); + + expect(() => { + portfolio.setLaunchRole(product, launchRole); + }).toThrowError(/Cannot set launch role when a StackSet rule is already defined for association/); + }), + + test('deploy with stacksets constraint', () => { + portfolio.addProduct(product); + + portfolio.deployWithStackSets(product, { + accounts: ['012345678901', '012345678901'], + regions: ['us-east-1', 'us-west-2', 'eu-west-1'], + adminRole: adminRole, + executionRoleName: 'StackSetExecutionRole', + description: 'stackset description', + messageLanguage: servicecatalog.MessageLanguage.JP, + }); + + expect(stack).toHaveResourceLike('AWS::ServiceCatalog::StackSetConstraint', { + PortfolioId: { Ref: 'MyPortfolio59CCA9C9' }, + ProductId: { Ref: 'MyProduct49A3C587' }, + AdminRole: { + 'Fn::GetAtt': [ + 'AdminRole38563C57', + 'Arn', + ], + }, + ExecutionRole: 'StackSetExecutionRole', + Description: 'stackset description', + AccountList: ['012345678901', '012345678901'], + RegionList: ['us-east-1', 'us-west-2', 'eu-west-1'], + StackInstanceControl: 'NOT_ALLOWED', + AcceptLanguage: 'jp', + }); + }), + + test('deployment with stacksets still adds without explicit association', () => { + portfolio.deployWithStackSets(product, { + accounts: ['012345678901', '012345678901'], + regions: ['us-east-1', 'us-west-2', 'eu-west-1'], + adminRole: adminRole, + executionRoleName: 'StackSetExecutionRole', + allowStackSetInstanceOperations: true, + }); + + expect(stack).toHaveResourceLike('AWS::ServiceCatalog::StackSetConstraint'); + }), + + test('fails to add multiple deploy with stackset constraints', () => { + portfolio.deployWithStackSets(product, { + accounts: ['012345678901', '012345678901'], + regions: ['us-east-1', 'us-west-2', 'eu-west-1'], + adminRole: adminRole, + executionRoleName: 'StackSetsExecutionRole', + }); + + expect(() => { + portfolio.deployWithStackSets(product, { + accounts: ['012345678901', '012345678901'], + regions: ['ap-east-1', 'ap-northeast-2', 'eu-west-1'], + adminRole: adminRole, + executionRoleName: 'StackSetExecutionRole', + }); + }).toThrowError(/Cannot configure multiple StackSet deployment constraints for association/); + }), + + test('fails to configure deployment with stacksets if a launch role has been set', () => { + portfolio.setLaunchRole(product, launchRole); + + expect(() => { + portfolio.deployWithStackSets(product, { + accounts: ['012345678901', '012345678901'], + regions: ['us-east-1', 'us-west-2', 'eu-west-1'], + adminRole: adminRole, + executionRoleName: 'StackSetExecutionRole', + allowStackSetInstanceOperations: true, + }); + }).toThrowError(/Cannot configure StackSet deployment when a launch role is already defined for association/); + }); + }); }); \ No newline at end of file From 32ed2290f8efb27bf622998f98808ff18a8cdef1 Mon Sep 17 00:00:00 2001 From: Ben Chaimberg Date: Thu, 22 Jul 2021 13:32:25 -0700 Subject: [PATCH 097/105] feat(core): lazy mappings will only synthesize if keys are unresolved (#15617) This feature adds new static methods to the CfnMapping construct that allow the creation of "lazy" mappings. A lazy mapping will only create a Mappings section in the synthesized CFN template if some "find" operation on the mapping was not able to return a value since one or more of the lookup keys were unresolved. Usage: ```ts // Register the mapping as a lazy mapping. CfnMapping.registerLazyMap('UNIQUEMAPPINGID', { TopLevelKey: { SecondLevelKey: 'value', }, }); // Later, find a value from the mapping. Since the keys are both // resolved, this returns a resolved value and does not create a // CfnMapping. CfnMapping.findInLazyMap(scope, 'UNIQUEMAPPINGID', 'TopLevelKey', 'SecondLevelKey'); // > 'value' // If we try to find a value from the mapping using unresolved keys, a // CfnMapping is created and a Fn::FindInMap is returned. CfnMapping.findInLazyMap(scope, 'UNIQUEMAPPINGID', 'TopLevelKey', Aws.REGION); // > { Fn::FindInMap: [ 'UNIQUEMAPPINGID', 'TopLevelKey', { Ref: 'AWS::Region' } ] } ``` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/README.md | 32 ++++ packages/@aws-cdk/core/lib/cfn-mapping.ts | 74 ++++++++-- packages/@aws-cdk/core/test/mappings.test.ts | 147 ++++++++++++++++++- packages/aws-cdk-lib/README.md | 34 ++++- 4 files changed, 269 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 44fdda5893019..a6d4d2efc5a9b 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -775,6 +775,38 @@ Mappings: us-east-2: US East (Ohio) ``` +Mappings can also be synthesized "lazily"; lazy mappings will only render a "Mappings" +section in the synthesized CloudFormation template if some `findInMap` call is unable to +immediately return a concrete value due to one or both of the keys being unresolved tokens +(some value only available at deploy-time). + +For example, the following code will not produce anything in the "Mappings" section. The +call to `findInMap` will be able to resolve the value during synthesis and simply return +`'US East (Ohio)'`. + +```ts +const regionTable = new CfnMapping(this, 'RegionTable', { + mapping: { + regionName: { + 'us-east-1': 'US East (N. Virginia)', + 'us-east-2': 'US East (Ohio)', + }, + }, + lazy: true, +}); + +regionTable.findInMap('regionName', 'us-east-2'); +``` + +On the other hand, the following code will produce the "Mappings" section shown above, +since the second-level key is an unresolved token. The call to `findInMap` will return a +token that resolves to `{ Fn::FindInMap: [ 'RegionTable', 'regionName', { Ref: AWS::Region +} ] }`. + +```ts +regionTable.findInMap('regionName', Aws.REGION); +``` + [cfn-mappings]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html ### Dynamic References diff --git a/packages/@aws-cdk/core/lib/cfn-mapping.ts b/packages/@aws-cdk/core/lib/cfn-mapping.ts index 196baf3aadb96..18b670a4ff927 100644 --- a/packages/@aws-cdk/core/lib/cfn-mapping.ts +++ b/packages/@aws-cdk/core/lib/cfn-mapping.ts @@ -1,8 +1,11 @@ import { Construct } from 'constructs'; +import { Annotations } from './annotations'; import { CfnRefElement } from './cfn-element'; import { Fn } from './cfn-fn'; import { Token } from './token'; +type Mapping = { [k1: string]: { [k2: string]: any } }; + export interface CfnMappingProps { /** * Mapping of key to a set of corresponding set of named values. @@ -14,18 +17,34 @@ export interface CfnMappingProps { * * @default - No mapping. */ - readonly mapping?: { [k1: string]: { [k2: string]: any } }; + readonly mapping?: Mapping; + + /* + * Synthesize this map in a lazy fashion. + * + * Lazy maps will only synthesize a mapping if a `findInMap` operation is unable to + * immediately return a value because one or both of the requested keys are unresolved + * tokens. In this case, `findInMap` will return a `Fn::FindInMap` CloudFormation + * intrinsic. + * + * @default false + */ + readonly lazy?: boolean; } /** * Represents a CloudFormation mapping. */ export class CfnMapping extends CfnRefElement { - private mapping: { [k1: string]: { [k2: string]: any } } = { }; + private mapping: Mapping; + private readonly lazy?: boolean; + private lazyRender = false; + private lazyInformed = false; constructor(scope: Construct, id: string, props: CfnMappingProps = {}) { super(scope, id); - this.mapping = props.mapping || { }; + this.mapping = props.mapping ?? { }; + this.lazy = props.lazy; } /** @@ -43,16 +62,25 @@ export class CfnMapping extends CfnRefElement { * @returns A reference to a value in the map based on the two keys. */ public findInMap(key1: string, key2: string): string { - // opportunistically check that the key exists (if the key does not contain tokens) - if (!Token.isUnresolved(key1) && !(key1 in this.mapping)) { - throw new Error(`Mapping doesn't contain top-level key '${key1}'`); + let fullyResolved = false; + if (!Token.isUnresolved(key1)) { + if (!(key1 in this.mapping)) { + throw new Error(`Mapping doesn't contain top-level key '${key1}'`); + } + if (!Token.isUnresolved(key2)) { + if (!(key2 in this.mapping[key1])) { + throw new Error(`Mapping doesn't contain second-level key '${key2}'`); + } + fullyResolved = true; + } } - - // opportunistically check that the second key exists (if the key does not contain tokens) - if (!Token.isUnresolved(key1) && !Token.isUnresolved(key2) && !(key2 in this.mapping[key1])) { - throw new Error(`Mapping doesn't contain second-level key '${key2}'`); + if (fullyResolved) { + if (this.lazy) { + return this.mapping[key1][key2]; + } + } else { + this.lazyRender = true; } - return Fn.findInMap(this.logicalId, key1, key2); } @@ -60,10 +88,24 @@ export class CfnMapping extends CfnRefElement { * @internal */ public _toCloudFormation(): object { - return { - Mappings: { - [this.logicalId]: this.mapping, - }, - }; + if (this.lazy === undefined && !this.lazyRender) { + this.informLazyUse(); + } + if (!this.lazy || (this.lazy && this.lazyRender)) { + return { + Mappings: { + [this.logicalId]: this.mapping, + }, + }; + } else { + return {}; + } + } + + private informLazyUse() { + if (!this.lazyInformed) { + Annotations.of(this).addInfo('Consider making this CfnMapping a lazy mapping by providing `lazy: true`: either no findInMap was called or every findInMap could be immediately resolved without using Fn::FindInMap'); + } + this.lazyInformed = true; } } diff --git a/packages/@aws-cdk/core/test/mappings.test.ts b/packages/@aws-cdk/core/test/mappings.test.ts index 7dd67fbcead45..4b2e15c4d4bf2 100644 --- a/packages/@aws-cdk/core/test/mappings.test.ts +++ b/packages/@aws-cdk/core/test/mappings.test.ts @@ -1,5 +1,7 @@ +import { ArtifactMetadataEntryType } from '@aws-cdk/cloud-assembly-schema'; +import { CloudAssembly } from '@aws-cdk/cx-api'; import { nodeunitShim, Test } from 'nodeunit-shim'; -import { Aws, CfnMapping, CfnResource, Fn, Stack } from '../lib'; +import { App, Aws, CfnMapping, CfnResource, Fn, Stack } from '../lib'; import { toCloudFormation } from './util'; nodeunitShim({ @@ -78,6 +80,13 @@ nodeunitShim({ const expected = { 'Fn::FindInMap': ['mapping', 'instanceCount', { Ref: 'AWS::Region' }] }; test.deepEqual(stack.resolve(v1), expected); test.deepEqual(stack.resolve(v2), expected); + test.deepEqual(toCloudFormation(stack).Mappings, { + mapping: { + instanceCount: { + 'us-east-1': 12, + }, + }, + }); test.done(); }, @@ -99,6 +108,13 @@ nodeunitShim({ test.deepEqual(stack.resolve(v), { 'Fn::FindInMap': ['mapping', { Ref: 'AWS::Region' }, 'size'], }); + test.deepEqual(toCloudFormation(stack).Mappings, { + mapping: { + 'us-east-1': { + size: 12, + }, + }, + }); test.done(); }, @@ -119,6 +135,135 @@ nodeunitShim({ // THEN test.throws(() => mapping.findInMap('not-found', Aws.REGION), /Mapping doesn't contain top-level key 'not-found'/); test.deepEqual(stack.resolve(v), { 'Fn::FindInMap': ['mapping', 'size', { Ref: 'AWS::Region' }] }); + test.deepEqual(toCloudFormation(stack).Mappings, { + mapping: { + size: { + 'us-east-1': 12, + }, + }, + }); test.done(); }, }); + +describe('lazy mapping', () => { + let stack: Stack; + let mapping: CfnMapping; + const backing = { + TopLevelKey1: { + SecondLevelKey1: [1, 2, 3], + SecondLevelKey2: { Hello: 'World' }, + }, + }; + + beforeEach(() => { + stack = new Stack(); + mapping = new CfnMapping(stack, 'Lazy Mapping', { + mapping: backing, + lazy: true, + }); + }); + + it('does not create CfnMapping if findInMap keys can be resolved', () => { + const retrievedValue = mapping.findInMap('TopLevelKey1', 'SecondLevelKey1'); + + expect(stack.resolve(retrievedValue)).toStrictEqual([1, 2, 3]); + expect(toCloudFormation(stack)).toStrictEqual({}); + }); + + it('does not create CfnMapping if findInMap is not called', () => { + expect(toCloudFormation(stack)).toStrictEqual({}); + }); + + it('creates CfnMapping if top level key cannot be resolved', () => { + const retrievedValue = mapping.findInMap(Aws.REGION, 'SecondLevelKey1'); + + expect(stack.resolve(retrievedValue)).toStrictEqual({ 'Fn::FindInMap': ['LazyMapping', { Ref: 'AWS::Region' }, 'SecondLevelKey1'] }); + expect(toCloudFormation(stack)).toStrictEqual({ + Mappings: { + LazyMapping: backing, + }, + }); + }); + + it('creates CfnMapping if second level key cannot be resolved', () => { + const retrievedValue = mapping.findInMap('TopLevelKey1', Aws.REGION); + + expect(stack.resolve(retrievedValue)).toStrictEqual({ 'Fn::FindInMap': ['LazyMapping', 'TopLevelKey1', { Ref: 'AWS::Region' }] }); + expect(toCloudFormation(stack)).toStrictEqual({ + Mappings: { + LazyMapping: backing, + }, + }); + }); + + it('throws if keys can be resolved but are not found in backing', () => { + expect(() => mapping.findInMap('NonExistentKey', 'SecondLevelKey1')) + .toThrowError(/Mapping doesn't contain top-level key .*/); + expect(() => mapping.findInMap('TopLevelKey1', 'NonExistentKey')) + .toThrowError(/Mapping doesn't contain second-level key .*/); + }); +}); + +describe('eager by default', () => { + const backing = { + TopLevelKey1: { + SecondLevelKey1: [1, 2, 3], + SecondLevelKey2: { Hello: 'World' }, + }, + }; + + let app: App; + let stack: Stack; + let mapping: CfnMapping; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack'); + mapping = new CfnMapping(stack, 'Lazy Mapping', { + mapping: backing, + }); + }); + + it('emits warning if no findInMap called', () => { + const assembly = app.synth(); + + expect(getInfoAnnotations(assembly)).toStrictEqual([{ + path: '/Stack/Lazy Mapping', + message: 'Consider making this CfnMapping a lazy mapping by providing `lazy: true`: either no findInMap was called or every findInMap could be immediately resolved without using Fn::FindInMap', + }]); + }); + + it('emits warning if every findInMap resolves immediately', () => { + mapping.findInMap('TopLevelKey1', 'SecondLevelKey1'); + + const assembly = app.synth(); + + expect(getInfoAnnotations(assembly)).toStrictEqual([{ + path: '/Stack/Lazy Mapping', + message: 'Consider making this CfnMapping a lazy mapping by providing `lazy: true`: either no findInMap was called or every findInMap could be immediately resolved without using Fn::FindInMap', + }]); + }); + + it('does not emit warning if a findInMap could not resolve immediately', () => { + mapping.findInMap('TopLevelKey1', Aws.REGION); + + const assembly = app.synth(); + + expect(getInfoAnnotations(assembly)).toStrictEqual([]); + }); +}); + +function getInfoAnnotations(casm: CloudAssembly) { + const result = new Array<{ path: string, message: string }>(); + for (const stack of Object.values(casm.manifest.artifacts ?? {})) { + for (const [path, md] of Object.entries(stack.metadata ?? {})) { + for (const x of md) { + if (x.type === ArtifactMetadataEntryType.INFO) { + result.push({ path, message: x.data as string }); + } + } + } + } + return result; +} diff --git a/packages/aws-cdk-lib/README.md b/packages/aws-cdk-lib/README.md index 445346a4a90bc..48d4cd65f93a5 100644 --- a/packages/aws-cdk-lib/README.md +++ b/packages/aws-cdk-lib/README.md @@ -808,6 +808,38 @@ Mappings: us-east-2: US East (Ohio) ``` +Mappings can also be synthesized "lazily"; lazy mappings will only render a "Mappings" +section in the synthesized CloudFormation template if some `findInMap` call is unable to +immediately return a concrete value due to one or both of the keys being unresolved tokens +(some value only available at deploy-time). + +For example, the following code will not produce anything in the "Mappings" section. The +call to `findInMap` will be able to resolve the value during synthesis and simply return +`'US East (Ohio)'`. + +```ts +const regionTable = new CfnMapping(this, 'RegionTable', { + mapping: { + regionName: { + 'us-east-1': 'US East (N. Virginia)', + 'us-east-2': 'US East (Ohio)', + }, + }, + lazy: true, +}); + +regionTable.findInMap('regionName', 'us-east-2'); +``` + +On the other hand, the following code will produce the "Mappings" section shown above, +since the second-level key is an unresolved token. The call to `findInMap` will return a +token that resolves to `{ Fn::FindInMap: [ 'RegionTable', 'regionName', { Ref: AWS::Region +} ] }`. + +```ts +regionTable.findInMap('regionName', Aws.REGION); +``` + [cfn-mappings]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html ### Dynamic References @@ -947,4 +979,4 @@ It's possible to synthesize the project with more Resources than the allowed (or Set the context key `@aws-cdk/core:stackResourceLimit` with the proper value, being 0 for disable the limit of resources. - \ No newline at end of file + From 91cf79bc55ffd72b1c79e2218eb76921fbac32b4 Mon Sep 17 00:00:00 2001 From: Joel Cox Date: Fri, 23 Jul 2021 08:46:37 +1000 Subject: [PATCH 098/105] fix(elasticsearch): slow logs incorrectly disabled for Elasticsearch versions lower than 5.1 (#15714) Fixes #15532 As discussed in #15532, this error should not have applied to slow logs in the first place, as they're supported by all Elasticsearch versions. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 8 ++------ .../aws-elasticsearch/test/domain.test.ts | 16 ++-------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 676d6984234c4..a29b530128c51 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1415,12 +1415,8 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { // Validate feature support for the given Elasticsearch version, per // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-features-by-version.html if (elasticsearchVersionNum < 5.1) { - if ( - props.logging?.slowIndexLogEnabled - || props.logging?.appLogEnabled - || props.logging?.slowSearchLogEnabled - ) { - throw new Error('Error and slow logs publishing requires Elasticsearch version 5.1 or later.'); + if (props.logging?.appLogEnabled) { + throw new Error('Error logs publishing requires Elasticsearch version 5.1 or later.'); } if (props.encryptionAtRest?.enabled) { throw new Error('Encryption of data at rest requires Elasticsearch version 5.1 or later.'); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 038eb1ebc82db..719341aa29408 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -1317,26 +1317,14 @@ describe('custom error responses', () => { })).toThrow(/Unknown Elasticsearch version: 5\.4/); }); - test('error when log publishing is enabled for elasticsearch version < 5.1', () => { - const error = /logs publishing requires Elasticsearch version 5.1 or later/; + test('error when error log publishing is enabled for elasticsearch version < 5.1', () => { + const error = /Error logs publishing requires Elasticsearch version 5.1 or later/; expect(() => new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V2_3, logging: { appLogEnabled: true, }, })).toThrow(error); - expect(() => new Domain(stack, 'Domain2', { - version: ElasticsearchVersion.V1_5, - logging: { - slowSearchLogEnabled: true, - }, - })).toThrow(error); - expect(() => new Domain(stack, 'Domain3', { - version: ElasticsearchVersion.V1_5, - logging: { - slowIndexLogEnabled: true, - }, - })).toThrow(error); }); test('error when encryption at rest is enabled for elasticsearch version < 5.1', () => { From 1b5d525cef8ef4209074156c56077eebaa38d57c Mon Sep 17 00:00:00 2001 From: Madeline Kusters <80541297+madeline-k@users.noreply.github.com> Date: Thu, 22 Jul 2021 17:33:27 -0700 Subject: [PATCH 099/105] feat(aws-kinesisfirehose): DeliveryStream API and basic S3 destination (#15544) This PR implements the minimum DeliveryStream API and S3 destination. More features for DeliveryStream and the S3 destination will follow in future PRs. This work is being tracked in https://github.com/aws/aws-cdk/milestone/16 For more context, see: https://github.com/aws/aws-cdk/pull/15505 and the RFC: https://github.com/aws/aws-cdk-rfcs/pull/342 closes #10810, #15499 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ec2/lib/connections.ts | 3 + packages/@aws-cdk/aws-ec2/package.json | 1 - .../.eslintrc.js | 3 + .../.gitignore | 19 + .../.npmignore | 28 ++ .../aws-kinesisfirehose-destinations/LICENSE | 201 +++++++++ .../aws-kinesisfirehose-destinations/NOTICE | 2 + .../README.md | 22 + .../jest.config.js | 2 + .../lib/common.ts | 32 ++ .../lib/index.ts | 2 + .../lib/private/helpers.ts | 66 +++ .../lib/s3-bucket.ts | 42 ++ .../package.json | 119 +++++ .../rosetta/default.ts-fixture | 11 + .../test/integ.s3-bucket.expected.json | 408 ++++++++++++++++++ .../test/integ.s3-bucket.ts | 28 ++ .../test/s3-bucket.test.ts | 223 ++++++++++ .../@aws-cdk/aws-kinesisfirehose/README.md | 236 +++++++++- .../lib/delivery-stream.ts | 266 ++++++++++++ .../aws-kinesisfirehose/lib/destination.ts | 40 ++ .../@aws-cdk/aws-kinesisfirehose/lib/index.ts | 3 + .../@aws-cdk/aws-kinesisfirehose/package.json | 16 +- .../rosetta/default.ts-fixture | 11 + .../rosetta/with-bucket.ts-fixture | 13 + .../rosetta/with-delivery-stream.ts-fixture | 12 + .../rosetta/with-destination.ts-fixture | 12 + .../test/delivery-stream.test.ts | 314 ++++++++++++++ .../test/integ.delivery-stream.expected.json | 194 +++++++++ .../test/integ.delivery-stream.ts | 37 ++ .../test/kinesisfirehose.test.ts | 6 - .../region-info/build-tools/fact-tables.ts | 29 ++ .../build-tools/generate-static-data.ts | 10 +- packages/@aws-cdk/region-info/lib/fact.ts | 5 + .../@aws-cdk/region-info/lib/region-info.ts | 7 + packages/aws-cdk-lib/package.json | 1 + packages/decdk/package.json | 1 + packages/monocdk/package.json | 1 + tools/pkglint/lib/rules.ts | 1 + 39 files changed, 2414 insertions(+), 13 deletions(-) create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/.gitignore create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/.npmignore create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/LICENSE create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/NOTICE create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/README.md create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/jest.config.js create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/common.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/index.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/private/helpers.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/s3-bucket.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/package.json create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/rosetta/default.ts-fixture create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.expected.json create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose-destinations/test/s3-bucket.test.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/lib/delivery-stream.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/lib/destination.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/rosetta/default.ts-fixture create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-bucket.ts-fixture create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-delivery-stream.ts-fixture create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-destination.ts-fixture create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.expected.json create mode 100644 packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.ts delete mode 100644 packages/@aws-cdk/aws-kinesisfirehose/test/kinesisfirehose.test.ts diff --git a/packages/@aws-cdk/aws-ec2/lib/connections.ts b/packages/@aws-cdk/aws-ec2/lib/connections.ts index 0ecccea97fdb2..b68c04299cdf3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/connections.ts +++ b/packages/@aws-cdk/aws-ec2/lib/connections.ts @@ -20,6 +20,9 @@ import { ISecurityGroup } from './security-group'; * An object that has a Connections object */ export interface IConnectable { + /** + * The network connections associated with this resource. + */ readonly connections: Connections; } diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 985422e5da1fb..2f2dd7ab6e1c4 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -288,7 +288,6 @@ "props-default-doc:@aws-cdk/aws-ec2.AclPortRange.from", "props-default-doc:@aws-cdk/aws-ec2.AclPortRange.to", "docs-public-apis:@aws-cdk/aws-ec2.ConnectionRule", - "docs-public-apis:@aws-cdk/aws-ec2.IConnectable.connections", "docs-public-apis:@aws-cdk/aws-ec2.IInstance", "docs-public-apis:@aws-cdk/aws-ec2.IPrivateSubnet", "docs-public-apis:@aws-cdk/aws-ec2.IPublicSubnet", diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/.eslintrc.js b/packages/@aws-cdk/aws-kinesisfirehose-destinations/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/.gitignore b/packages/@aws-cdk/aws-kinesisfirehose-destinations/.gitignore new file mode 100644 index 0000000000000..147448f7df4fe --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/.gitignore @@ -0,0 +1,19 @@ +*.js +tsconfig.json +*.js.map +*.d.ts +*.generated.ts +dist +lib/generated/resources.ts +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/.npmignore b/packages/@aws-cdk/aws-kinesisfirehose-destinations/.npmignore new file mode 100644 index 0000000000000..aaabf1df59065 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/.npmignore @@ -0,0 +1,28 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/LICENSE b/packages/@aws-cdk/aws-kinesisfirehose-destinations/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/NOTICE b/packages/@aws-cdk/aws-kinesisfirehose-destinations/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/README.md b/packages/@aws-cdk/aws-kinesisfirehose-destinations/README.md new file mode 100644 index 0000000000000..03ef4657b3f78 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/README.md @@ -0,0 +1,22 @@ +# Amazon Kinesis Data Firehose Destinations Library + + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + +--- + + + +This library provides constructs for adding destinations to a Amazon Kinesis Data Firehose +delivery stream. Destinations can be added by specifying the `destinations` prop when +defining a delivery stream. + +See [Amazon Kinesis Data Firehose module README](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-kinesisfirehose-readme.html) for usage examples. diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/jest.config.js b/packages/@aws-cdk/aws-kinesisfirehose-destinations/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/common.ts b/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/common.ts new file mode 100644 index 0000000000000..3a97970d1ddbb --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/common.ts @@ -0,0 +1,32 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; + +/** + * Generic properties for defining a delivery stream destination. + */ +export interface CommonDestinationProps { + /** + * If true, log errors when data transformation or data delivery fails. + * + * If `logGroup` is provided, this will be implicitly set to `true`. + * + * @default true - errors are logged. + */ + readonly logging?: boolean; + + /** + * The CloudWatch log group where log streams will be created to hold error logs. + * + * @default - if `logging` is set to `true`, a log group will be created for you. + */ + readonly logGroup?: logs.ILogGroup; + + /** + * The IAM role associated with this destination. + * + * Assumed by Kinesis Data Firehose to invoke processors and write to destinations + * + * @default - a role will be created with default permissions. + */ + readonly role?: iam.IRole; +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/index.ts b/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/index.ts new file mode 100644 index 0000000000000..7297f91a768c8 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/index.ts @@ -0,0 +1,2 @@ +export * from './common'; +export * from './s3-bucket'; diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/private/helpers.ts b/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/private/helpers.ts new file mode 100644 index 0000000000000..a2032c41914a0 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/private/helpers.ts @@ -0,0 +1,66 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import * as logs from '@aws-cdk/aws-logs'; +import * as cdk from '@aws-cdk/core'; +import { Construct, Node } from 'constructs'; + +export interface DestinationLoggingProps { + /** + * If true, log errors when data transformation or data delivery fails. + * + * If `logGroup` is provided, this will be implicitly set to `true`. + * + * @default true - errors are logged. + */ + readonly logging?: boolean; + + /** + * The CloudWatch log group where log streams will be created to hold error logs. + * + * @default - if `logging` is set to `true`, a log group will be created for you. + */ + readonly logGroup?: logs.ILogGroup; + + /** + * The IAM role associated with this destination. + */ + readonly role: iam.IRole; + + /** + * The ID of the stream that is created in the log group where logs will be placed. + * + * Must be unique within the log group, so should be different every time this function is called. + */ + readonly streamId: string; +} + +export interface DestinationLoggingOutput { + /** + * Logging options that will be injected into the destination configuration. + */ + readonly loggingOptions: firehose.CfnDeliveryStream.CloudWatchLoggingOptionsProperty; + + /** + * Resources that were created by the sub-config creator that must be deployed before the delivery stream is deployed. + */ + readonly dependables: cdk.IDependable[]; +} + +export function createLoggingOptions(scope: Construct, props: DestinationLoggingProps): DestinationLoggingOutput | undefined { + if (props.logging === false && props.logGroup) { + throw new Error('logging cannot be set to false when logGroup is provided'); + } + if (props.logging !== false || props.logGroup) { + const logGroup = props.logGroup ?? Node.of(scope).tryFindChild('LogGroup') as logs.ILogGroup ?? new logs.LogGroup(scope, 'LogGroup'); + const logGroupGrant = logGroup.grantWrite(props.role); + return { + loggingOptions: { + enabled: true, + logGroupName: logGroup.logGroupName, + logStreamName: logGroup.addStream(props.streamId).logStreamName, + }, + dependables: [logGroupGrant], + }; + } + return undefined; +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/s3-bucket.ts b/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/s3-bucket.ts new file mode 100644 index 0000000000000..ad3c0313ff061 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/lib/s3-bucket.ts @@ -0,0 +1,42 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Construct } from 'constructs'; +import { CommonDestinationProps } from './common'; +import { createLoggingOptions } from './private/helpers'; + +/** + * Props for defining an S3 destination of a Kinesis Data Firehose delivery stream. + */ +export interface S3BucketProps extends CommonDestinationProps { } + +/** + * An S3 bucket destination for data from a Kinesis Data Firehose delivery stream. + */ +export class S3Bucket implements firehose.IDestination { + constructor(private readonly bucket: s3.IBucket, private readonly props: S3BucketProps = {}) { } + + bind(scope: Construct, _options: firehose.DestinationBindOptions): firehose.DestinationConfig { + const role = this.props.role ?? new iam.Role(scope, 'S3 Destination Role', { + assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), + }); + + const bucketGrant = this.bucket.grantReadWrite(role); + + const { loggingOptions, dependables: loggingDependables } = createLoggingOptions(scope, { + logging: this.props.logging, + logGroup: this.props.logGroup, + role, + streamId: 'S3Destination', + }) ?? {}; + + return { + extendedS3DestinationConfiguration: { + cloudWatchLoggingOptions: loggingOptions, + roleArn: role.roleArn, + bucketArn: this.bucket.bucketArn, + }, + dependables: [bucketGrant, ...(loggingDependables ?? [])], + }; + } +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/package.json b/packages/@aws-cdk/aws-kinesisfirehose-destinations/package.json new file mode 100644 index 0000000000000..92cb7c8aa4332 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/package.json @@ -0,0 +1,119 @@ +{ + "name": "@aws-cdk/aws-kinesisfirehose-destinations", + "version": "0.0.0", + "description": "CDK Destinations Constructs for AWS Kinesis Firehose", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.kinesisfirehose.destinations", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "kinesisfirehose-destinations" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.KinesisFirehose.Destinations", + "packageId": "Amazon.CDK.AWS.KinesisFirehose.Destinations", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-kinesisfirehose-destinations", + "module": "aws_cdk.aws_kinesisfirehose_destinations", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-kinesisfirehose-destinations" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test+package": "yarn build+test && yarn package", + "build+test": "yarn build && yarn test", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "kinesisfirehose" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^26.0.24", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "cfn2ts": "0.0.0", + "jest": "^26.6.3", + "pkglint": "0.0.0", + "@aws-cdk/assert-internal": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awslint": { + "exclude": [] + }, + "awscdkio": { + "announce": false + }, + "cdk-build": { + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-kinesisfirehose-destinations/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..fe46e06908b34 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct } from '@aws-cdk/core'; +import { S3Bucket } from '@aws-cdk/aws-kinesisfirehose-destinations'; + +class Fixture extends Construct { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.expected.json b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.expected.json new file mode 100644 index 0000000000000..00bc62879e11e --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.expected.json @@ -0,0 +1,408 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "BucketAutoDeleteObjectsCustomResourceBAFD23C2": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "Bucket83908E77" + } + }, + "DependsOn": [ + "BucketPolicyE9A3008A" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "Bucket83908E77" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "LogGroupF5B46931": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LogGroupS3Destination70CE1003": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "LogGroupF5B46931" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "DeliveryStreamServiceRole964EEBCC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "DeliveryStreamS3DestinationRole500FC089": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "DeliveryStreamS3DestinationRoleDefaultPolicy3015D8C7": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LogGroupF5B46931", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "DeliveryStreamS3DestinationRoleDefaultPolicy3015D8C7", + "Roles": [ + { + "Ref": "DeliveryStreamS3DestinationRole500FC089" + } + ] + } + }, + "DeliveryStreamF6D5572D": { + "Type": "AWS::KinesisFirehose::DeliveryStream", + "Properties": { + "DeliveryStreamType": "DirectPut", + "ExtendedS3DestinationConfiguration": { + "BucketARN": { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "CloudWatchLoggingOptions": { + "Enabled": true, + "LogGroupName": { + "Ref": "LogGroupF5B46931" + }, + "LogStreamName": { + "Ref": "LogGroupS3Destination70CE1003" + } + }, + "RoleARN": { + "Fn::GetAtt": [ + "DeliveryStreamS3DestinationRole500FC089", + "Arn" + ] + } + } + }, + "DependsOn": [ + "DeliveryStreamS3DestinationRoleDefaultPolicy3015D8C7" + ] + } + }, + "Parameters": { + "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B": { + "Type": "String", + "Description": "S3 bucket for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + }, + "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51": { + "Type": "String", + "Description": "S3 key for asset version \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + }, + "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1ArtifactHash9ECACDFD": { + "Type": "String", + "Description": "Artifact hash for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + } + }, + "Mappings": { + "awscdkawskinesisfirehoseCidrBlocks": { + "af-south-1": { + "FirehoseCidrBlock": "13.244.121.224/27" + }, + "ap-east-1": { + "FirehoseCidrBlock": "18.162.221.32/27" + }, + "ap-northeast-1": { + "FirehoseCidrBlock": "13.113.196.224/27" + }, + "ap-northeast-2": { + "FirehoseCidrBlock": "13.209.1.64/27" + }, + "ap-northeast-3": { + "FirehoseCidrBlock": "13.208.177.192/27" + }, + "ap-south-1": { + "FirehoseCidrBlock": "13.232.67.32/27" + }, + "ap-southeast-1": { + "FirehoseCidrBlock": "13.228.64.192/27" + }, + "ap-southeast-2": { + "FirehoseCidrBlock": "13.210.67.224/27" + }, + "ca-central-1": { + "FirehoseCidrBlock": "35.183.92.128/27" + }, + "cn-north-1": { + "FirehoseCidrBlock": "52.81.151.32/27" + }, + "cn-northwest-1": { + "FirehoseCidrBlock": "161.189.23.64/27" + }, + "eu-central-1": { + "FirehoseCidrBlock": "35.158.127.160/27" + }, + "eu-north-1": { + "FirehoseCidrBlock": "13.53.63.224/27" + }, + "eu-south-1": { + "FirehoseCidrBlock": "15.161.135.128/27" + }, + "eu-west-1": { + "FirehoseCidrBlock": "52.19.239.192/27" + }, + "eu-west-2": { + "FirehoseCidrBlock": "18.130.1.96/27" + }, + "eu-west-3": { + "FirehoseCidrBlock": "35.180.1.96/27" + }, + "me-south-1": { + "FirehoseCidrBlock": "15.185.91.0/27" + }, + "sa-east-1": { + "FirehoseCidrBlock": "18.228.1.128/27" + }, + "us-east-1": { + "FirehoseCidrBlock": "52.70.63.192/27" + }, + "us-east-2": { + "FirehoseCidrBlock": "13.58.135.96/27" + }, + "us-gov-east-1": { + "FirehoseCidrBlock": "18.253.138.96/27" + }, + "us-gov-west-1": { + "FirehoseCidrBlock": "52.61.204.160/27" + }, + "us-west-1": { + "FirehoseCidrBlock": "13.57.135.192/27" + }, + "us-west-2": { + "FirehoseCidrBlock": "52.89.255.224/27" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.ts b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.ts new file mode 100644 index 0000000000000..222eaa6c0fb84 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import * as logs from '@aws-cdk/aws-logs'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as destinations from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-firehose-delivery-stream-s3-all-properties'); + +const bucket = new s3.Bucket(stack, 'Bucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, +}); + +const logGroup = new logs.LogGroup(stack, 'LogGroup', { + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [new destinations.S3Bucket(bucket, { + logging: true, + logGroup: logGroup, + })], +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/s3-bucket.test.ts b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/s3-bucket.test.ts new file mode 100644 index 0000000000000..50e891b3091d1 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/s3-bucket.test.ts @@ -0,0 +1,223 @@ +import '@aws-cdk/assert-internal/jest'; +import { ABSENT, MatchStyle, ResourcePart, anything, arrayWith } from '@aws-cdk/assert-internal'; +import * as iam from '@aws-cdk/aws-iam'; +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import * as logs from '@aws-cdk/aws-logs'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as firehosedestinations from '../lib'; + +describe('S3 destination', () => { + let stack: cdk.Stack; + let bucket: s3.IBucket; + let destinationRole: iam.IRole; + + beforeEach(() => { + stack = new cdk.Stack(); + bucket = new s3.Bucket(stack, 'Bucket'); + destinationRole = new iam.Role(stack, 'Destination Role', { + assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), + }); + }); + + it('provides defaults when no configuration is provided', () => { + new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [new firehosedestinations.S3Bucket(bucket, { role: destinationRole })], + }); + + expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { + ExtendedS3DestinationConfiguration: { + BucketARN: stack.resolve(bucket.bucketArn), + CloudWatchLoggingOptions: { + Enabled: true, + LogGroupName: anything(), + LogStreamName: anything(), + }, + RoleARN: stack.resolve(destinationRole.roleArn), + }, + }); + expect(stack).toHaveResource('AWS::Logs::LogGroup'); + expect(stack).toHaveResource('AWS::Logs::LogStream'); + }); + + it('creates a role when none is provided', () => { + + new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [new firehosedestinations.S3Bucket(bucket)], + }); + + expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + ExtendedS3DestinationConfiguration: { + RoleARN: { + 'Fn::GetAtt': [ + 'DeliveryStreamS3DestinationRoleD96B8345', + 'Arn', + ], + }, + }, + }); + expect(stack).toMatchTemplate({ + ['DeliveryStreamS3DestinationRoleD96B8345']: { + Type: 'AWS::IAM::Role', + }, + }, MatchStyle.SUPERSET); + }); + + it('grants read/write access to the bucket', () => { + const destination = new firehosedestinations.S3Bucket(bucket, { role: destinationRole }); + + new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [destination], + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Roles: [stack.resolve(destinationRole.roleName)], + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject*', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + stack.resolve(bucket.bucketArn), + { 'Fn::Join': ['', [stack.resolve(bucket.bucketArn), '/*']] }, + ], + }, + ], + }, + }); + }); + + it('bucket and log group grants are depended on by delivery stream', () => { + const logGroup = logs.LogGroup.fromLogGroupName(stack, 'Log Group', 'evergreen'); + const destination = new firehosedestinations.S3Bucket(bucket, { role: destinationRole, logGroup }); + new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [destination], + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyName: 'DestinationRoleDefaultPolicy1185C75D', + Roles: [stack.resolve(destinationRole.roleName)], + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject*', + 's3:Abort*', + ], + Effect: 'Allow', + Resource: [ + stack.resolve(bucket.bucketArn), + { 'Fn::Join': ['', [stack.resolve(bucket.bucketArn), '/*']] }, + ], + }, + { + Action: [ + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ], + Effect: 'Allow', + Resource: stack.resolve(logGroup.logGroupArn), + }, + ], + }, + }); + expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + DependsOn: ['DestinationRoleDefaultPolicy1185C75D'], + }, ResourcePart.CompleteDefinition); + }); + + describe('logging', () => { + it('creates resources and configuration by default', () => { + new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [new firehosedestinations.S3Bucket(bucket)], + }); + + expect(stack).toHaveResource('AWS::Logs::LogGroup'); + expect(stack).toHaveResource('AWS::Logs::LogStream'); + expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + ExtendedS3DestinationConfiguration: { + CloudWatchLoggingOptions: { + Enabled: true, + LogGroupName: anything(), + LogStreamName: anything(), + }, + }, + }); + }); + + it('does not create resources or configuration if disabled', () => { + new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [new firehosedestinations.S3Bucket(bucket, { logging: false })], + }); + + expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + ExtendedS3DestinationConfiguration: { + CloudWatchLoggingOptions: ABSENT, + }, + }); + }); + + it('uses provided log group', () => { + const logGroup = new logs.LogGroup(stack, 'Log Group'); + + new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [new firehosedestinations.S3Bucket(bucket, { logGroup })], + }); + + expect(stack).toCountResources('AWS::Logs::LogGroup', 1); + expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + ExtendedS3DestinationConfiguration: { + CloudWatchLoggingOptions: { + Enabled: true, + LogGroupName: stack.resolve(logGroup.logGroupName), + LogStreamName: anything(), + }, + }, + }); + }); + + it('throws error if logging disabled but log group provided', () => { + const destination = new firehosedestinations.S3Bucket(bucket, { logging: false, logGroup: new logs.LogGroup(stack, 'Log Group') }); + + expect(() => new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [destination], + })).toThrowError('logging cannot be set to false when logGroup is provided'); + }); + + it('grants log group write permissions to destination role', () => { + const logGroup = new logs.LogGroup(stack, 'Log Group'); + + new firehose.DeliveryStream(stack, 'DeliveryStream', { + destinations: [new firehosedestinations.S3Bucket(bucket, { logGroup, role: destinationRole })], + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + Roles: [stack.resolve(destinationRole.roleName)], + PolicyDocument: { + Statement: arrayWith( + { + Action: [ + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ], + Effect: 'Allow', + Resource: stack.resolve(logGroup.logGroupArn), + }, + ), + }, + }); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-kinesisfirehose/README.md b/packages/@aws-cdk/aws-kinesisfirehose/README.md index 9c4d9f96c6f36..5034ac4a0765f 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/README.md +++ b/packages/@aws-cdk/aws-kinesisfirehose/README.md @@ -9,8 +9,242 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +[Amazon Kinesis Data Firehose](https://docs.aws.amazon.com/firehose/latest/dev/what-is-this-service.html) +is a service for fully-managed delivery of real-time streaming data to storage services +such as Amazon S3, Amazon Redshift, Amazon Elasticsearch, Splunk, or any custom HTTP +endpoint or third-party services such as Datadog, Dynatrace, LogicMonitor, MongoDB, New +Relic, and Sumo Logic. + +Kinesis Data Firehose delivery streams are distinguished from Kinesis data streams in +their models of consumtpion. Whereas consumers read from a data stream by actively pulling +data from the stream, a delivery stream pushes data to its destination on a regular +cadence. This means that data streams are intended to have consumers that do on-demand +processing, like AWS Lambda or Amazon EC2. On the other hand, delivery streams are +intended to have destinations that are sources for offline processing and analytics, such +as Amazon S3 and Amazon Redshift. + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) +project. It allows you to define Kinesis Data Firehose delivery streams. + +## Defining a Delivery Stream + +In order to define a Delivery Stream, you must specify a destination. An S3 bucket can be +used as a destination. More supported destinations are covered [below](#destinations). + +```ts +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; +import * as s3 from '@aws-cdk/aws-s3'; + +const bucket = new s3.Bucket(this, 'Bucket'); +new DeliveryStream(this, 'Delivery Stream', { + destinations: [new destinations.S3Bucket(bucket)], +}); +``` + +The above example defines the following resources: + +- An S3 bucket +- A Kinesis Data Firehose delivery stream with Direct PUT as the source and CloudWatch + error logging turned on. +- An IAM role which gives the delivery stream permission to write to the S3 bucket. + +## Sources + +There are two main methods of sourcing input data: Kinesis Data Streams and via a "direct +put". This construct library currently only supports "direct put". See [#15500](https://github.com/aws/aws-cdk/issues/15500) to track the status of adding support for Kinesis Data Streams. + +See: [Sending Data to a Delivery Stream](https://docs.aws.amazon.com/firehose/latest/dev/basic-write.html) +in the *Kinesis Data Firehose Developer Guide*. + +### Direct Put + +Data must be provided via "direct put", ie., by using a `PutRecord` or `PutRecordBatch` API call. There are a number of ways of doing +so, such as: + +- Kinesis Agent: a standalone Java application that monitors and delivers files while + handling file rotation, checkpointing, and retries. See: [Writing to Kinesis Data Firehose Using Kinesis Agent](https://docs.aws.amazon.com/firehose/latest/dev/writing-with-agents.html) + in the *Kinesis Data Firehose Developer Guide*. +- AWS SDK: a general purpose solution that allows you to deliver data to a delivery stream + from anywhere using Java, .NET, Node.js, Python, or Ruby. See: [Writing to Kinesis Data Firehose Using the AWS SDK](https://docs.aws.amazon.com/firehose/latest/dev/writing-with-sdk.html) + in the *Kinesis Data Firehose Developer Guide*. +- CloudWatch Logs: subscribe to a log group and receive filtered log events directly into + a delivery stream. See: [logs-destinations](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-logs-destinations-readme.html). +- Eventbridge: add an event rule target to send events to a delivery stream based on the + rule filtering. See: [events-targets](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-events-targets-readme.html). +- SNS: add a subscription to send all notifications from the topic to a delivery + stream. See: [sns-subscriptions](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-sns-subscriptions-readme.html). +- IoT: add an action to an IoT rule to send various IoT information to a delivery stream + +## Destinations + +The following destinations are supported. See [kinesisfirehose-destinations](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-kinesisfirehose-destinations-readme.html) +for the implementations of these destinations. + +### S3 + +Defining a delivery stream with an S3 bucket destination: + +```ts +import * as s3 from '@aws-cdk/aws-s3'; +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; + +const bucket = new s3.Bucket(this, 'Bucket'); + +const s3Destination = new destinations.S3Bucket(bucket); + +new DeliveryStream(this, 'Delivery Stream', { + destinations: [s3Destination], +}); +``` + +## Monitoring + +Kinesis Data Firehose is integrated with CloudWatch, so you can monitor the performance of +your delivery streams via logs and metrics. + +### Logs + +Kinesis Data Firehose will send logs to CloudWatch when data transformation or data +delivery fails. The CDK will enable logging by default and create a CloudWatch LogGroup +and LogStream for your Delivery Stream. + +You can provide a specific log group to specify where the CDK will create the log streams +where log events will be sent: + +```ts fixture=with-bucket +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; +import * as logs from '@aws-cdk/aws-logs'; + +const logGroup = new logs.LogGroup(this, 'Log Group'); +const destination = new destinations.S3Bucket(bucket, { + logGroup: logGroup, +}); +new DeliveryStream(this, 'Delivery Stream', { + destinations: [destination], +}); +``` + +Logging can also be disabled: + +```ts fixture=with-bucket +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; + +const destination = new destinations.S3Bucket(bucket, { + logging: false, +}); +new DeliveryStream(this, 'Delivery Stream', { + destinations: [destination], +}); +``` + +See: [Monitoring using CloudWatch Logs](https://docs.aws.amazon.com/firehose/latest/dev/monitoring-with-cloudwatch-logs.html) +in the *Kinesis Data Firehose Developer Guide*. + +## Specifying an IAM role + +The DeliveryStream class automatically creates IAM service roles with all the minimum +necessary permissions for Kinesis Data Firehose to access the resources referenced by your +delivery stream. One service role is created for the delivery stream that allows Kinesis +Data Firehose to read from a Kinesis data stream (if one is configured as the delivery +stream source) and for server-side encryption. Another service role is created for each +destination, which gives Kinesis Data Firehose write access to the destination resource, +as well as the ability to invoke data transformers and read schemas for record format +conversion. If you wish, you may specify your own IAM role for either the delivery stream +or the destination service role, or both. It must have the correct trust policy (it must +allow Kinesis Data Firehose to assume it) or delivery stream creation or data delivery +will fail. Other required permissions to destination resources, encryption keys, etc., +will be provided automatically. + +```ts fixture=with-bucket +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; +import * as iam from '@aws-cdk/aws-iam'; + +// Create service roles for the delivery stream and destination. +// These can be used for other purposes and granted access to different resources. +// They must include the Kinesis Data Firehose service principal in their trust policies. +// Two separate roles are shown below, but the same role can be used for both purposes. +const deliveryStreamRole = new iam.Role(this, 'Delivery Stream Role', { + assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), +}); +const destinationRole = new iam.Role(this, 'Destination Role', { + assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), +}); + +// Specify the roles created above when defining the destination and delivery stream. +const destination = new destinations.S3Bucket(bucket, { role: destinationRole }); +new DeliveryStream(this, 'Delivery Stream', { + destinations: [destination], + role: deliveryStreamRole, +}); +``` + +See [Controlling Access](https://docs.aws.amazon.com/firehose/latest/dev/controlling-access.html) +in the *Kinesis Data Firehose Developer Guide*. + +## Granting application access to a delivery stream + +IAM roles, users or groups which need to be able to work with delivery streams should be +granted IAM permissions. + +Any object that implements the `IGrantable` interface (ie., has an associated principal) +can be granted permissions to a delivery stream by calling: + +- `grantPutRecords(principal)` - grants the principal the ability to put records onto the + delivery stream +- `grant(principal, ...actions)` - grants the principal permission to a custom set of + actions + +```ts fixture=with-delivery-stream +import * as iam from '@aws-cdk/aws-iam'; +const lambdaRole = new iam.Role(this, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), +}); + +// Give the role permissions to write data to the delivery stream +deliveryStream.grantPutRecords(lambdaRole); +``` + +The following write permissions are provided to a service principal by the `grantPutRecords()` method: + +- `firehose:PutRecord` +- `firehose:PutRecordBatch` + +## Granting a delivery stream access to a resource + +Conversely to the above, Kinesis Data Firehose requires permissions in order for delivery +streams to interact with resources that you own. For example, if an S3 bucket is specified +as a destination of a delivery stream, the delivery stream must be granted permissions to +put and get objects from the bucket. When using the built-in AWS service destinations +found in the `@aws-cdk/aws-kinesisfirehose-destinations` module, the CDK grants the +permissions automatically. However, custom or third-party destinations may require custom +permissions. In this case, use the delivery stream as an `IGrantable`, as follows: + +```ts fixture=with-delivery-stream +import * as lambda from '@aws-cdk/aws-lambda'; + +const fn = new lambda.Function(this, 'Function', { + code: lambda.Code.fromInline('exports.handler = (event) => {}'), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', +}); + +fn.grantInvoke(deliveryStream); +``` + +## Multiple destinations + +Though the delivery stream allows specifying an array of destinations, only one +destination per delivery stream is currently allowed. This limitation is enforced at CDK +synthesis time and will throw an error. diff --git a/packages/@aws-cdk/aws-kinesisfirehose/lib/delivery-stream.ts b/packages/@aws-cdk/aws-kinesisfirehose/lib/delivery-stream.ts new file mode 100644 index 0000000000000..4968930808be6 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/lib/delivery-stream.ts @@ -0,0 +1,266 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { RegionInfo } from '@aws-cdk/region-info'; +import { Construct, Node } from 'constructs'; +import { IDestination } from './destination'; +import { CfnDeliveryStream } from './kinesisfirehose.generated'; + +const PUT_RECORD_ACTIONS = [ + 'firehose:PutRecord', + 'firehose:PutRecordBatch', +]; + +/** + * Represents a Kinesis Data Firehose delivery stream. + */ +export interface IDeliveryStream extends cdk.IResource, iam.IGrantable, ec2.IConnectable { + /** + * The ARN of the delivery stream. + * + * @attribute + */ + readonly deliveryStreamArn: string; + + /** + * The name of the delivery stream. + * + * @attribute + */ + readonly deliveryStreamName: string; + + /** + * Grant the `grantee` identity permissions to perform `actions`. + */ + grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; + + /** + * Grant the `grantee` identity permissions to perform `firehose:PutRecord` and `firehose:PutRecordBatch` actions on this delivery stream. + */ + grantPutRecords(grantee: iam.IGrantable): iam.Grant; + + /** + * Return the given named metric for this delivery stream. + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; +} + +/** + * Base class for new and imported Kinesis Data Firehose delivery streams. + */ +abstract class DeliveryStreamBase extends cdk.Resource implements IDeliveryStream { + + public abstract readonly deliveryStreamName: string; + + public abstract readonly deliveryStreamArn: string; + + public abstract readonly grantPrincipal: iam.IPrincipal; + + /** + * Network connections between Kinesis Data Firehose and other resources, i.e. Redshift cluster. + */ + public readonly connections: ec2.Connections; + + constructor(scope: Construct, id: string, props: cdk.ResourceProps = {}) { + super(scope, id, props); + + this.connections = setConnections(this); + } + + public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { + return iam.Grant.addToPrincipal({ + resourceArns: [this.deliveryStreamArn], + grantee: grantee, + actions: actions, + }); + } + + public grantPutRecords(grantee: iam.IGrantable): iam.Grant { + return this.grant(grantee, ...PUT_RECORD_ACTIONS); + } + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/Firehose', + metricName: metricName, + dimensions: { + DeliveryStreamName: this.deliveryStreamName, + }, + ...props, + }).attachTo(this); + } +} + +/** + * Properties for a new delivery stream. + */ +export interface DeliveryStreamProps { + /** + * The destinations that this delivery stream will deliver data to. + * + * Only a singleton array is supported at this time. + */ + readonly destinations: IDestination[]; + + /** + * A name for the delivery stream. + * + * @default - a name is generated by CloudFormation. + */ + readonly deliveryStreamName?: string; + + /** + * The IAM role associated with this delivery stream. + * + * Assumed by Kinesis Data Firehose to read from sources and encrypt data server-side. + * + * @default - a role will be created with default permissions. + */ + readonly role?: iam.IRole; +} + +/** + * A full specification of a delivery stream that can be used to import it fluently into the CDK application. + */ +export interface DeliveryStreamAttributes { + /** + * The ARN of the delivery stream. + * + * At least one of deliveryStreamArn and deliveryStreamName must be provided. + * + * @default - derived from `deliveryStreamName`. + */ + readonly deliveryStreamArn?: string; + + /** + * The name of the delivery stream + * + * At least one of deliveryStreamName and deliveryStreamArn must be provided. + * + * @default - derived from `deliveryStreamArn`. + */ + readonly deliveryStreamName?: string; + + /** + * The IAM role associated with this delivery stream. + * + * Assumed by Kinesis Data Firehose to read from sources and encrypt data server-side. + * + * @default - the imported stream cannot be granted access to other resources as an `iam.IGrantable`. + */ + readonly role?: iam.IRole; +} + +/** + * Create a Kinesis Data Firehose delivery stream + * + * @resource AWS::KinesisFirehose::DeliveryStream + */ +export class DeliveryStream extends DeliveryStreamBase { + /** + * Import an existing delivery stream from its name. + */ + static fromDeliveryStreamName(scope: Construct, id: string, deliveryStreamName: string): IDeliveryStream { + return this.fromDeliveryStreamAttributes(scope, id, { deliveryStreamName }); + } + + /** + * Import an existing delivery stream from its ARN. + */ + static fromDeliveryStreamArn(scope: Construct, id: string, deliveryStreamArn: string): IDeliveryStream { + return this.fromDeliveryStreamAttributes(scope, id, { deliveryStreamArn }); + } + + /** + * Import an existing delivery stream from its attributes. + */ + static fromDeliveryStreamAttributes(scope: Construct, id: string, attrs: DeliveryStreamAttributes): IDeliveryStream { + if (!attrs.deliveryStreamName && !attrs.deliveryStreamArn) { + throw new Error('Either deliveryStreamName or deliveryStreamArn must be provided in DeliveryStreamAttributes'); + } + const deliveryStreamName = attrs.deliveryStreamName ?? + cdk.Stack.of(scope).splitArn(attrs.deliveryStreamArn!, cdk.ArnFormat.SLASH_RESOURCE_NAME).resourceName; + + if (!deliveryStreamName) { + throw new Error(`No delivery stream name found in ARN: '${attrs.deliveryStreamArn}'`); + } + const deliveryStreamArn = attrs.deliveryStreamArn ?? cdk.Stack.of(scope).formatArn({ + service: 'firehose', + resource: 'deliverystream', + resourceName: attrs.deliveryStreamName, + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + }); + class Import extends DeliveryStreamBase { + public readonly deliveryStreamName = deliveryStreamName!; + public readonly deliveryStreamArn = deliveryStreamArn; + public readonly grantPrincipal = attrs.role ?? new iam.UnknownPrincipal({ resource: this }); + } + return new Import(scope, id); + } + + readonly deliveryStreamName: string; + + readonly deliveryStreamArn: string; + + readonly grantPrincipal: iam.IPrincipal; + + constructor(scope: Construct, id: string, props: DeliveryStreamProps) { + super(scope, id, { + physicalName: props.deliveryStreamName, + }); + + if (props.destinations.length !== 1) { + throw new Error(`Only one destination is allowed per delivery stream, given ${props.destinations.length}`); + } + + const role = props.role ?? new iam.Role(this, 'Service Role', { + assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), + }); + this.grantPrincipal = role; + + const destinationConfig = props.destinations[0].bind(this, {}); + + const resource = new CfnDeliveryStream(this, 'Resource', { + deliveryStreamName: props.deliveryStreamName, + deliveryStreamType: 'DirectPut', + ...destinationConfig, + }); + destinationConfig.dependables?.forEach(dependable => resource.node.addDependency(dependable)); + + this.deliveryStreamArn = this.getResourceArnAttribute(resource.attrArn, { + service: 'kinesis', + resource: 'deliverystream', + resourceName: this.physicalName, + }); + this.deliveryStreamName = this.getResourceNameAttribute(resource.ref); + } +} + +function setConnections(scope: Construct) { + const stack = cdk.Stack.of(scope); + + const mappingId = '@aws-cdk/aws-kinesisfirehose.CidrBlocks'; + let cfnMapping = Node.of(stack).tryFindChild(mappingId) as cdk.CfnMapping; + + if (!cfnMapping) { + const mapping: {[region: string]: { FirehoseCidrBlock: string }} = {}; + RegionInfo.regions.forEach((regionInfo) => { + if (regionInfo.firehoseCidrBlock) { + mapping[regionInfo.name] = { + FirehoseCidrBlock: regionInfo.firehoseCidrBlock, + }; + } + }); + cfnMapping = new cdk.CfnMapping(stack, mappingId, { + mapping, + lazy: true, + }); + } + + const cidrBlock = cfnMapping.findInMap(stack.region, 'FirehoseCidrBlock'); + + return new ec2.Connections({ + peer: ec2.Peer.ipv4(cidrBlock), + }); +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose/lib/destination.ts b/packages/@aws-cdk/aws-kinesisfirehose/lib/destination.ts new file mode 100644 index 0000000000000..eb277babcdebb --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/lib/destination.ts @@ -0,0 +1,40 @@ +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDeliveryStream } from './kinesisfirehose.generated'; + +/** + * A Kinesis Data Firehose delivery stream destination configuration. + */ +export interface DestinationConfig { + /** + * S3 destination configuration properties. + * + * @default - S3 destination is not used. + */ + readonly extendedS3DestinationConfiguration?: CfnDeliveryStream.ExtendedS3DestinationConfigurationProperty; + + /** + * Any resources that were created by the destination when binding it to the stack that must be deployed before the delivery stream is deployed. + * + * @default [] + */ + readonly dependables?: cdk.IDependable[]; +} + +/** + * Options when binding a destination to a delivery stream. + */ +export interface DestinationBindOptions { +} + +/** + * A Kinesis Data Firehose delivery stream destination. + */ +export interface IDestination { + /** + * Binds this destination to the Kinesis Data Firehose delivery stream. + * + * Implementers should use this method to bind resources to the stack and initialize values using the provided stream. + */ + bind(scope: Construct, options: DestinationBindOptions): DestinationConfig; +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose/lib/index.ts b/packages/@aws-cdk/aws-kinesisfirehose/lib/index.ts index dd7beef14d159..3eddb6dec468e 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/lib/index.ts +++ b/packages/@aws-cdk/aws-kinesisfirehose/lib/index.ts @@ -1,2 +1,5 @@ +export * from './delivery-stream'; +export * from './destination'; + // AWS::KinesisFirehose CloudFormation Resources: export * from './kinesisfirehose.generated'; diff --git a/packages/@aws-cdk/aws-kinesisfirehose/package.json b/packages/@aws-cdk/aws-kinesisfirehose/package.json index fc949c6290e88..d6651f8d08c62 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/package.json @@ -73,26 +73,36 @@ }, "license": "Apache-2.0", "devDependencies": { + "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "pkglint": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/region-info": "0.0.0", "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/region-info": "0.0.0", "constructs": "^3.3.69" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-kinesisfirehose/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-kinesisfirehose/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..8a68efc25aa8e --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import { Construct, Stack } from '@aws-cdk/core'; +import { DeliveryStream, DestinationBindOptions, DestinationConfig, IDestination } from '@aws-cdk/aws-kinesisfirehose'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-bucket.ts-fixture b/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-bucket.ts-fixture new file mode 100644 index 0000000000000..d0851cff49639 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-bucket.ts-fixture @@ -0,0 +1,13 @@ +// Fixture with a bucket already created +import { Construct, Stack } from '@aws-cdk/core'; +import { DeliveryStream, DestinationBindOptions, DestinationConfig, IDestination } from '@aws-cdk/aws-kinesisfirehose'; +import * as s3 from '@aws-cdk/aws-s3'; +declare const bucket: s3.Bucket; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-delivery-stream.ts-fixture b/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-delivery-stream.ts-fixture new file mode 100644 index 0000000000000..c7b75b20d2c1b --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-delivery-stream.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with a delivery stream already created +import { Construct, Stack } from '@aws-cdk/core'; +import { DeliveryStream, DestinationBindOptions, DestinationConfig, IDestination } from '@aws-cdk/aws-kinesisfirehose'; +declare const deliveryStream: DeliveryStream; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-destination.ts-fixture b/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-destination.ts-fixture new file mode 100644 index 0000000000000..37d78bf7a43d3 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/rosetta/with-destination.ts-fixture @@ -0,0 +1,12 @@ +// Fixture with a destination already created +import { Construct, Stack } from '@aws-cdk/core'; +import { DeliveryStream, DestinationBindOptions, DestinationConfig, IDestination } from '@aws-cdk/aws-kinesisfirehose'; +declare const destination: IDestination; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts b/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts new file mode 100644 index 0000000000000..bb9c3ba744a2f --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/test/delivery-stream.test.ts @@ -0,0 +1,314 @@ +import '@aws-cdk/assert-internal/jest'; +import { ABSENT, ResourcePart, SynthUtils, anything } from '@aws-cdk/assert-internal'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Construct, Node } from 'constructs'; +import * as firehose from '../lib'; + +describe('delivery stream', () => { + let stack: cdk.Stack; + let dependable: Construct; + let mockS3Destination: firehose.IDestination; + + const bucketArn = 'arn:aws:s3:::my-bucket'; + const roleArn = 'arn:aws:iam::111122223333:role/my-role'; + + beforeEach(() => { + stack = new cdk.Stack(); + mockS3Destination = { + bind(scope: Construct, _options: firehose.DestinationBindOptions): firehose.DestinationConfig { + dependable = new class extends cdk.Construct { + constructor(depScope: Construct, id: string) { + super(depScope, id); + new cdk.CfnResource(this, 'Resource', { type: 'CDK::Dummy' }); + } + }(scope, 'Dummy Dep'); + return { + extendedS3DestinationConfiguration: { + bucketArn: bucketArn, + roleArn: roleArn, + }, + dependables: [dependable], + }; + }, + }; + }); + + test('creates stream with default values', () => { + new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { + DeliveryStreamEncryptionConfigurationInput: ABSENT, + DeliveryStreamName: ABSENT, + DeliveryStreamType: 'DirectPut', + KinesisStreamSourceConfiguration: ABSENT, + ExtendedS3DestinationConfiguration: { + BucketARN: bucketArn, + RoleARN: roleArn, + }, + }); + }); + + test('provided role is set as grant principal', () => { + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), + }); + + const deliveryStream = new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + role: role, + }); + + expect(deliveryStream.grantPrincipal).toBe(role); + }); + + test('not providing role creates one', () => { + new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Principal: { + Service: 'firehose.amazonaws.com', + }, + }, + ], + }, + }); + }); + + test('grant provides access to stream', () => { + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + const deliveryStream = new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + deliveryStream.grant(role, 'firehose:PutRecord'); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'firehose:PutRecord', + Resource: stack.resolve(deliveryStream.deliveryStreamArn), + }, + ], + }, + Roles: [stack.resolve(role.roleName)], + }); + }); + + test('grantPutRecords provides PutRecord* access to stream', () => { + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + const deliveryStream = new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + deliveryStream.grantPutRecords(role); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'firehose:PutRecord', + 'firehose:PutRecordBatch', + ], + Resource: stack.resolve(deliveryStream.deliveryStreamArn), + }, + ], + }, + Roles: [stack.resolve(role.roleName)], + }); + }); + + test('dependables supplied from destination are depended on by just the CFN resource', () => { + const dependableId = stack.resolve((Node.of(dependable).defaultChild as cdk.CfnResource).logicalId); + + new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + expect(stack).toHaveResourceLike('AWS::KinesisFirehose::DeliveryStream', { + DependsOn: [dependableId], + }, ResourcePart.CompleteDefinition); + expect(stack).toHaveResourceLike('AWS::IAM::Role', { + DependsOn: ABSENT, + }, ResourcePart.CompleteDefinition); + }); + + test('supplying 0 or multiple destinations throws', () => { + expect(() => new firehose.DeliveryStream(stack, 'No Destinations', { + destinations: [], + })).toThrowError(/Only one destination is allowed per delivery stream/); + expect(() => new firehose.DeliveryStream(stack, 'Too Many Destinations', { + destinations: [mockS3Destination, mockS3Destination], + })).toThrowError(/Only one destination is allowed per delivery stream/); + }); + + describe('metric methods provide a Metric with configured and attached properties', () => { + beforeEach(() => { + stack = new cdk.Stack(undefined, undefined, { env: { account: '000000000000', region: 'us-west-1' } }); + }); + + test('metric', () => { + const deliveryStream = new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + const metric = deliveryStream.metric('IncomingRecords'); + + expect(metric).toMatchObject({ + account: stack.account, + region: stack.region, + namespace: 'AWS/Firehose', + metricName: 'IncomingRecords', + dimensions: { + DeliveryStreamName: deliveryStream.deliveryStreamName, + }, + }); + }); + }); + + test('allows connections for Firehose IP addresses using map when region not specified', () => { + const vpc = new ec2.Vpc(stack, 'VPC'); + const securityGroup = new ec2.SecurityGroup(stack, 'Security Group', { vpc }); + const deliveryStream = new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + securityGroup.connections.allowFrom(deliveryStream, ec2.Port.allTcp()); + + expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [ + { + CidrIp: { + 'Fn::FindInMap': [ + anything(), + { + Ref: 'AWS::Region', + }, + 'FirehoseCidrBlock', + ], + }, + }, + ], + }); + }); + + test('allows connections for Firehose IP addresses using literal when region specified', () => { + stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-west-1' } }); + const vpc = new ec2.Vpc(stack, 'VPC'); + const securityGroup = new ec2.SecurityGroup(stack, 'Security Group', { vpc }); + const deliveryStream = new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + securityGroup.connections.allowFrom(deliveryStream, ec2.Port.allTcp()); + + expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [ + { + CidrIp: '13.57.135.192/27', + }, + ], + }); + }); + + test('only adds one Firehose IP address mapping to stack even if multiple delivery streams defined', () => { + new firehose.DeliveryStream(stack, 'Delivery Stream 1', { + destinations: [mockS3Destination], + }); + new firehose.DeliveryStream(stack, 'Delivery Stream 2', { + destinations: [mockS3Destination], + }); + + expect(Object.keys(SynthUtils.toCloudFormation(stack).Mappings).length).toBe(1); + }); + + test('can add tags', () => { + const deliveryStream = new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], + }); + + cdk.Tags.of(deliveryStream).add('tagKey', 'tagValue'); + + expect(stack).toHaveResource('AWS::KinesisFirehose::DeliveryStream', { + Tags: [ + { + Key: 'tagKey', + Value: 'tagValue', + }, + ], + }); + }); + + describe('importing', () => { + test('from name', () => { + const deliveryStream = firehose.DeliveryStream.fromDeliveryStreamName(stack, 'DeliveryStream', 'mydeliverystream'); + + expect(deliveryStream.deliveryStreamName).toBe('mydeliverystream'); + expect(stack.resolve(deliveryStream.deliveryStreamArn)).toStrictEqual({ + 'Fn::Join': ['', ['arn:', stack.resolve(stack.partition), ':firehose:', stack.resolve(stack.region), ':', stack.resolve(stack.account), ':deliverystream/mydeliverystream']], + }); + expect(deliveryStream.grantPrincipal).toBeInstanceOf(iam.UnknownPrincipal); + }); + + test('from ARN', () => { + const deliveryStream = firehose.DeliveryStream.fromDeliveryStreamArn(stack, 'DeliveryStream', 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/mydeliverystream'); + + expect(deliveryStream.deliveryStreamName).toBe('mydeliverystream'); + expect(deliveryStream.deliveryStreamArn).toBe('arn:aws:firehose:xx-west-1:111122223333:deliverystream/mydeliverystream'); + expect(deliveryStream.grantPrincipal).toBeInstanceOf(iam.UnknownPrincipal); + }); + + test('from attributes (just name)', () => { + const deliveryStream = firehose.DeliveryStream.fromDeliveryStreamAttributes(stack, 'DeliveryStream', { deliveryStreamName: 'mydeliverystream' }); + + expect(deliveryStream.deliveryStreamName).toBe('mydeliverystream'); + expect(stack.resolve(deliveryStream.deliveryStreamArn)).toStrictEqual({ + 'Fn::Join': ['', ['arn:', stack.resolve(stack.partition), ':firehose:', stack.resolve(stack.region), ':', stack.resolve(stack.account), ':deliverystream/mydeliverystream']], + }); + expect(deliveryStream.grantPrincipal).toBeInstanceOf(iam.UnknownPrincipal); + }); + + test('from attributes (just ARN)', () => { + const deliveryStream = firehose.DeliveryStream.fromDeliveryStreamAttributes(stack, 'DeliveryStream', { deliveryStreamArn: 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/mydeliverystream' }); + + expect(deliveryStream.deliveryStreamName).toBe('mydeliverystream'); + expect(deliveryStream.deliveryStreamArn).toBe('arn:aws:firehose:xx-west-1:111122223333:deliverystream/mydeliverystream'); + expect(deliveryStream.grantPrincipal).toBeInstanceOf(iam.UnknownPrincipal); + }); + + test('from attributes (with role)', () => { + const role = iam.Role.fromRoleArn(stack, 'Delivery Stream Role', 'arn:aws:iam::111122223333:role/DeliveryStreamRole'); + const deliveryStream = firehose.DeliveryStream.fromDeliveryStreamAttributes(stack, 'DeliveryStream', { deliveryStreamName: 'mydeliverystream', role }); + + expect(deliveryStream.deliveryStreamName).toBe('mydeliverystream'); + expect(stack.resolve(deliveryStream.deliveryStreamArn)).toStrictEqual({ + 'Fn::Join': ['', ['arn:', stack.resolve(stack.partition), ':firehose:', stack.resolve(stack.region), ':', stack.resolve(stack.account), ':deliverystream/mydeliverystream']], + }); + expect(deliveryStream.grantPrincipal).toBe(role); + }); + + test('throws when malformatted ARN', () => { + expect(() => firehose.DeliveryStream.fromDeliveryStreamAttributes(stack, 'DeliveryStream', { deliveryStreamArn: 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/' })) + .toThrowError("No delivery stream name found in ARN: 'arn:aws:firehose:xx-west-1:111122223333:deliverystream/'"); + }); + + test('throws when without name or ARN', () => { + expect(() => firehose.DeliveryStream.fromDeliveryStreamAttributes(stack, 'DeliveryStream', {})) + .toThrowError('Either deliveryStreamName or deliveryStreamArn must be provided in DeliveryStreamAttributes'); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.expected.json b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.expected.json new file mode 100644 index 0000000000000..f9e785a3def9e --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.expected.json @@ -0,0 +1,194 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RoleDefaultPolicy5FFB7DAB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RoleDefaultPolicy5FFB7DAB", + "Roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + }, + "DeliveryStreamServiceRole964EEBCC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "DeliveryStreamF6D5572D": { + "Type": "AWS::KinesisFirehose::DeliveryStream", + "Properties": { + "DeliveryStreamType": "DirectPut", + "ExtendedS3DestinationConfiguration": { + "BucketARN": { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "RoleARN": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } + } + }, + "DependsOn": [ + "RoleDefaultPolicy5FFB7DAB" + ] + } + }, + "Mappings": { + "awscdkawskinesisfirehoseCidrBlocks": { + "af-south-1": { + "FirehoseCidrBlock": "13.244.121.224/27" + }, + "ap-east-1": { + "FirehoseCidrBlock": "18.162.221.32/27" + }, + "ap-northeast-1": { + "FirehoseCidrBlock": "13.113.196.224/27" + }, + "ap-northeast-2": { + "FirehoseCidrBlock": "13.209.1.64/27" + }, + "ap-northeast-3": { + "FirehoseCidrBlock": "13.208.177.192/27" + }, + "ap-south-1": { + "FirehoseCidrBlock": "13.232.67.32/27" + }, + "ap-southeast-1": { + "FirehoseCidrBlock": "13.228.64.192/27" + }, + "ap-southeast-2": { + "FirehoseCidrBlock": "13.210.67.224/27" + }, + "ca-central-1": { + "FirehoseCidrBlock": "35.183.92.128/27" + }, + "cn-north-1": { + "FirehoseCidrBlock": "52.81.151.32/27" + }, + "cn-northwest-1": { + "FirehoseCidrBlock": "161.189.23.64/27" + }, + "eu-central-1": { + "FirehoseCidrBlock": "35.158.127.160/27" + }, + "eu-north-1": { + "FirehoseCidrBlock": "13.53.63.224/27" + }, + "eu-south-1": { + "FirehoseCidrBlock": "15.161.135.128/27" + }, + "eu-west-1": { + "FirehoseCidrBlock": "52.19.239.192/27" + }, + "eu-west-2": { + "FirehoseCidrBlock": "18.130.1.96/27" + }, + "eu-west-3": { + "FirehoseCidrBlock": "35.180.1.96/27" + }, + "me-south-1": { + "FirehoseCidrBlock": "15.185.91.0/27" + }, + "sa-east-1": { + "FirehoseCidrBlock": "18.228.1.128/27" + }, + "us-east-1": { + "FirehoseCidrBlock": "52.70.63.192/27" + }, + "us-east-2": { + "FirehoseCidrBlock": "13.58.135.96/27" + }, + "us-gov-east-1": { + "FirehoseCidrBlock": "18.253.138.96/27" + }, + "us-gov-west-1": { + "FirehoseCidrBlock": "52.61.204.160/27" + }, + "us-west-1": { + "FirehoseCidrBlock": "13.57.135.192/27" + }, + "us-west-2": { + "FirehoseCidrBlock": "52.89.255.224/27" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.ts b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.ts new file mode 100644 index 0000000000000..1adfd9bfc0221 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/test/integ.delivery-stream.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env node +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; +import * as firehose from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-firehose-delivery-stream'); + +const bucket = new s3.Bucket(stack, 'Bucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), +}); + +const mockS3Destination: firehose.IDestination = { + bind(_scope: constructs.Construct, _options: firehose.DestinationBindOptions): firehose.DestinationConfig { + const bucketGrant = bucket.grantReadWrite(role); + return { + extendedS3DestinationConfiguration: { + bucketArn: bucket.bucketArn, + roleArn: role.roleArn, + }, + dependables: [bucketGrant], + }; + }, +}; + +new firehose.DeliveryStream(stack, 'Delivery Stream', { + destinations: [mockS3Destination], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-kinesisfirehose/test/kinesisfirehose.test.ts b/packages/@aws-cdk/aws-kinesisfirehose/test/kinesisfirehose.test.ts deleted file mode 100644 index c4505ad966984..0000000000000 --- a/packages/@aws-cdk/aws-kinesisfirehose/test/kinesisfirehose.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert-internal/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts index c2ce689f3aaf3..28ec007b00c84 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -158,3 +158,32 @@ export const APPMESH_ECR_ACCOUNTS: { [region: string]: string } = { 'us-west-1': '840364872350', 'us-west-2': '840364872350', }; + +// https://docs.aws.amazon.com/firehose/latest/dev/controlling-access.html#using-iam-rs-vpc +export const FIREHOSE_CIDR_BLOCKS: { [region: string]: string } = { + 'af-south-1': '13.244.121.224', + 'ap-east-1': '18.162.221.32', + 'ap-northeast-1': '13.113.196.224', + 'ap-northeast-2': '13.209.1.64', + 'ap-northeast-3': '13.208.177.192', + 'ap-south-1': '13.232.67.32', + 'ap-southeast-1': '13.228.64.192', + 'ap-southeast-2': '13.210.67.224', + 'ca-central-1': '35.183.92.128', + 'cn-north-1': '52.81.151.32', + 'cn-northwest-1': '161.189.23.64', + 'eu-central-1': '35.158.127.160', + 'eu-north-1': '13.53.63.224', + 'eu-south-1': '15.161.135.128', + 'eu-west-1': '52.19.239.192', + 'eu-west-2': '18.130.1.96', + 'eu-west-3': '35.180.1.96', + 'me-south-1': '15.185.91.0', + 'sa-east-1': '18.228.1.128', + 'us-east-1': '52.70.63.192', + 'us-east-2': '13.58.135.96', + 'us-gov-east-1': '18.253.138.96', + 'us-gov-west-1': '52.61.204.160', + 'us-west-1': '13.57.135.192', + 'us-west-2': '52.89.255.224', +}; diff --git a/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts b/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts index d23704b6d0062..63455b72ef665 100644 --- a/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts +++ b/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts @@ -3,14 +3,15 @@ import * as fs from 'fs-extra'; import { Default } from '../lib/default'; import { AWS_REGIONS, AWS_SERVICES } from './aws-entities'; import { - APPMESH_ECR_ACCOUNTS, AWS_CDK_METADATA, AWS_OLDER_REGIONS, DLC_REPOSITORY_ACCOUNTS, ELBV2_ACCOUNTS, PARTITION_MAP, - ROUTE_53_BUCKET_WEBSITE_ZONE_IDS, + APPMESH_ECR_ACCOUNTS, AWS_CDK_METADATA, AWS_OLDER_REGIONS, DLC_REPOSITORY_ACCOUNTS, ELBV2_ACCOUNTS, FIREHOSE_CIDR_BLOCKS, + PARTITION_MAP, ROUTE_53_BUCKET_WEBSITE_ZONE_IDS, } from './fact-tables'; async function main(): Promise { checkRegions(APPMESH_ECR_ACCOUNTS); checkRegions(DLC_REPOSITORY_ACCOUNTS); checkRegions(ELBV2_ACCOUNTS); + checkRegions(FIREHOSE_CIDR_BLOCKS); checkRegions(ROUTE_53_BUCKET_WEBSITE_ZONE_IDS); const lines = [ @@ -61,6 +62,11 @@ async function main(): Promise { registerFact(region, 'APPMESH_ECR_ACCOUNT', APPMESH_ECR_ACCOUNTS[region]); + const firehoseCidrBlock = FIREHOSE_CIDR_BLOCKS[region]; + if (firehoseCidrBlock) { + registerFact(region, 'FIREHOSE_CIDR_BLOCK', `${FIREHOSE_CIDR_BLOCKS[region]}/27`); + } + const vpcEndpointServiceNamePrefix = `${domainSuffix.split('.').reverse().join('.')}.vpce`; registerFact(region, 'VPC_ENDPOINT_SERVICE_NAME_PREFIX', vpcEndpointServiceNamePrefix); diff --git a/packages/@aws-cdk/region-info/lib/fact.ts b/packages/@aws-cdk/region-info/lib/fact.ts index 3b5e57835cc7e..6ccef0e8b794f 100644 --- a/packages/@aws-cdk/region-info/lib/fact.ts +++ b/packages/@aws-cdk/region-info/lib/fact.ts @@ -152,6 +152,11 @@ export class FactName { */ public static readonly APPMESH_ECR_ACCOUNT = 'appMeshRepositoryAccount'; + /** + * The CIDR block used by Kinesis Data Firehose servers. + */ + public static readonly FIREHOSE_CIDR_BLOCK = 'firehoseCidrBlock'; + /** * The name of the regional service principal for a given service. * diff --git a/packages/@aws-cdk/region-info/lib/region-info.ts b/packages/@aws-cdk/region-info/lib/region-info.ts index 042b3cec9c177..9e28120a8da62 100644 --- a/packages/@aws-cdk/region-info/lib/region-info.ts +++ b/packages/@aws-cdk/region-info/lib/region-info.ts @@ -117,4 +117,11 @@ export class RegionInfo { public get appMeshRepositoryAccount(): string | undefined { return Fact.find(this.name, FactName.APPMESH_ECR_ACCOUNT); } + + /** + * The CIDR block used by Kinesis Data Firehose servers. + */ + public get firehoseCidrBlock(): string | undefined { + return Fact.find(this.name, FactName.FIREHOSE_CIDR_BLOCK); + } } diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index a200e97145db6..411d6eed9a312 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -239,6 +239,7 @@ "@aws-cdk/aws-kinesisanalytics": "0.0.0", "@aws-cdk/aws-kinesisanalytics-flink": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-kinesisfirehose-destinations": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 0c38c44ff0625..73c670cec466d 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -146,6 +146,7 @@ "@aws-cdk/aws-kinesisanalytics": "0.0.0", "@aws-cdk/aws-kinesisanalytics-flink": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-kinesisfirehose-destinations": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index 33bbc89af3414..43227b31a4041 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -240,6 +240,7 @@ "@aws-cdk/aws-kinesisanalytics": "0.0.0", "@aws-cdk/aws-kinesisanalytics-flink": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-kinesisfirehose-destinations": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index a696b43bceabe..add3c58fee4b2 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -1634,6 +1634,7 @@ export class NoExperimentalDependents extends ValidationRule { ['@aws-cdk/aws-apigatewayv2-integrations', ['@aws-cdk/aws-apigatewayv2']], ['@aws-cdk/aws-apigatewayv2-authorizers', ['@aws-cdk/aws-apigatewayv2']], ['@aws-cdk/aws-events-targets', ['@aws-cdk/aws-kinesisfirehose']], + ['@aws-cdk/aws-kinesisfirehose-destinations', ['@aws-cdk/aws-kinesisfirehose']], ]); private readonly excludedModules = ['@aws-cdk/cloudformation-include']; From cb6e7c9c0c046a5c03bd1a4f1474c9ece1963604 Mon Sep 17 00:00:00 2001 From: Douglas Naphas Date: Thu, 22 Jul 2021 21:39:08 -0400 Subject: [PATCH 100/105] docs: fix link to example integration test in Contributing.md (#15729) Closes gh-15728. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97bc876d86a0c..423fb1e897ff3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -251,7 +251,7 @@ The steps here are usually AWS CLI commands but they need not be. Examples: * [integ.destinations.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-lambda-destinations/test/integ.destinations.ts#L7) -* [integ.token-authorizer.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.ts#L6) +* [integ.token-authorizer.lit.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.ts#L7-L12) #### yarn watch (Optional) From e133bca61b95b71d51b509b646ff1720099ee31e Mon Sep 17 00:00:00 2001 From: marciocarmona Date: Thu, 22 Jul 2021 19:18:44 -0700 Subject: [PATCH 101/105] fix(stepfunctions): non-object arguments to recurseObject are incorrectly treated as objects (#14631) This doesn't actually fix the issue #12935 as currently the Json paths won't be resolved for Lambda steps where the `Resource` is the Lambda ARN and not `arn:aws:states:::lambda:invoke`, but it at least fixes the issue for Text inputs when `payloadResponseOnly: true` and will avoid the same error from happening again if the `recurseObject` is called with a value that's not an object. Ideally the `TaskInput.value` field should be changed to `{ [key: string]: any } | string` here to ensure the type check when sending the value to methods like `FieldUtils.renderObject`: https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/aws-stepfunctions/lib/input.ts#L65 Or even better the `TaskInput` should be made generic like: ``` export class TaskInput { ... private constructor(public readonly type: T, public readonly value: ValueType[T]) {} } type ValueType = { [InputType.OBJECT]: { [key: string]: any }, [InputType.TEXT]: string } ``` However, any of the changes above wouldn't be backwards compatible and could break not only internal references in the `aws-cdk` but also on any customer packages using the CDK. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-stepfunctions/lib/json-path.ts | 7 +++- .../aws-stepfunctions/test/fields.test.ts | 35 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts b/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts index 7a4a56c536a6b..b4602b5e887d0 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts @@ -82,7 +82,12 @@ interface FieldHandlers { } export function recurseObject(obj: object | undefined, handlers: FieldHandlers, visited: object[] = []): object | undefined { - if (obj === undefined) { return undefined; } + // If the argument received is not actually an object (string, number, boolean, undefined, ...) or null + // just return it as is as there's nothing to be rendered. This should only happen in the original call to + // recurseObject as any recursive calls to it are checking for typeof value === 'object' && value !== null + if (typeof obj !== 'object' || obj === null) { + return obj; + } // Avoiding infinite recursion if (visited.includes(obj)) { return {}; } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts index 15591ddeebd76..382ec424177f9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts @@ -1,5 +1,5 @@ import '@aws-cdk/assert-internal/jest'; -import { FieldUtils, JsonPath } from '../lib'; +import { FieldUtils, JsonPath, TaskInput } from '../lib'; describe('Fields', () => { const jsonPathValidationErrorMsg = /exactly '\$', '\$\$', start with '\$.', start with '\$\$.' or start with '\$\['/; @@ -153,6 +153,39 @@ describe('Fields', () => { .toStrictEqual(['$.listField', '$.numField', '$.stringField']); }); + test('rendering a non-object value should just return itself', () => { + expect( + FieldUtils.renderObject(TaskInput.fromText('Hello World').value), + ).toEqual( + 'Hello World', + ); + expect( + FieldUtils.renderObject('Hello World' as any), + ).toEqual( + 'Hello World', + ); + expect( + FieldUtils.renderObject(null as any), + ).toEqual( + null, + ); + expect( + FieldUtils.renderObject(3.14 as any), + ).toEqual( + 3.14, + ); + expect( + FieldUtils.renderObject(true as any), + ).toEqual( + true, + ); + expect( + FieldUtils.renderObject(undefined), + ).toEqual( + undefined, + ); + }); + test('repeated object references at different tree paths should not be considered as recursions', () => { const repeatedObject = { field: JsonPath.stringAt('$.stringField'), From 7668400ec8d4e6ee042c05976f95e42147993375 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 23 Jul 2021 12:41:21 +0200 Subject: [PATCH 102/105] fix(pipelines): Secrets Manager permissions not added to asset projects (#15718) We used to use an immutable singleton role with `*` permissions for the assets projects, because if there were many different destinations in a pipeline, and each asset build had to publish to each destination, the policy could grow too long and exceed the maximum policy size. However, this also disabled the automatic policy wrangling that CodeBuild would do for us, like automatically adding permissions to bind to a VPC, and adding permissions to read Secrets Manager secrets. This especially becoming relevant since that now in the modern API, it's possible to modify build the environment in a way that normally automatically adds SecretsManager permission, but now won't (and it's not possible to fix either). Replace the immutable singleton role with a mutable singleton role, but in such a way that it won't add permissions statements for which it already has a `*` statement (to cut down on duplication), and have the CB project do the automatic VPC bind permissions again. Fixes #15628. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-codebuild/lib/project.ts | 46 +- .../aws-codebuild/test/test.project.ts | 35 + .../lib/codepipeline/codepipeline.ts | 113 +- .../@aws-cdk/pipelines/lib/legacy/pipeline.ts | 85 +- .../@aws-cdk/pipelines/lib/legacy/stage.ts | 2 +- .../lib/private/asset-singleton-role.ts | 85 + .../pipelines/test/compliance/assets.test.ts | 117 +- .../integ.newpipeline-with-vpc.expected.json | 2401 +++++++++++++++++ .../test/integ.newpipeline-with-vpc.ts | 57 + 9 files changed, 2730 insertions(+), 211 deletions(-) create mode 100644 packages/@aws-cdk/pipelines/lib/private/asset-singleton-role.ts create mode 100644 packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json create mode 100644 packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.ts diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 30f8be9051751..efb4896534c60 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -29,6 +29,8 @@ import { CODEPIPELINE_SOURCE_ARTIFACTS_TYPE, NO_SOURCE_TYPE } from './source-typ // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; +const VPC_POLICY_SYM = Symbol.for('@aws-cdk/aws-codebuild.roleVpcPolicy'); + /** * The type returned from {@link IProject#enableBatchBuilds}. */ @@ -1437,23 +1439,33 @@ export class Project extends ProjectBase { }, })); - const policy = new iam.Policy(this, 'PolicyDocument', { - statements: [ - new iam.PolicyStatement({ - resources: ['*'], - actions: [ - 'ec2:CreateNetworkInterface', - 'ec2:DescribeNetworkInterfaces', - 'ec2:DeleteNetworkInterface', - 'ec2:DescribeSubnets', - 'ec2:DescribeSecurityGroups', - 'ec2:DescribeDhcpOptions', - 'ec2:DescribeVpcs', - ], - }), - ], - }); - this.role.attachInlinePolicy(policy); + // If the same Role is used for multiple Projects, always creating a new `iam.Policy` + // will attach the same policy multiple times, probably exceeding the maximum size of the + // Role policy. Make sure we only do it once for the same role. + // + // This deduplication could be a feature of the Role itself, but that feels risky and + // is hard to implement (what with Tokens and all). Safer to fix it locally for now. + let policy: iam.Policy | undefined = (this.role as any)[VPC_POLICY_SYM]; + if (!policy) { + policy = new iam.Policy(this, 'PolicyDocument', { + statements: [ + new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeVpcs', + ], + }), + ], + }); + this.role.attachInlinePolicy(policy); + (this.role as any)[VPC_POLICY_SYM] = policy; + } // add an explicit dependency between the EC2 Policy and this Project - // otherwise, creating the Project fails, as it requires these permissions diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts index 2dc652e64f39d..038bb2e0e45cf 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts @@ -429,6 +429,41 @@ export = { test.done(); }, + 'if a role is shared between projects in a VPC, the VPC Policy is only attached once'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'), + }); + const source = codebuild.Source.gitHubEnterprise({ + httpsCloneUrl: 'https://mygithub-enterprise.com/myuser/myrepo', + }); + + // WHEN + new codebuild.Project(stack, 'Project1', { source, role, vpc, projectName: 'P1' }); + new codebuild.Project(stack, 'Project2', { source, role, vpc, projectName: 'P2' }); + + // THEN + // - 1 is for `ec2:CreateNetworkInterfacePermission`, deduplicated as they're part of a single policy + // - 1 is for `ec2:CreateNetworkInterface`, this is the separate Policy we're deduplicating + // We would have found 3 if the deduplication didn't work. + expect(stack).to(countResources('AWS::IAM::Policy', 2)); + + // THEN - both Projects have a DependsOn on the same policy + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + Properties: { Name: 'P1' }, + DependsOn: ['Project1PolicyDocumentF9761562'], + }, ResourcePart.CompleteDefinition)); + + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + Properties: { Name: 'P1' }, + DependsOn: ['Project1PolicyDocumentF9761562'], + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + 'can use an imported Role for a Project within a VPC'(test: Test) { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts index e3c10d29b0740..4e509faee2111 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts @@ -4,13 +4,14 @@ import * as cp from '@aws-cdk/aws-codepipeline'; import * as cpa from '@aws-cdk/aws-codepipeline-actions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; -import { Aws, Fn, IDependable, Lazy, PhysicalName, Stack } from '@aws-cdk/core'; +import { Aws, Fn, Lazy, PhysicalName, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct, Node } from 'constructs'; import { AssetType, FileSet, IFileSetProducer, ManualApprovalStep, ShellStep, StackAsset, StackDeployment, Step } from '../blueprint'; import { DockerCredential, dockerCredentialsInstallCommands, DockerCredentialUsage } from '../docker-credentials'; import { GraphNode, GraphNodeCollection, isGraph, AGraphNode, PipelineGraph } from '../helpers-internal'; import { PipelineBase } from '../main'; +import { AssetSingletonRole } from '../private/asset-singleton-role'; import { appOf, assemblyBuilderOf, embeddedAsmPath, obtainScope } from '../private/construct-internals'; import { toPosixPath } from '../private/fs'; import { enumerate, flatten, maybeSuffix, noUndefined } from '../private/javascript'; @@ -254,11 +255,6 @@ export class CodePipeline extends PipelineBase { */ private readonly assetCodeBuildRoles: Record = {}; - /** - * Policies created for the build projects that they have to depend on - */ - private readonly assetAttachedPolicies: Record = {}; - /** * Per asset type, the target role ARNs that need to be assumed */ @@ -635,7 +631,7 @@ export class CodePipeline extends PipelineBase { } } - const assetBuildConfig = this.obtainAssetCodeBuildRole(assets[0].assetType); + const role = this.obtainAssetCodeBuildRole(assets[0].assetType); // The base commands that need to be run const script = new CodeBuildStep(node.id, { @@ -647,13 +643,12 @@ export class CodePipeline extends PipelineBase { buildEnvironment: { privileged: assets.some(asset => asset.assetType === AssetType.DOCKER_IMAGE), }, - role: assetBuildConfig.role, + role, }); // Customizations that are not accessible to regular users return CodeBuildFactory.fromCodeBuildStep(node.id, script, { additionalConstructLevel: false, - additionalDependable: assetBuildConfig.dependable, // If we use a single publisher, pass buildspec via file otherwise it'll // grow too big. @@ -775,18 +770,15 @@ export class CodePipeline extends PipelineBase { * Modeled after the CodePipeline role and 'CodePipelineActionRole' roles. * Generates one role per asset type to separate file and Docker/image-based permissions. */ - private obtainAssetCodeBuildRole(assetType: AssetType): AssetCodeBuildRole { + private obtainAssetCodeBuildRole(assetType: AssetType): iam.IRole { if (this.assetCodeBuildRoles[assetType]) { - return { - role: this.assetCodeBuildRoles[assetType], - dependable: this.assetAttachedPolicies[assetType], - }; + return this.assetCodeBuildRoles[assetType]; } const stack = Stack.of(this); const rolePrefix = assetType === AssetType.DOCKER_IMAGE ? 'Docker' : 'File'; - const assetRole = new iam.Role(this.assetsScope, `${rolePrefix}Role`, { + const assetRole = new AssetSingletonRole(this.assetsScope, `${rolePrefix}Role`, { roleName: PhysicalName.GENERATE_IF_NEEDED, assumedBy: new iam.CompositePrincipal( new iam.ServicePrincipal('codebuild.amazonaws.com'), @@ -794,45 +786,6 @@ export class CodePipeline extends PipelineBase { ), }); - // Logging permissions - const logGroupArn = stack.formatArn({ - service: 'logs', - resource: 'log-group', - sep: ':', - resourceName: '/aws/codebuild/*', - }); - assetRole.addToPolicy(new iam.PolicyStatement({ - resources: [logGroupArn], - actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], - })); - - // CodeBuild report groups - const codeBuildArn = stack.formatArn({ - service: 'codebuild', - resource: 'report-group', - resourceName: '*', - }); - assetRole.addToPolicy(new iam.PolicyStatement({ - actions: [ - 'codebuild:CreateReportGroup', - 'codebuild:CreateReport', - 'codebuild:UpdateReport', - 'codebuild:BatchPutTestCases', - 'codebuild:BatchPutCodeCoverages', - ], - resources: [codeBuildArn], - })); - - // CodeBuild start/stop - assetRole.addToPolicy(new iam.PolicyStatement({ - resources: ['*'], - actions: [ - 'codebuild:BatchGetBuilds', - 'codebuild:StartBuild', - 'codebuild:StopBuild', - ], - })); - // Publishing role access // The ARNs include raw AWS pseudo parameters (e.g., ${AWS::Partition}), which need to be substituted. // Lazy-evaluated so all asset publishing roles are included. @@ -846,51 +799,8 @@ export class CodePipeline extends PipelineBase { this.dockerCredentials.forEach(reg => reg.grantRead(assetRole, DockerCredentialUsage.ASSET_PUBLISHING)); } - // Artifact access - this.pipeline.artifactBucket.grantRead(assetRole); - - // VPC permissions required for CodeBuild - // Normally CodeBuild itself takes care of this but we're creating a singleton role so now - // we need to do this. - const assetCodeBuildOptions = this.codeBuildDefaultsFor(CodeBuildProjectType.ASSETS); - if (assetCodeBuildOptions?.vpc) { - const vpcPolicy = new iam.Policy(assetRole, 'VpcPolicy', { - statements: [ - new iam.PolicyStatement({ - resources: [`arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:network-interface/*`], - actions: ['ec2:CreateNetworkInterfacePermission'], - conditions: { - StringEquals: { - 'ec2:Subnet': assetCodeBuildOptions.vpc - .selectSubnets(assetCodeBuildOptions.subnetSelection).subnetIds - .map(si => `arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:subnet/${si}`), - 'ec2:AuthorizedService': 'codebuild.amazonaws.com', - }, - }, - }), - new iam.PolicyStatement({ - resources: ['*'], - actions: [ - 'ec2:CreateNetworkInterface', - 'ec2:DescribeNetworkInterfaces', - 'ec2:DeleteNetworkInterface', - 'ec2:DescribeSubnets', - 'ec2:DescribeSecurityGroups', - 'ec2:DescribeDhcpOptions', - 'ec2:DescribeVpcs', - ], - }), - ], - }); - assetRole.attachInlinePolicy(vpcPolicy); - this.assetAttachedPolicies[assetType] = vpcPolicy; - } - - this.assetCodeBuildRoles[assetType] = assetRole.withoutPolicyUpdates(); - return { - role: this.assetCodeBuildRoles[assetType], - dependable: this.assetAttachedPolicies[assetType], - }; + this.assetCodeBuildRoles[assetType] = assetRole; + return assetRole; } } @@ -903,11 +813,6 @@ function dockerUsageFromCodeBuild(cbt: CodeBuildProjectType): DockerCredentialUs } } -interface AssetCodeBuildRole { - readonly role: iam.IRole; - readonly dependable?: IDependable; -} - enum CodeBuildProjectType { SYNTH = 'SYNTH', ASSETS = 'ASSETS', diff --git a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts index e100c8c4a90f0..b8d7769ad3fc0 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts @@ -3,11 +3,12 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; -import { Annotations, App, Aws, CfnOutput, Fn, Lazy, PhysicalName, Stack, Stage } from '@aws-cdk/core'; +import { Annotations, App, CfnOutput, Fn, Lazy, PhysicalName, Stack, Stage } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { AssetType } from '../blueprint/asset-type'; import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; import { ApplicationSecurityCheck } from '../private/application-security-check'; +import { AssetSingletonRole } from '../private/asset-singleton-role'; import { appOf, assemblyBuilderOf } from '../private/construct-internals'; import { DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; import { AddStageOptions, AssetPublishingCommand, BaseStageOptions, CdkStage, StackOutput } from './stage'; @@ -580,50 +581,11 @@ class AssetPublishing extends CoreConstruct { if (this.assetRoles[assetType]) { return this.assetRoles[assetType]; } const rolePrefix = assetType === AssetType.DOCKER_IMAGE ? 'Docker' : 'File'; - const assetRole = new iam.Role(this, `${rolePrefix}Role`, { + const assetRole = new AssetSingletonRole(this, `${rolePrefix}Role`, { roleName: PhysicalName.GENERATE_IF_NEEDED, assumedBy: new iam.CompositePrincipal(new iam.ServicePrincipal('codebuild.amazonaws.com'), new iam.AccountPrincipal(Stack.of(this).account)), }); - // Logging permissions - const logGroupArn = Stack.of(this).formatArn({ - service: 'logs', - resource: 'log-group', - sep: ':', - resourceName: '/aws/codebuild/*', - }); - assetRole.addToPolicy(new iam.PolicyStatement({ - resources: [logGroupArn], - actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], - })); - - // CodeBuild report groups - const codeBuildArn = Stack.of(this).formatArn({ - service: 'codebuild', - resource: 'report-group', - resourceName: '*', - }); - assetRole.addToPolicy(new iam.PolicyStatement({ - actions: [ - 'codebuild:CreateReportGroup', - 'codebuild:CreateReport', - 'codebuild:UpdateReport', - 'codebuild:BatchPutTestCases', - 'codebuild:BatchPutCodeCoverages', - ], - resources: [codeBuildArn], - })); - - // CodeBuild start/stop - assetRole.addToPolicy(new iam.PolicyStatement({ - resources: ['*'], - actions: [ - 'codebuild:BatchGetBuilds', - 'codebuild:StartBuild', - 'codebuild:StopBuild', - ], - })); - // Publishing role access // The ARNs include raw AWS pseudo parameters (e.g., ${AWS::Partition}), which need to be substituted. // Lazy-evaluated so all asset publishing roles are included. @@ -637,46 +599,7 @@ class AssetPublishing extends CoreConstruct { this.dockerCredentials.forEach(reg => reg.grantRead(assetRole, DockerCredentialUsage.ASSET_PUBLISHING)); } - // Artifact access - this.pipeline.artifactBucket.grantRead(assetRole); - - // VPC permissions required for CodeBuild - // Normally CodeBuild itself takes care of this but we're creating a singleton role so now - // we need to do this. - if (this.props.vpc) { - const vpcPolicy = new iam.Policy(assetRole, 'VpcPolicy', { - statements: [ - new iam.PolicyStatement({ - resources: [`arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:network-interface/*`], - actions: ['ec2:CreateNetworkInterfacePermission'], - conditions: { - StringEquals: { - 'ec2:Subnet': this.props.vpc - .selectSubnets(this.props.subnetSelection).subnetIds - .map(si => `arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:subnet/${si}`), - 'ec2:AuthorizedService': 'codebuild.amazonaws.com', - }, - }, - }), - new iam.PolicyStatement({ - resources: ['*'], - actions: [ - 'ec2:CreateNetworkInterface', - 'ec2:DescribeNetworkInterfaces', - 'ec2:DeleteNetworkInterface', - 'ec2:DescribeSubnets', - 'ec2:DescribeSecurityGroups', - 'ec2:DescribeDhcpOptions', - 'ec2:DescribeVpcs', - ], - }), - ], - }); - assetRole.attachInlinePolicy(vpcPolicy); - this.assetAttachedPolicies[assetType] = vpcPolicy; - } - - this.assetRoles[assetType] = assetRole.withoutPolicyUpdates(); + this.assetRoles[assetType] = assetRole; return this.assetRoles[assetType]; } } diff --git a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts index 3b4140fdba5ce..bfb997e908196 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts @@ -9,6 +9,7 @@ import { Construct, Node } from 'constructs'; import { AssetType } from '../blueprint/asset-type'; import { ApplicationSecurityCheck } from '../private/application-security-check'; import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; +import { pipelineSynth } from '../private/construct-internals'; import { topologicalSort } from '../private/toposort'; import { DeployCdkStackAction } from './actions'; import { CdkPipeline } from './pipeline'; @@ -16,7 +17,6 @@ import { CdkPipeline } from './pipeline'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; -import { pipelineSynth } from '../private/construct-internals'; /** * Construction properties for a CdkStage diff --git a/packages/@aws-cdk/pipelines/lib/private/asset-singleton-role.ts b/packages/@aws-cdk/pipelines/lib/private/asset-singleton-role.ts new file mode 100644 index 0000000000000..5d52c1d5a47a9 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/asset-singleton-role.ts @@ -0,0 +1,85 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; +import { ConcreteDependable, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +/** + * Role which will be reused across asset jobs + * + * Has some '*' resources to save IAM policy space, and will not + * actually add policies that look like policies that were already added. + */ +export class AssetSingletonRole extends iam.Role { + private _rejectDuplicates = false; + + constructor(scope: Construct, id: string, props: iam.RoleProps) { + super(scope, id, props); + + // Logging permissions + this.addToPolicy(new iam.PolicyStatement({ + resources: [Stack.of(this).formatArn({ + service: 'logs', + resource: 'log-group', + sep: ':', + resourceName: '/aws/codebuild/*', + })], + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + })); + + // CodeBuild report groups + this.addToPolicy(new iam.PolicyStatement({ + actions: [ + 'codebuild:CreateReportGroup', + 'codebuild:CreateReport', + 'codebuild:UpdateReport', + 'codebuild:BatchPutTestCases', + 'codebuild:BatchPutCodeCoverages', + ], + resources: [Stack.of(this).formatArn({ + service: 'codebuild', + resource: 'report-group', + resourceName: '*', + })], + })); + + // CodeBuild start/stop + this.addToPolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 'codebuild:BatchGetBuilds', + 'codebuild:StartBuild', + 'codebuild:StopBuild', + ], + })); + + this._rejectDuplicates = true; + } + + public addToPrincipalPolicy(statement: PolicyStatement): iam.AddToPrincipalPolicyResult { + const json = statement.toStatementJson(); + const acts = JSON.stringify(json.Action); + + // These have already been added with wildcard resources on creation + const alreadyAdded = [ + '["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"]', + '["codebuild:CreateReportGroup","codebuild:CreateReport","codebuild:UpdateReport","codebuild:BatchPutTestCases","codebuild:BatchPutCodeCoverages"]', + '["codebuild:BatchGetBuilds","codebuild:StartBuild","codebuild:StopBuild"]', + ]; + + if (this._rejectDuplicates && alreadyAdded.includes(acts)) { + // Pretend we did it + return { statementAdded: true, policyDependable: new ConcreteDependable() }; + } + + // These are added in duplicate (specifically these come from + // Project#bindToCodePipeline) -- the original singleton asset role didn't + // have these, and they're not necessary either, so in order to not cause + // unnecessary diffs, recognize and drop them there as well. + if (acts === '["kms:Decrypt","kms:Encrypt","kms:ReEncrypt*","kms:GenerateDataKey*"]') { + // Pretend we did it + return { statementAdded: true, policyDependable: new ConcreteDependable() }; + } + + return super.addToPrincipalPolicy(statement); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts index 8a465309016ef..c1b72cf7ab316 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts @@ -670,7 +670,7 @@ describe('pipeline with VPC', () => { } }); - behavior('Asset publishing CodeBuild Projects have a dependency on attached policies to the role', (suite) => { + behavior('Asset publishing CodeBuild Projects have correct VPC permissions', (suite) => { suite.legacy(() => { const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk', { vpc, @@ -690,17 +690,31 @@ describe('pipeline with VPC', () => { function THEN_codePipelineExpectation() { // Assets Project + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Resource: '*', + Action: [ + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeVpcs', + ], + }, + ], + }, + Roles: [{ Ref: 'CdkAssetsDockerRole484B6DD3' }], + }); expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { Properties: { - ServiceRole: { - 'Fn::GetAtt': [ - 'CdkAssetsDockerRole484B6DD3', - 'Arn', - ], - }, + ServiceRole: { 'Fn::GetAtt': ['CdkAssetsDockerRole484B6DD3', 'Arn'] }, }, DependsOn: [ - 'CdkAssetsDockerRoleVpcPolicy86CA024B', + 'CdkAssetsDockerAsset1PolicyDocument8DA96A22', ], }, ResourcePart.CompleteDefinition); } @@ -939,3 +953,90 @@ function expectedAssetRolePolicy(assumeRolePattern: string | string[], attachedR Roles: [{ Ref: attachedRole }], }; } + + +behavior('necessary secrets manager permissions get added to asset roles', suite => { + // Not possible to configure this for legacy pipelines + suite.doesNotApply.legacy(); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + assetPublishingCodeBuildDefaults: { + buildEnvironment: { + environmentVariables: { + FOOBAR: { + value: 'FoobarSecret', + type: cb.BuildEnvironmentVariableType.SECRETS_MANAGER, + }, + }, + }, + }, + }); + pipeline.addStage(new FileAssetApp(pipelineStack, 'MyApp')); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: 'secretsmanager:GetSecretValue', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':secretsmanager:us-pipeline:123pipeline:secret:FoobarSecret-??????', + ], + ], + }, + }), + }, + Roles: [ + { Ref: 'PipelineAssetsFileRole59943A77' }, + ], + }); + } +}); + +behavior('adding environment variable to assets job adds SecretsManager permissions', suite => { + // No way to manipulate buildEnvironment in legacy API + suite.doesNotApply.legacy(); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + assetPublishingCodeBuildDefaults: { + buildEnvironment: { + environmentVariables: { + FOOBAR: { + value: 'FoobarSecret', + type: cb.BuildEnvironmentVariableType.SECRETS_MANAGER, + }, + }, + }, + }, + }); + pipeline.addStage(new FileAssetApp(pipelineStack, 'MyApp')); + + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith( + objectLike({ + Action: 'secretsmanager:GetSecretValue', + Effect: 'Allow', + Resource: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':secretsmanager:us-pipeline:123pipeline:secret:FoobarSecret-??????', + ]], + }, + }), + ), + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json new file mode 100644 index 0000000000000..848406b0ad02e --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json @@ -0,0 +1,2401 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "PipelineStack/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "PipelineArtifactsBucketAEA9A052": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucketPolicyF53CCC52": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleB27FAA37": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicy7BDC1ABB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset1CodePipelineActionRoleC0EC649A", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset2CodePipelineActionRole06965A59", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicy7BDC1ABB", + "Roles": [ + { + "Ref": "PipelineRoleB27FAA37" + } + ] + } + }, + "Pipeline9850B417": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleB27FAA37", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "rix0rrr", + "Repo": "cdk-pipelines-demo", + "Branch": "main", + "OAuthToken": "{{resolve:secretsmanager:github-token:SecretString:::}}", + "PollForSourceChanges": false + }, + "Name": "rix0rrr_cdk-pipelines-demo", + "OutputArtifacts": [ + { + "Name": "rix0rrr_cdk-pipelines-demo_Source" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"00ebacfb32b1bde8d3638577308e7b7144dfa3b0a58a83bc6ff38a3b1f26951c\"}]" + }, + "InputArtifacts": [ + { + "Name": "rix0rrr_cdk-pipelines-demo_Source" + } + ], + "Name": "Synth", + "OutputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"9eda7f97d24aac861052bb47a41b80eecdd56096bf9a88a27c88d94c463785c8\"}]" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "SelfMutate", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "UpdatePipeline" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineAssetsFileAsset185A67CB4" + } + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "FileAsset1", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset1CodePipelineActionRoleC0EC649A", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineAssetsFileAsset24D2D639B" + } + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "FileAsset2", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset2CodePipelineActionRole06965A59", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Assets" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack1", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-cfn-exec-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "Synth_Output::assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.template.json" + }, + "InputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "Name": "Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "Beta-Stack1", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "RunOrder": 2 + } + ], + "Name": "Beta" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "Type": "S3" + }, + "RestartExecutionOnUpdate": true + }, + "DependsOn": [ + "PipelineRoleDefaultPolicy7BDC1ABB", + "PipelineRoleB27FAA37" + ] + }, + "PipelineSourcerix0rrrcdkpipelinesdemoWebhookResourceDB0C1BCA": { + "Type": "AWS::CodePipeline::Webhook", + "Properties": { + "Authentication": "GITHUB_HMAC", + "AuthenticationConfiguration": { + "SecretToken": "{{resolve:secretsmanager:github-token:SecretString:::}}" + }, + "Filters": [ + { + "JsonPath": "$.ref", + "MatchEquals": "refs/heads/{Branch}" + } + ], + "TargetAction": "rix0rrr_cdk-pipelines-demo", + "TargetPipeline": { + "Ref": "Pipeline9850B417" + }, + "TargetPipelineVersion": 1, + "RegisterWithThirdParty": true + } + }, + "PipelineBuildSynthCdkBuildProjectRole231EEA2A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ec2:CreateNetworkInterfacePermission", + "Condition": { + "StringEquals": { + "ec2:Subnet": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + ] + } + ], + "ec2:AuthorizedService": "codebuild.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":network-interface/*" + ] + ] + } + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C", + "Roles": [ + { + "Ref": "PipelineBuildSynthCdkBuildProjectRole231EEA2A" + } + ] + } + }, + "PipelineBuildSynthCdkBuildProjectSecurityGroup84F92459": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic generated security group for CodeBuild PipelineStackPipelineBuildSynthCdkBuildProject225CEB2C", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "PipelineBuildSynthCdkBuildProject6BEFA8E6": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectRole231EEA2A", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm ci\",\n \"npm run build\",\n \"npx cdk synth\"\n ]\n }\n },\n \"artifacts\": {\n \"base-directory\": \"cdk.out\",\n \"files\": \"**/*\"\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3", + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectSecurityGroup84F92459", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "DependsOn": [ + "PipelineBuildSynthCdkBuildProjectPolicyDocument4D16371A" + ] + }, + "PipelineBuildSynthCdkBuildProjectPolicyDocument4D16371A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeDhcpOptions", + "ec2:DescribeVpcs" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCdkBuildProjectPolicyDocument4D16371A", + "Roles": [ + { + "Ref": "PipelineBuildSynthCdkBuildProjectRole231EEA2A" + } + ] + } + }, + "PipelineBuildSynthCodePipelineActionRole4E7A6C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProject6BEFA8E6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290", + "Roles": [ + { + "Ref": "PipelineBuildSynthCodePipelineActionRole4E7A6C97" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleDefaultPolicyE626265B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationDAA41400", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleDefaultPolicyE626265B", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutateCodePipelineActionRoleD6D4E5CF" + } + ] + } + }, + "PipelineAssetsFileAsset1CodePipelineActionRoleC0EC649A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineAssetsFileAsset1CodePipelineActionRoleDefaultPolicy5F0BE7E8": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset185A67CB4", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineAssetsFileAsset1CodePipelineActionRoleDefaultPolicy5F0BE7E8", + "Roles": [ + { + "Ref": "PipelineAssetsFileAsset1CodePipelineActionRoleC0EC649A" + } + ] + } + }, + "PipelineAssetsFileAsset2CodePipelineActionRole06965A59": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineAssetsFileAsset2CodePipelineActionRoleDefaultPolicy2399F4BC": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset24D2D639B", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineAssetsFileAsset2CodePipelineActionRoleDefaultPolicy2399F4BC", + "Roles": [ + { + "Ref": "PipelineAssetsFileAsset2CodePipelineActionRole06965A59" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutationRole57E559E8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ec2:CreateNetworkInterfacePermission", + "Condition": { + "StringEquals": { + "ec2:Subnet": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + ] + } + ], + "ec2:AuthorizedService": "codebuild.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":network-interface/*" + ] + ] + } + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineUpdatePipelineSelfMutationDAA41400" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Condition": { + "ForAnyValue:StringEquals": { + "iam:ResourceTag/aws-cdk:bootstrap-role": [ + "image-publishing", + "file-publishing", + "deploy" + ] + } + }, + "Effect": "Allow", + "Resource": "arn:*:iam::12345678:role/*" + }, + { + "Action": "cloudformation:DescribeStacks", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:ListBucket", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutationRoleDefaultPolicyA225DA4E", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutationRole57E559E8" + } + ] + } + }, + "PipelineUpdatePipelineSelfMutationSecurityGroup94164EDC": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic generated security group for CodeBuild PipelineStackPipelineUpdatePipelineSelfMutationE51045FC", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "PipelineUpdatePipelineSelfMutationDAA41400": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationRole57E559E8", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g aws-cdk\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk -a . deploy PipelineStack --require-approval=never --verbose\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3", + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "PipelineUpdatePipelineSelfMutationSecurityGroup94164EDC", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "DependsOn": [ + "PipelineUpdatePipelineSelfMutationPolicyDocumentD327DC74" + ] + }, + "PipelineUpdatePipelineSelfMutationPolicyDocumentD327DC74": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeDhcpOptions", + "ec2:DescribeVpcs" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineUpdatePipelineSelfMutationPolicyDocumentD327DC74", + "Roles": [ + { + "Ref": "PipelineUpdatePipelineSelfMutationRole57E559E8" + } + ] + } + }, + "PipelineAssetsFileRole59943A77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com", + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineAssetsFileRoleDefaultPolicy14DB8755": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/*" + ] + ] + } + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/*" + ] + ] + } + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + ] + }, + { + "Action": "ec2:CreateNetworkInterfacePermission", + "Condition": { + "StringEquals": { + "ec2:Subnet": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + ] + } + ], + "ec2:AuthorizedService": "codebuild.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":network-interface/*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineAssetsFileRoleDefaultPolicy14DB8755", + "Roles": [ + { + "Ref": "PipelineAssetsFileRole59943A77" + } + ] + } + }, + "PipelineAssetsFileAsset1SecurityGroupF04F1AD4": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic generated security group for CodeBuild PipelineStackPipelineAssetsFileAsset10191BEFB", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "PipelineAssetsFileAsset185A67CB4": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineAssetsFileRole59943A77", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.assets.json\\\" --verbose publish \\\"8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5:current_account-current_region\\\"\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3", + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset1SecurityGroupF04F1AD4", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "DependsOn": [ + "PipelineAssetsFileAsset1PolicyDocument4681543E" + ] + }, + "PipelineAssetsFileAsset1PolicyDocument4681543E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeDhcpOptions", + "ec2:DescribeVpcs" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineAssetsFileAsset1PolicyDocument4681543E", + "Roles": [ + { + "Ref": "PipelineAssetsFileRole59943A77" + } + ] + } + }, + "PipelineAssetsFileAsset2SecurityGroupA400C1A5": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatic generated security group for CodeBuild PipelineStackPipelineAssetsFileAsset24DB856A2", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "PipelineAssetsFileAsset24D2D639B": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineAssetsFileRole59943A77", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install -g cdk-assets\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"cdk-assets --path \\\"assembly-PipelineStack-Beta/PipelineStackBetaStack1E6541489.assets.json\\\" --verbose publish \\\"ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e:current_account-current_region\\\"\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3", + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "PipelineAssetsFileAsset2SecurityGroupA400C1A5", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "DependsOn": [ + "PipelineAssetsFileAsset1PolicyDocument4681543E" + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.ts b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.ts new file mode 100644 index 0000000000000..590757335081f --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.ts @@ -0,0 +1,57 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +/// !cdk-integ PipelineStack +import * as path from 'path'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { App, Stack, StackProps, Stage, StageProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as pipelines from '../lib'; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'Vpc'); + + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + codeBuildDefaults: { vpc }, + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub('rix0rrr/cdk-pipelines-demo', 'main'), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }), + }); + + pipeline.addStage(new AppStage(this, 'Beta')); + } +} + +class AppStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack = new Stack(this, 'Stack1'); + new s3_assets.Asset(stack, 'Asset', { + path: path.join(__dirname, 'testhelpers/assets/test-file-asset.txt'), + }); + new s3_assets.Asset(stack, 'Asset2', { + path: path.join(__dirname, 'testhelpers/assets/test-file-asset-two.txt'), + }); + + new sqs.Queue(stack, 'OtherQueue'); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, +}); +new PipelineStack(app, 'PipelineStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +}); +app.synth(); \ No newline at end of file From 774b9ed209558dbb94475ad66d854a907205028a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 23 Jul 2021 13:22:16 +0200 Subject: [PATCH 103/105] docs(pipelines): fix table rendering in MarkDown (#15734) GitHub's MarkDown rendering doesn't recognize the `+` in a table column separator. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/ORIGINAL_API.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/pipelines/ORIGINAL_API.md b/packages/@aws-cdk/pipelines/ORIGINAL_API.md index 14152b5b7f6d6..119e0037408e8 100644 --- a/packages/@aws-cdk/pipelines/ORIGINAL_API.md +++ b/packages/@aws-cdk/pipelines/ORIGINAL_API.md @@ -17,7 +17,7 @@ Replace `new CdkPipeline` with `new CodePipeline`. Some configuration properties have been changed: | Old API | New API | -|--------------------------------+------------------------------------------------------------------------------------------------| +|--------------------------------|------------------------------------------------------------------------------------------------| | `cloudAssemblyArtifact` | removed | | `sourceAction` | removed | | `synthAction` | `synth` | @@ -96,7 +96,7 @@ potentially `addWave().addStage()`. All stages inside a wave are deployed in parallel, which was not a capability of the original API. | Old API | New API | -|-------------------------------+-------------------------------------------------------------------------------------------------------------------------------| +|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------| | `addApplicationStage()` | `addStage()` | | `addStage().addApplication()` | `addStage()`. Adding multiple CDK Stages into a single Pipeline stage is not supported, add multiple Pipeline stages instead. | From 81cbfec5ddf065aac442d925484a358ee8cd26a1 Mon Sep 17 00:00:00 2001 From: Julian Michel Date: Sat, 24 Jul 2021 00:22:22 +0200 Subject: [PATCH 104/105] fix(elasticsearch): advancedOptions in domain has no effect (#15330) Property `advancedOptions` in ElasticSearch domain did have no effect because the assignment was missing. * add assignment for advancedOptions to fix issue * test cases * describe function in readme Fixes #14067 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-elasticsearch/README.md | 15 ++++ .../@aws-cdk/aws-elasticsearch/lib/domain.ts | 2 + .../aws-elasticsearch/test/domain.test.ts | 28 ++++++++ .../test/integ.elasticsearch.expected.json | 68 +++++++++++++++---- .../test/integ.elasticsearch.ts | 7 +- 5 files changed, 106 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 8585ffdd8bc55..7a002f3d2cfde 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -290,3 +290,18 @@ new Domain(stack, 'Domain', { It is also possible to specify a custom certificate instead of the auto-generated one. Additionally, an automatic CNAME-Record is created if a hosted zone is provided for the custom endpoint + +## Advanced options + +[Advanced options](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-advanced-options) can used to configure additional options. + +```ts +new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + advancedOptions: { + 'rest.action.multi.allow_explicit_index': 'false', + 'indices.fielddata.cache.size': '25', + 'indices.query.bool.max_clause_count': '2048', + }, +}); +``` diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index a29b530128c51..842071fa0ec68 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -410,6 +410,7 @@ export interface DomainProps { /** * Additional options to specify for the Amazon ES domain. * + * @see https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-advanced-options * @default - no advanced options are specified */ readonly advancedOptions?: { [key: string]: (string) }; @@ -1678,6 +1679,7 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { }, } : undefined, + advancedOptions: props.advancedOptions, }); this.domain.applyRemovalPolicy(props.removalPolicy); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 719341aa29408..6966882c55549 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -1633,6 +1633,34 @@ describe('unsigned basic auth', () => { }); }); +describe('advanced options', () => { + test('use advanced options', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + advancedOptions: { + 'rest.action.multi.allow_explicit_index': 'true', + 'indices.fielddata.cache.size': '50', + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedOptions: { + 'rest.action.multi.allow_explicit_index': 'true', + 'indices.fielddata.cache.size': '50', + }, + }); + }); + + test('advanced options absent by default', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedOptions: assert.ABSENT, + }); + }); +}); function testGrant( expectedActions: string[], diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 2f43d85055ad1..4299402202092 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -107,6 +107,11 @@ "Domain19FCBCB91": { "Type": "AWS::Elasticsearch::Domain", "Properties": { + "AdvancedOptions": { + "rest.action.multi.allow_explicit_index": "false", + "indices.fielddata.cache.size": "25", + "indices.query.bool.max_clause_count": "2048" + }, "CognitoOptions": { "Enabled": false }, @@ -206,7 +211,15 @@ { "Ref": "Domain19FCBCB91" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain19FCBCB91" }, @@ -222,7 +235,15 @@ { "Ref": "Domain19FCBCB91" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain19FCBCB91" }, @@ -275,7 +296,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3BucketD609D0D9" + "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD" }, "S3Key": { "Fn::Join": [ @@ -288,7 +309,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3VersionKey77CF589B" + "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" } ] } @@ -301,7 +322,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3VersionKey77CF589B" + "Ref": "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A" } ] } @@ -432,6 +453,11 @@ "Domain2644FE48C": { "Type": "AWS::Elasticsearch::Domain", "Properties": { + "AdvancedOptions": { + "rest.action.multi.allow_explicit_index": "false", + "indices.fielddata.cache.size": "25", + "indices.query.bool.max_clause_count": "2048" + }, "CognitoOptions": { "Enabled": false }, @@ -531,7 +557,15 @@ { "Ref": "Domain2644FE48C" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain2644FE48C" }, @@ -547,7 +581,15 @@ { "Ref": "Domain2644FE48C" }, - "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"*\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"AWS\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root\\\"},\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", { "Ref": "Domain2644FE48C" }, @@ -566,17 +608,17 @@ } }, "Parameters": { - "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3BucketD609D0D9": { + "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3BucketB17E5ABD": { "Type": "String", - "Description": "S3 bucket for asset \"4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02c\"" + "Description": "S3 bucket for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" }, - "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cS3VersionKey77CF589B": { + "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4S3VersionKey77778F6A": { "Type": "String", - "Description": "S3 key for asset version \"4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02c\"" + "Description": "S3 key for asset version \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" }, - "AssetParameters4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02cArtifactHash86CFA15D": { + "AssetParameters5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4ArtifactHash580E429C": { "Type": "String", - "Description": "Artifact hash for asset \"4600faecd25ab407ff0a9d16f935c93062aaea5d415e97046bb8befe6c8ec02c\"" + "Description": "Artifact hash for asset \"5c61041c12314e1ad8e67a0107fa3733382a206a78cdc1576fffa7e93caca5b4\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index d2851bd3d47b9..fb112d26390ee 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -24,12 +24,17 @@ class TestStack extends Stack { encryptionAtRest: { enabled: true, }, + advancedOptions: { + 'rest.action.multi.allow_explicit_index': 'false', + 'indices.fielddata.cache.size': '25', + 'indices.query.bool.max_clause_count': '2048', + }, // test the access policies custom resource works accessPolicies: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['es:ESHttp*'], - principals: [new iam.AnyPrincipal()], + principals: [new iam.AccountRootPrincipal()], resources: ['*'], }), ], From a8b1c471b7058bbf739a1d4f5b4860656ebd5432 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 26 Jul 2021 12:13:52 +0100 Subject: [PATCH 105/105] feat(assertions): retrieve matching resources from the template (#15642) Provide API `findResources()` that retrieves the matching resources from the template given its type and optional predicate. For complex assertions that cannot be modeled using the primitives provided by this module, this API allows an 'escape hatch' so that assertions can be written directly into the test case. This is being used in the `aws-cloudwatch` module, specifically to assert widgets in a CloudWatch Dashboard that are modeled as serialized JSON within a property in the resource. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assertions/README.md | 6 +- packages/@aws-cdk/assertions/lib/index.ts | 2 +- .../{has-resource.ts => private/resource.ts} | 60 +++++++++++++----- .../lib/{assertions.ts => template.ts} | 13 +++- .../{assertions.test.ts => template.test.ts} | 62 +++++++++++++++++++ packages/@aws-cdk/aws-cloudwatch/package.json | 4 +- .../aws-cloudwatch/test/alarm.test.ts | 27 ++++---- .../test/composite-alarm.test.ts | 4 +- .../test/cross-environment.test.ts | 4 +- .../aws-cloudwatch/test/dashboard.test.ts | 44 ++++++------- .../aws-cloudwatch/test/metric-math.test.ts | 4 +- .../aws-cloudwatch/test/metrics.test.ts | 6 +- 12 files changed, 172 insertions(+), 64 deletions(-) rename packages/@aws-cdk/assertions/lib/{has-resource.ts => private/resource.ts} (53%) rename packages/@aws-cdk/assertions/lib/{assertions.ts => template.ts} (86%) rename packages/@aws-cdk/assertions/test/{assertions.test.ts => template.test.ts} (80%) diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 5d61312a812cb..1a79f8294cf3b 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -62,7 +62,7 @@ in a template. assert.resourceCountIs('Foo::Bar', 2); ``` -## Resource Matching +## Resource Matching & Retrieval Beyond resource counting, the module also allows asserting that a resource with specific properties are present. @@ -88,6 +88,10 @@ assert.hasResource('Foo::Bar', { }); ``` +Beyond assertions, the module provides APIs to retrieve matching resources. +The `findResources()` API is complementary to the `hasResource()` API, except, +instead of asserting its presence, it returns the set of matching resources. + By default, the `hasResource()` and `hasResourceProperties()` APIs perform deep partial object matching. This behavior can be configured using matchers. See subsequent section on [special matchers](#special-matchers). diff --git a/packages/@aws-cdk/assertions/lib/index.ts b/packages/@aws-cdk/assertions/lib/index.ts index 937b59823762c..963039f921bc1 100644 --- a/packages/@aws-cdk/assertions/lib/index.ts +++ b/packages/@aws-cdk/assertions/lib/index.ts @@ -1,3 +1,3 @@ -export * from './assertions'; +export * from './template'; export * from './match'; export * from './matcher'; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/has-resource.ts b/packages/@aws-cdk/assertions/lib/private/resource.ts similarity index 53% rename from packages/@aws-cdk/assertions/lib/has-resource.ts rename to packages/@aws-cdk/assertions/lib/private/resource.ts index e4b6bf3c1eb06..22a2b01734b70 100644 --- a/packages/@aws-cdk/assertions/lib/has-resource.ts +++ b/packages/@aws-cdk/assertions/lib/private/resource.ts @@ -1,6 +1,20 @@ -import { Match } from './match'; -import { Matcher, MatchResult } from './matcher'; -import { StackInspector } from './vendored/assert'; +import { Match } from '../match'; +import { Matcher, MatchResult } from '../matcher'; +import { StackInspector } from '../vendored/assert'; + +export function findResources(inspector: StackInspector, type: string, props: any = {}): { [key: string]: any }[] { + const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props); + let results: { [key: string]: any }[] = []; + + eachResourceWithType(inspector, type, (resource) => { + const result = matcher.test(resource); + if (!result.hasFailed()) { + results.push(resource); + } + }); + + return results; +} export function hasResource(inspector: StackInspector, type: string, props: any): string | void { const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props); @@ -8,19 +22,22 @@ export function hasResource(inspector: StackInspector, type: string, props: any) let closestResource: { [key: string]: any } | undefined = undefined; let count: number = 0; - for (const logicalId of Object.keys(inspector.value.Resources ?? {})) { - const resource: { [key: string]: any } = inspector.value.Resources[logicalId]; - if (resource.Type === type) { - count++; - const result = matcher.test(resource); - if (!result.hasFailed()) { - return; - } - if (closestResult === undefined || closestResult.failCount > result.failCount) { - closestResult = result; - closestResource = resource; - } + let match = false; + eachResourceWithType(inspector, type, (resource) => { + if (match) { return; } + count++; + const result = matcher.test(resource); + if (!result.hasFailed()) { + match = true; + } + if (closestResult === undefined || closestResult.failCount > result.failCount) { + closestResult = result; + closestResource = resource; } + }); + + if (match) { + return; } if (closestResult === undefined) { @@ -37,6 +54,19 @@ export function hasResource(inspector: StackInspector, type: string, props: any) ].join('\n'); } +function eachResourceWithType( + inspector: StackInspector, + type: string, + cb: (resource: {[key: string]: any}) => void): void { + + for (const logicalId of Object.keys(inspector.value.Resources ?? {})) { + const resource: { [key: string]: any } = inspector.value.Resources[logicalId]; + if (resource.Type === type) { + cb(resource); + } + } +} + function formatMessage(closestResult: MatchResult, closestResource: {}): string { return [ 'The closest result is:', diff --git a/packages/@aws-cdk/assertions/lib/assertions.ts b/packages/@aws-cdk/assertions/lib/template.ts similarity index 86% rename from packages/@aws-cdk/assertions/lib/assertions.ts rename to packages/@aws-cdk/assertions/lib/template.ts index aa7eaeb0bfd7d..c76f8150292a2 100644 --- a/packages/@aws-cdk/assertions/lib/assertions.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -1,7 +1,7 @@ import { Stack, Stage } from '@aws-cdk/core'; -import { hasResource } from './has-resource'; import { Match } from './match'; import { Matcher } from './matcher'; +import { findResources, hasResource } from './private/resource'; import * as assert from './vendored/assert'; /** @@ -82,6 +82,17 @@ export class TemplateAssertions { } } + /** + * Get the set of matching resources of a given type and properties in the CloudFormation template. + * @param type the type to match in the CloudFormation template + * @param props by default, matches all resources with the given type. + * When a literal is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findResources(type: string, props: any = {}): { [key: string]: any }[] { + return findResources(this.inspector, type, props); + } + /** * Assert that the CloudFormation template matches the given value * @param expected the expected CloudFormation template as key-value pairs. diff --git a/packages/@aws-cdk/assertions/test/assertions.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts similarity index 80% rename from packages/@aws-cdk/assertions/test/assertions.test.ts rename to packages/@aws-cdk/assertions/test/template.test.ts index bd0cdb828f921..70b09b618446c 100644 --- a/packages/@aws-cdk/assertions/test/assertions.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -221,6 +221,68 @@ describe('TemplateAssertions', () => { })).toThrow(/No resource/); }); }); + + describe('getResources', () => { + test('matching resource type', () => { + const stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { baz: 'qux', fred: 'waldo' }, + }); + + const inspect = TemplateAssertions.fromStack(stack); + expect(inspect.findResources('Foo::Bar')).toEqual([{ + Type: 'Foo::Bar', + Properties: { baz: 'qux', fred: 'waldo' }, + }]); + }); + + test('no matching resource type', () => { + const stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { baz: 'qux', fred: 'waldo' }, + }); + + const inspect = TemplateAssertions.fromStack(stack); + expect(inspect.findResources('Foo::Baz')).toEqual([]); + }); + + test('matching resource props', () => { + const stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { baz: 'qux', fred: 'waldo' }, + }); + + const inspect = TemplateAssertions.fromStack(stack); + expect(inspect.findResources('Foo::Bar', { + Properties: { baz: 'qux' }, + }).length).toEqual(1); + }); + + test('no matching resource props', () => { + const stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { baz: 'qux', fred: 'waldo' }, + }); + + const inspect = TemplateAssertions.fromStack(stack); + expect(inspect.findResources('Foo::Bar', { + Properties: { baz: 'waldo' }, + })).toEqual([]); + }); + + test('multiple matching resources', () => { + const stack = new Stack(); + new CfnResource(stack, 'Foo', { type: 'Foo::Bar' }); + new CfnResource(stack, 'Bar', { type: 'Foo::Bar' }); + + const inspect = TemplateAssertions.fromStack(stack); + expect(inspect.findResources('Foo::Bar').length).toEqual(2); + }); + }); }); function expectToThrow(fn: () => void, msgs: (RegExp | string)[], done: jest.DoneCallback): void { diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index 246ad8a89ef37..683ca79bde154 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -73,13 +73,13 @@ }, "license": "Apache-2.0", "devDependencies": { + "@aws-cdk/assertions": "0.0.0", "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", - "pkglint": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts index bfe3eca913b67..437dbc5df4ab8 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/alarm.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { ABSENT } from '@aws-cdk/assert-internal'; +import { Match, TemplateAssertions } from '@aws-cdk/assertions'; import { Duration, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Alarm, IAlarm, IAlarmAction, Metric, MathExpression, IMetric } from '../lib'; @@ -68,7 +67,7 @@ describe('Alarm', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, MetricName: 'Metric', @@ -94,7 +93,7 @@ describe('Alarm', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, MetricName: 'Metric', @@ -120,14 +119,14 @@ describe('Alarm', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, MetricName: 'Metric', Namespace: 'CDK/Test', Period: 300, Statistic: 'Maximum', - ExtendedStatistic: ABSENT, + ExtendedStatistic: Match.absentProperty(), Threshold: 1000, }); @@ -147,13 +146,13 @@ describe('Alarm', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, MetricName: 'Metric', Namespace: 'CDK/Test', Period: 300, - Statistic: ABSENT, + Statistic: Match.absentProperty(), ExtendedStatistic: 'p99', Threshold: 1000, }); @@ -174,7 +173,7 @@ describe('Alarm', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 3, DatapointsToAlarm: 2, @@ -204,7 +203,7 @@ describe('Alarm', () => { alarm.addOkAction(new TestAlarmAction('C')); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { AlarmActions: ['A'], InsufficientDataActions: ['B'], OKActions: ['C'], @@ -226,7 +225,7 @@ describe('Alarm', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 2, MetricName: 'Metric', @@ -251,7 +250,7 @@ describe('Alarm', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { ExtendedStatistic: 'p99.9', }); @@ -270,8 +269,8 @@ describe('Alarm', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { - Statistic: ABSENT, + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { + Statistic: Match.absentProperty(), ExtendedStatistic: 'tm99.9999999999', }); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts index 054f1b21724ee..e77d33a546ca5 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { Alarm, AlarmRule, AlarmState, CompositeAlarm, Metric } from '../lib'; @@ -59,7 +59,7 @@ describe('CompositeAlarm', () => { alarmRule, }); - expect(stack).toHaveResource('AWS::CloudWatch::CompositeAlarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::CompositeAlarm', { AlarmName: 'CompositeAlarm', AlarmRule: { 'Fn::Join': [ diff --git a/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts index 959ceafab54fc..fc00cda0753d1 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/cross-environment.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; import { Alarm, GraphWidget, IWidget, Metric } from '../lib'; @@ -89,7 +89,7 @@ describe('cross environment', () => { }); // THEN - expect(stack1).toHaveResourceLike('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack1).hasResourceProperties('AWS::CloudWatch::Alarm', { MetricName: 'ACount', Namespace: 'Test', Period: 300, diff --git a/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts index 5501a47ba5c3a..cf3c4caa96042 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/dashboard.test.ts @@ -1,5 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; -import { isSuperObject } from '@aws-cdk/assert-internal'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import { Dashboard, GraphWidget, PeriodOverride, TextWidget } from '../lib'; @@ -27,11 +26,13 @@ describe('Dashboard', () => { })); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', thatHasWidgets([ + const resources = TemplateAssertions.fromStack(stack).findResources('AWS::CloudWatch::Dashboard'); + expect(resources.length).toEqual(1); + hasWidgets(resources[0].Properties, [ { type: 'text', width: 10, height: 2, x: 0, y: 0, properties: { markdown: 'first' } }, { type: 'text', width: 1, height: 4, x: 0, y: 2, properties: { markdown: 'second' } }, { type: 'text', width: 4, height: 1, x: 0, y: 6, properties: { markdown: 'third' } }, - ])); + ]); }); @@ -61,11 +62,13 @@ describe('Dashboard', () => { ); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', thatHasWidgets([ + const resources = TemplateAssertions.fromStack(stack).findResources('AWS::CloudWatch::Dashboard'); + expect(resources.length).toEqual(1); + hasWidgets(resources[0].Properties, [ { type: 'text', width: 10, height: 2, x: 0, y: 0, properties: { markdown: 'first' } }, { type: 'text', width: 1, height: 4, x: 10, y: 0, properties: { markdown: 'second' } }, { type: 'text', width: 4, height: 1, x: 11, y: 0, properties: { markdown: 'third' } }, - ])); + ]); }); @@ -81,7 +84,7 @@ describe('Dashboard', () => { ); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Dashboard', { DashboardBody: { 'Fn::Join': ['', [ '{"widgets":[{"type":"metric","width":1,"height":1,"x":0,"y":0,"properties":{"view":"timeSeries","region":"', @@ -110,7 +113,7 @@ describe('Dashboard', () => { ); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Dashboard', { DashboardBody: { 'Fn::Join': ['', [ '{"start":"-9H","end":"2018-12-17T06:00:00.000Z","periodOverride":"inherit",\ @@ -135,7 +138,7 @@ describe('Dashboard', () => { }); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Dashboard', { DashboardName: 'MyCustomDashboardName', }); @@ -151,7 +154,7 @@ describe('Dashboard', () => { new Dashboard(stack, 'MyDashboard'); // THEN - expect(stack).toHaveResource('AWS::CloudWatch::Dashboard', {}); + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Dashboard', {}); }); @@ -178,15 +181,14 @@ describe('Dashboard', () => { /** * Returns a property predicate that checks that the given Dashboard has the indicated widgets */ -function thatHasWidgets(widgets: any): (props: any) => boolean { - return (props: any) => { - try { - const actualWidgets = JSON.parse(props.DashboardBody).widgets; - return isSuperObject(actualWidgets, widgets); - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error parsing', props); - throw e; - } - }; +function hasWidgets(props: any, widgets: any[]) { + let actualWidgets: any[] = []; + try { + actualWidgets = JSON.parse(props.DashboardBody).widgets; + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error parsing', props); + throw e; + } + expect(actualWidgets).toEqual(expect.arrayContaining(widgets)); } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/metric-math.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/metric-math.test.ts index b9379bbdc360c..52e7803575d05 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/metric-math.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/metric-math.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import { Duration, Stack } from '@aws-cdk/core'; import { Alarm, GraphWidget, IWidget, MathExpression, Metric } from '../lib'; @@ -638,7 +638,7 @@ function graphMetricsAre(w: IWidget, metrics: any[]) { } function alarmMetricsAre(metrics: any[]) { - expect(stack).toHaveResourceLike('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { Metrics: metrics, }); } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts index ff1620f91ed50..f7b5c69f8018b 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/metrics.test.ts @@ -1,4 +1,4 @@ -import '@aws-cdk/assert-internal/jest'; +import { TemplateAssertions } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Alarm, Metric } from '../lib'; @@ -15,7 +15,7 @@ describe('Metrics', () => { Metric.grantPutMetricData(role); // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -188,7 +188,7 @@ describe('Metrics', () => { dimensionA: 'value1', dimensionB: 'value2', }); - expect(stack).toHaveResourceLike('AWS::CloudWatch::Alarm', { + TemplateAssertions.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { Namespace: 'Test', MetricName: 'Metric', Dimensions: [