From d0352df082279588e7392db9dccef5c403ecd916 Mon Sep 17 00:00:00 2001 From: Shivam Verma <68244110+sshver@users.noreply.github.com> Date: Mon, 7 Dec 2020 12:46:14 -0800 Subject: [PATCH] feat(appmesh): add ClientPolicy to VirtualNode, VirtualGateway and VirtualService (#11563) Adds backend defaults to Virtual Node and Virtual Gateways. Adds validation context for the backend defined on the Virtual Node. Before merging (TODO): - [ ] Update README ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-acmpca/lib/certificate-authority.ts | 33 ++++++ packages/@aws-cdk/aws-acmpca/lib/index.ts | 1 + packages/@aws-cdk/aws-appmesh/README.md | 53 ++++++++- .../@aws-cdk/aws-appmesh/lib/client-policy.ts | 110 ++++++++++++++++++ packages/@aws-cdk/aws-appmesh/lib/index.ts | 1 + .../aws-appmesh/lib/shared-interfaces.ts | 3 +- .../aws-appmesh/lib/virtual-gateway.ts | 10 +- .../@aws-cdk/aws-appmesh/lib/virtual-node.ts | 14 ++- .../aws-appmesh/lib/virtual-service.ts | 24 ++++ packages/@aws-cdk/aws-appmesh/package.json | 2 + .../aws-appmesh/test/integ.mesh.expected.json | 26 +++++ .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 10 +- .../aws-appmesh/test/test.virtual-gateway.ts | 43 +++++++ .../aws-appmesh/test/test.virtual-node.ts | 104 ++++++++++++++++- 14 files changed, 427 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/aws-acmpca/lib/certificate-authority.ts create mode 100644 packages/@aws-cdk/aws-appmesh/lib/client-policy.ts diff --git a/packages/@aws-cdk/aws-acmpca/lib/certificate-authority.ts b/packages/@aws-cdk/aws-acmpca/lib/certificate-authority.ts new file mode 100644 index 0000000000000..2c3131976e925 --- /dev/null +++ b/packages/@aws-cdk/aws-acmpca/lib/certificate-authority.ts @@ -0,0 +1,33 @@ +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +/** + * Interface which all CertificateAuthority based class must implement + */ +export interface ICertificateAuthority extends cdk.IResource { + /** + * The Amazon Resource Name of the Certificate + * + * @attribute + */ + readonly certificateAuthorityArn: string; +} + +/** + * Defines a Certificate for ACMPCA + * + * @resource AWS::ACMPCA::CertificateAuthority + */ +export class CertificateAuthority { + /** + * Import an existing Certificate given an ARN + */ + public static fromCertificateAuthorityArn(scope: Construct, id: string, certificateAuthorityArn: string): ICertificateAuthority { + return new class extends cdk.Resource implements ICertificateAuthority { + readonly certificateAuthorityArn = certificateAuthorityArn; + }(scope, id); + } + + private constructor() { + } +} diff --git a/packages/@aws-cdk/aws-acmpca/lib/index.ts b/packages/@aws-cdk/aws-acmpca/lib/index.ts index 6a40358cf4409..11c6bf8e1b620 100644 --- a/packages/@aws-cdk/aws-acmpca/lib/index.ts +++ b/packages/@aws-cdk/aws-acmpca/lib/index.ts @@ -1,2 +1,3 @@ // AWS::ACMPCA CloudFormation Resources: export * from './acmpca.generated'; +export * from './certificate-authority'; diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index c417378403c24..4b38e150a042e 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -163,7 +163,7 @@ const node = mesh.addVirtualNode('virtual-node', { }); ``` -Create a `VirtualNode` with the the constructor and add tags. +Create a `VirtualNode` with the constructor and add tags. ```ts const node = new VirtualNode(this, 'node', { @@ -184,14 +184,57 @@ const node = new VirtualNode(this, 'node', { idle: cdk.Duration.seconds(5), }, })], + backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: '/keys/local_cert_chain.pem', + }), accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), }); cdk.Tag.add(node, 'Environment', 'Dev'); ``` +Create a `VirtualNode` with the constructor and add backend virtual service. + +```ts +const node = new VirtualNode(this, 'node', { + mesh, + cloudMapService: service, + listeners: [appmesh.VirtualNodeListener.httpNodeListener({ + port: 8080, + healthCheck: { + healthyThreshold: 3, + interval: Duration.seconds(5), // min + path: '/ping', + port: 8080, + protocol: Protocol.HTTP, + timeout: Duration.seconds(2), // min + unhealthyThreshold: 2, + }, + timeout: { + idle: cdk.Duration.seconds(5), + }, + })], + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), +}); + +const virtualService = new appmesh.VirtualService(stack, 'service-1', { + virtualServiceName: 'service1.domain.local', + mesh, + clientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: '/keys/local_cert_chain.pem', + ports: [8080, 8081], + }), +}); + +node.addBackend(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 `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 `backendsDefaultClientPolicy` property are added to the node while creating the virtual node. These are virtual node's service backends client policy defaults. + ## 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. @@ -269,6 +312,8 @@ using rules defined in gateway routes which can be added to your virtual gateway Create a virtual gateway with the constructor: ```ts +const certificateAuthorityArn = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012'; + const gateway = new appmesh.VirtualGateway(stack, 'gateway', { mesh: mesh, listeners: [appmesh.VirtualGatewayListener.http({ @@ -277,6 +322,10 @@ const gateway = new appmesh.VirtualGateway(stack, 'gateway', { interval: cdk.Duration.seconds(10), }, })], + backendsDefaultClientPolicy: appmesh.ClientPolicy.acmTrust({ + certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], + ports: [8080, 8081], + }), accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), virtualGatewayName: 'virtualGateway', }); @@ -300,6 +349,8 @@ const gateway = mesh.addVirtualGateway('gateway', { The listeners field can be omitted which will default to an HTTP Listener on port 8080. A gateway route can be added using the `gateway.addGatewayRoute()` method. +The `backendsDefaultClientPolicy` property are added to the node while creating the virtual gateway. These are virtual gateway's service backends client policy defaults. + ## Adding a Gateway Route A _gateway route_ is attached to a virtual gateway and routes traffic to an existing virtual service. diff --git a/packages/@aws-cdk/aws-appmesh/lib/client-policy.ts b/packages/@aws-cdk/aws-appmesh/lib/client-policy.ts new file mode 100644 index 0000000000000..03236ee1c8f32 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/client-policy.ts @@ -0,0 +1,110 @@ +import * as acmpca from '@aws-cdk/aws-acmpca'; +import * as cdk from '@aws-cdk/core'; +import { CfnVirtualNode } from './appmesh.generated'; + +enum CertificateType { + ACMPCA = 'acm', + FILE = 'file', +} + +/** + * Properties of TLS Client Policy + */ +export interface ClientPolicyConfig { + /** + * Represents single Client Policy property + */ + readonly clientPolicy: CfnVirtualNode.ClientPolicyProperty; +} + +/** + * Represents the property needed to define a Client Policy + */ +export interface ClientPolicyOptions { + /** + * TLS is enforced on the ports specified here. + * If no ports are specified, TLS will be enforced on all the ports. + * + * @default - none + */ + readonly ports?: number[]; +} + +/** + * ACM Trust Properties + */ +export interface AcmTrustOptions extends ClientPolicyOptions { + /** + * Contains information for your private certificate authority + */ + readonly certificateAuthorities: acmpca.ICertificateAuthority[]; +} + +/** + * File Trust Properties + */ +export interface FileTrustOptions extends ClientPolicyOptions { + /** + * Path to the Certificate Chain file on the file system where the Envoy is deployed. + */ + readonly certificateChain: string; +} + +/** + * Defines the TLS validation context trust. + */ +export abstract class ClientPolicy { + /** + * Tells envoy where to fetch the validation context from + */ + public static fileTrust(props: FileTrustOptions): ClientPolicy { + return new ClientPolicyImpl(props.ports, CertificateType.FILE, props.certificateChain, undefined); + } + + /** + * TLS validation context trust for ACM Private Certificate Authority (CA). + */ + public static acmTrust(props: AcmTrustOptions): ClientPolicy { + return new ClientPolicyImpl(props.ports, CertificateType.ACMPCA, undefined, props.certificateAuthorities); + } + + /** + * Returns Trust context based on trust type. + */ + public abstract bind(scope: cdk.Construct): ClientPolicyConfig; + +} + +class ClientPolicyImpl extends ClientPolicy { + constructor (private readonly ports: number[] | undefined, + private readonly certificateType: CertificateType, + private readonly certificateChain: string | undefined, + private readonly certificateAuthorityArns: acmpca.ICertificateAuthority[] | undefined) { super(); } + + public bind(_scope: cdk.Construct): ClientPolicyConfig { + if (this.certificateType === CertificateType.ACMPCA && this.certificateAuthorityArns?.map(certificateArn => + certificateArn.certificateAuthorityArn).length === 0) { + throw new Error('You must provide at least one Certificate Authority when creating an ACM Trust ClientPolicy'); + } else { + return { + clientPolicy: { + tls: { + ports: this.ports, + validation: { + trust: { + [this.certificateType]: this.certificateType === CertificateType.FILE + ? { + certificateChain: this.certificateChain, + } + : { + certificateAuthorityArns: this.certificateAuthorityArns?.map(certificateArn => + certificateArn.certificateAuthorityArn), + }, + }, + }, + }, + }, + }; + } + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index 1b468f21ec613..f9e4b6a07ff36 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -13,3 +13,4 @@ export * from './virtual-gateway'; export * from './virtual-gateway-listener'; export * from './gateway-route'; export * from './gateway-route-spec'; +export * from './client-policy'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index 21ab96b6ce56a..c8b3ba88f6378 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -138,4 +138,5 @@ class FileAccessLog extends AccessLog { }, }; } -} \ No newline at end of file +} + diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts index 4a0c5fef1af75..1e1144fed1038 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts @@ -1,8 +1,8 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualGateway } from './appmesh.generated'; +import { ClientPolicy } from './client-policy'; import { GatewayRoute, GatewayRouteBaseProps } from './gateway-route'; - import { IMesh, Mesh } from './mesh'; import { AccessLog } from './shared-interfaces'; import { VirtualGatewayListener, VirtualGatewayListenerConfig } from './virtual-gateway-listener'; @@ -60,6 +60,13 @@ export interface VirtualGatewayBaseProps { * @default - no access logging */ readonly accessLog?: AccessLog; + + /** + * Default Configuration Virtual Node uses to communicate with Virtual Service + * + * @default - No Config + */ + readonly backendsDefaultClientPolicy?: ClientPolicy; } /** @@ -173,6 +180,7 @@ export class VirtualGateway extends VirtualGatewayBase { meshName: this.mesh.meshName, spec: { listeners: this.listeners.map(listener => listener.listener), + backendDefaults: props.backendsDefaultClientPolicy?.bind(this), logging: accessLogging !== undefined ? { accessLog: accessLogging.virtualGatewayAccessLog, } : undefined, diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts index e868ce69936c7..f092b85caa0e2 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts @@ -2,6 +2,7 @@ import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualNode } from './appmesh.generated'; +import { ClientPolicy } from './client-policy'; import { IMesh, Mesh } from './mesh'; import { AccessLog } from './shared-interfaces'; import { VirtualNodeListener, VirtualNodeListenerConfig } from './virtual-node-listener'; @@ -94,6 +95,13 @@ export interface VirtualNodeBaseProps { * @default - No access logging */ readonly accessLog?: AccessLog; + + /** + * Default Configuration Virtual Node uses to communicate with Virtual Service + * + * @default - No Config + */ + readonly backendsDefaultClientPolicy?: ClientPolicy; } /** @@ -193,8 +201,9 @@ export class VirtualNode extends VirtualNodeBase { virtualNodeName: this.physicalName, meshName: this.mesh.meshName, spec: { - backends: cdk.Lazy.any({ produce: () => this.backends }, { omitEmptyArray: true }), - listeners: cdk.Lazy.any({ produce: () => this.listeners.map(listener => listener.listener) }, { omitEmptyArray: true }), + backends: cdk.Lazy.anyValue({ produce: () => this.backends }, { omitEmptyArray: true }), + listeners: cdk.Lazy.anyValue({ produce: () => this.listeners.map(listener => listener.listener) }, { omitEmptyArray: true }), + backendDefaults: props.backendsDefaultClientPolicy?.bind(this), serviceDiscovery: { dns: props.dnsHostName !== undefined ? { hostname: props.dnsHostName } : undefined, awsCloudMap: props.cloudMapService !== undefined ? { @@ -231,6 +240,7 @@ export class VirtualNode extends VirtualNodeBase { this.backends.push({ virtualService: { virtualServiceName: virtualService.virtualServiceName, + clientPolicy: virtualService.clientPolicy?.bind(this).clientPolicy, }, }); } diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts index 677eb96b3e4a8..9ca5d5010b7f4 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts @@ -1,6 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualService } from './appmesh.generated'; +import { ClientPolicy } from './client-policy'; import { IMesh, Mesh } from './mesh'; import { IVirtualNode } from './virtual-node'; import { IVirtualRouter } from './virtual-router'; @@ -27,6 +28,11 @@ export interface IVirtualService extends cdk.IResource { * The Mesh which the VirtualService belongs to */ readonly mesh: IMesh; + + /** + * Client policy for this Virtual Service + */ + readonly clientPolicy?: ClientPolicy; } /** @@ -57,6 +63,13 @@ export interface VirtualServiceBaseProps { * @default - At most one of virtualRouter and virtualNode is allowed. */ readonly virtualNode?: IVirtualNode; + + /** + * Client policy for this Virtual Service + * + * @default - none + */ + readonly clientPolicy?: ClientPolicy; } /** @@ -96,6 +109,7 @@ export class VirtualService extends cdk.Resource implements IVirtualService { return new class extends cdk.Resource implements IVirtualService { readonly virtualServiceName = attrs.virtualServiceName; readonly mesh = attrs.mesh; + readonly clientPolicy = attrs.clientPolicy; readonly virtualServiceArn = cdk.Stack.of(this).formatArn({ service: 'appmesh', resource: `mesh/${attrs.mesh.meshName}/virtualService`, @@ -119,6 +133,8 @@ export class VirtualService extends cdk.Resource implements IVirtualService { */ public readonly mesh: IMesh; + public readonly clientPolicy?: ClientPolicy; + private readonly virtualServiceProvider?: CfnVirtualService.VirtualServiceProviderProperty; constructor(scope: Construct, id: string, props: VirtualServiceProps) { @@ -131,6 +147,7 @@ export class VirtualService extends cdk.Resource implements IVirtualService { } this.mesh = props.mesh; + this.clientPolicy = props.clientPolicy; // Check which provider to use node or router (or neither) if (props.virtualRouter) { @@ -186,4 +203,11 @@ export interface VirtualServiceAttributes { * The Mesh which the VirtualService belongs to */ readonly mesh: IMesh; + + /** + * Client policy for this Virtual Service + * + * @default - none + */ + readonly clientPolicy?: ClientPolicy; } diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index f200312093d3a..c956c2e77c844 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -86,6 +86,7 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-acmpca": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", @@ -93,6 +94,7 @@ "constructs": "^3.2.0" }, "peerDependencies": { + "@aws-cdk/aws-acmpca": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", 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 e74a824311783..e3a9f98c5a016 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -691,6 +691,19 @@ ] }, "Spec": { + "BackendDefaults": { + "ClientPolicy": { + "TLS": { + "Validation": { + "Trust": { + "ACM": { + "CertificateAuthorityArns": ["arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012"] + } + } + } + } + } + }, "Backends": [ { "VirtualService": { @@ -739,6 +752,19 @@ ] }, "Spec": { + "BackendDefaults": { + "ClientPolicy": { + "TLS": { + "Validation": { + "Trust": { + "File": { + "CertificateChain": "path-to-certificate" + } + } + } + } + } + }, "Listeners": [ { "HealthCheck": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 1a064b5ce7031..63e027b8ef0e5 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -1,7 +1,7 @@ +import * as acmpca from '@aws-cdk/aws-acmpca'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; - import * as appmesh from '../lib/'; export const app = new cdk.App(); @@ -62,6 +62,8 @@ router.addRoute('route-1', { }), }); +const certificateAuthorityArn = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012'; + const node2 = mesh.addVirtualNode('node2', { dnsHostName: `node2.${namespace.namespaceName}`, listeners: [appmesh.VirtualNodeListener.http({ @@ -75,6 +77,9 @@ const node2 = mesh.addVirtualNode('node2', { unhealthyThreshold: 2, }, })], + backendsDefaultClientPolicy: appmesh.ClientPolicy.acmTrust({ + certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], + }), backends: [ new appmesh.VirtualService(stack, 'service-3', { virtualServiceName: 'service3.domain.local', @@ -96,6 +101,9 @@ const node3 = mesh.addVirtualNode('node3', { unhealthyThreshold: 2, }, })], + backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: 'path-to-certificate', + }), accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), }); 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 8e76b0f5c85f5..6207e49ce0be1 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -238,6 +238,49 @@ export = { test.done(); }, }, + + 'When creating a VirtualGateway with backend defaults': { + 'should add backend defaults to the VirtualGateway'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + new appmesh.VirtualGateway(stack, 'virtual-gateway', { + virtualGatewayName: 'virtual-gateway', + mesh: mesh, + backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: 'path-to-certificate', + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + VirtualGatewayName: 'virtual-gateway', + Spec: { + BackendDefaults: { + ClientPolicy: { + TLS: { + Validation: { + Trust: { + File: { + CertificateChain: 'path-to-certificate', + }, + }, + }, + }, + }, + }, + }, + })); + + test.done(); + }, + }, + 'Can import VirtualGateways using an ARN'(test: Test) { const app = new cdk.App(); // GIVEN 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 75c7d0579e71b..739b34476331c 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -1,7 +1,7 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; +import * as acmpca from '@aws-cdk/aws-acmpca'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; - import * as appmesh from '../lib'; export = { @@ -255,6 +255,108 @@ export = { test.done(); }, }, + + 'when a default backend is added': { + 'should add a backend default to the resource'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const certificateAuthorityArn = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012'; + + new appmesh.VirtualNode(stack, 'test-node', { + mesh, + dnsHostName: 'test', + backendsDefaultClientPolicy: appmesh.ClientPolicy.acmTrust({ + certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], + ports: [8080, 8081], + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualNode', { + Spec: { + BackendDefaults: { + ClientPolicy: { + TLS: { + Ports: [8080, 8081], + Validation: { + Trust: { + ACM: { + CertificateAuthorityArns: [`${certificateAuthorityArn}`], + }, + }, + }, + }, + }, + }, + }, + })); + + test.done(); + }, + }, + + 'when a backend is added': { + 'should add a backend virtual service to the resource'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = new appmesh.VirtualNode(stack, 'test-node', { + mesh, + dnsHostName: 'test', + }); + + const service1 = new appmesh.VirtualService(stack, 'service-1', { + virtualServiceName: 'service1.domain.local', + mesh, + clientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: 'path-to-certificate', + ports: [8080, 8081], + }), + }); + + node.addBackend(service1); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualNode', { + Spec: { + Backends: [ + { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['service1A48078CF', 'VirtualServiceName'], + }, + ClientPolicy: { + TLS: { + Ports: [8080, 8081], + Validation: { + Trust: { + File: { + CertificateChain: 'path-to-certificate', + }, + }, + }, + }, + }, + }, + }, + ], + }, + })); + + test.done(); + }, + }, }, 'Can import Virtual Nodes using an ARN'(test: Test) { // GIVEN