diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 1becd4cf81772..e38a8ea49a9e9 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -126,8 +126,6 @@ mesh.addVirtualService('virtual-service', { A `virtual node` acts as a logical pointer to a particular task group, such as an Amazon ECS service or a Kubernetes deployment. -![Virtual node logical diagram](https://docs.aws.amazon.com/app-mesh/latest/userguide/images/virtual_node.png) - When you create a `virtual node`, you must specify the DNS service discovery hostname for your task group. 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. 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. @@ -144,7 +142,6 @@ const namespace = new servicediscovery.PrivateDnsNamespace(this, 'test-namespace const service = namespace.createService('Svc'); const node = mesh.addVirtualNode('virtual-node', { - dnsHostName: 'node-a', cloudMapService: service, listener: { portMapping: { @@ -170,7 +167,6 @@ Create a `VirtualNode` with the the constructor and add tags. ```typescript const node = new VirtualNode(this, 'node', { mesh, - dnsHostName: 'node-1', cloudMapService: service, listener: { portMapping: { @@ -193,7 +189,7 @@ const node = new VirtualNode(this, 'node', { cdk.Tag.add(node, 'Environment', 'Dev'); ``` -The listeners property can be left blank and added later with the `mesh.addListeners()` method. The `healthcheck` property is optional but if specifying a listener, the `portMappings` must contain at least one property. +The listeners property can be left blank and added later with the `node.addListeners()` method. The `healthcheck` property is optional but if specifying a listener, the `portMappings` must contain at least one property. ## Adding a Route @@ -235,34 +231,79 @@ router.addRoute('route', { }); ``` -Multiple routes may also be added at once to different applications or targets. +## Adding a Virtual Gateway + +A _virtual gateway_ allows resources outside your mesh to communicate to resources that are 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. + +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, +using rules defined in gateway routes which can be added to your virtual gateway. + +Create a virtual gateway with the constructor: ```typescript -ratingsRouter.addRoutes( - ['route1', 'route2'], - [ - { - routeTargets: [ - { - virtualNode, - weight: 1, - }, - ], - prefix: `/path-to-app`, - routeType: RouteType.HTTP, +const gateway = new appmesh.VirtualGateway(stack, 'gateway', { + mesh: mesh, + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), }, - { - routeTargets: [ - { - virtualNode: virtualNode2, - weight: 1, - }, - ], - prefix: `/path-to-app2`, - routeType: RouteType.HTTP, + })], + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + virtualGatewayName: 'virtualGateway', +}); +``` + +Add a virtual gateway directly to the mesh: + +```typescript +const gateway = mesh.addVirtualGateway('gateway', { + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + virtualGatewayName: 'virtualGateway', + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), + }, + })], +}); +``` + +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. + +## 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. + +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 `/`. + +```typescript +gateway.addGatewayRoute('gateway-route-http', { + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + match: { + prefixMatch: '/', }, - ] -); + }), +}); ``` -The number of `route ids` and `route targets` must match as each route needs to have a unique name per router. +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. + +```typescript +gateway.addGatewayRoute('gateway-route-grpc', { + routeSpec: appmesh.GatewayRouteSpec.grpcRouteSpec({ + routeTarget: virtualService, + match: { + serviceName: 'my-service.default.svc.cluster.local', + }, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts new file mode 100644 index 0000000000000..b75890ece1453 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts @@ -0,0 +1,209 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnGatewayRoute } from './appmesh.generated'; +import { Protocol } from './shared-interfaces'; +import { IVirtualService } from './virtual-service'; + +/** + * 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. + */ + readonly prefixPath: string; +} + +/** + * The criterion for determining a request match for this GatewayRoute + */ +export interface GrpcGatewayRouteMatch { + /** + * The fully qualified domain name for the service to match from the request + */ + readonly serviceName: string; +} + +/** + * Properties specific for HTTP Based GatewayRoutes + */ +export interface HttpRouteSpecProps { + /** + * The criterion for determining a request match for this GatewayRoute + * + * @default - matches on '/' + */ + readonly match?: HttpGatewayRouteMatch; + + /** + * The VirtualService this GatewayRoute directs traffic to + */ + readonly routeTarget: IVirtualService; +} + +/** + * Properties specific for a GRPC GatewayRoute + */ +export interface GrpcRouteSpecProps { + /** + * The criterion for determining a request match for this GatewayRoute + */ + readonly match: GrpcGatewayRouteMatch; + + /** + * The VirtualService this GatewayRoute directs traffic to + */ + readonly routeTarget: IVirtualService; +} + +/** + * All Properties for GatewayRoute Specs + */ +export interface GatewayRouteSpecConfig { + /** + * The spec for an http gateway route + * + * @default - no http spec + */ + readonly httpSpecConfig?: CfnGatewayRoute.HttpGatewayRouteProperty; + + /** + * The spec for an http2 gateway route + * + * @default - no http2 spec + */ + readonly http2SpecConfig?: CfnGatewayRoute.HttpGatewayRouteProperty; + + /** + * The spec for a grpc gateway route + * + * @default - no grpc spec + */ + readonly grpcSpecConfig?: CfnGatewayRoute.GrpcGatewayRouteProperty; +} + +/** + * Used to generate specs with different protocols for a GatewayRoute + */ +export abstract class GatewayRouteSpec { + /** + * Creates an HTTP Based GatewayRoute + * + * @param props - no http gateway route + */ + public static httpRouteSpec(props: HttpRouteSpecProps): GatewayRouteSpec { + return new HttpGatewayRouteSpec(props, Protocol.HTTP); + } + + /** + * Creates an HTTP2 Based GatewayRoute + * + * @param props - no http2 gateway route + */ + public static http2RouteSpec(props: HttpRouteSpecProps): GatewayRouteSpec { + return new HttpGatewayRouteSpec(props, Protocol.HTTP2); + } + + /** + * Creates an GRPC Based GatewayRoute + * + * @param props - no grpc gateway route + */ + public static grpcRouteSpec(props: GrpcRouteSpecProps): GatewayRouteSpec { + return new GrpcGatewayRouteSpec(props); + } + + /** + * Called when the GatewayRouteSpec type is initialized. Can be used to enforce + * mutual exclusivity with future properties + */ + public abstract bind(scope: cdk.Construct): GatewayRouteSpecConfig; +} + +class HttpGatewayRouteSpec extends GatewayRouteSpec { + /** + * The criterion for determining a request match for this GatewayRoute. + * + * @default - matches on '/' + */ + readonly match?: HttpGatewayRouteMatch; + + /** + * The VirtualService this GatewayRoute directs traffic to + */ + readonly routeTarget: IVirtualService; + + /** + * Type of route you are creating + */ + readonly routeType: Protocol; + + constructor(props: HttpRouteSpecProps, protocol: Protocol.HTTP | Protocol.HTTP2) { + super(); + this.routeTarget = props.routeTarget; + this.routeType = protocol; + this.match = props.match; + } + + public bind(_scope: cdk.Construct): GatewayRouteSpecConfig { + const prefixPath = this.match ? this.match.prefixPath : '/'; + if (prefixPath[0] != '/') { + throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); + } + const httpConfig: CfnGatewayRoute.HttpGatewayRouteProperty = { + match: { + prefix: prefixPath, + }, + action: { + target: { + virtualService: { + virtualServiceName: this.routeTarget.virtualServiceName, + }, + }, + }, + }; + return { + httpSpecConfig: this.routeType === Protocol.HTTP ? httpConfig : undefined, + http2SpecConfig: this.routeType === Protocol.HTTP2 ? httpConfig : undefined, + }; + } +} + +class GrpcGatewayRouteSpec extends GatewayRouteSpec { + /** + * The criterion for determining a request match for this GatewayRoute. + * + * @default - no default + */ + readonly match: GrpcGatewayRouteMatch; + + /** + * The VirtualService this GatewayRoute directs traffic to + */ + readonly routeTarget: IVirtualService; + + constructor(props: GrpcRouteSpecProps) { + super(); + this.match = props.match; + this.routeTarget = props.routeTarget; + } + + public bind(_scope: cdk.Construct): GatewayRouteSpecConfig { + return { + grpcSpecConfig: { + action: { + target: { + virtualService: { + virtualServiceName: this.routeTarget.virtualServiceName, + }, + }, + }, + match: { + serviceName: this.match.serviceName, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts b/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts new file mode 100644 index 0000000000000..cc00c0f632ac3 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts @@ -0,0 +1,168 @@ +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnGatewayRoute } from './appmesh.generated'; +import { GatewayRouteSpec } from './gateway-route-spec'; +import { IVirtualGateway, VirtualGateway } from './virtual-gateway'; + +/** + * Interface for which all GatewayRoute based classes MUST implement + */ +export interface IGatewayRoute extends cdk.IResource { + /** + * The name of the GatewayRoute + * + * @attribute + */ + readonly gatewayRouteName: string, + + /** + * The Amazon Resource Name (ARN) for the GatewayRoute + * + * @attribute + */ + readonly gatewayRouteArn: string; + + /** + * The VirtualGateway the GatewayRoute belongs to + */ + readonly virtualGateway: IVirtualGateway; +} + +/** + * Basic configuration properties for a GatewayRoute + */ +export interface GatewayRouteBaseProps { + /** + * The name of the GatewayRoute + * + * @default - an automatically generated name + */ + readonly gatewayRouteName?: string; + + /** + * What protocol the route uses + */ + readonly routeSpec: GatewayRouteSpec; +} + +/** + * Properties to define a new GatewayRoute + */ +export interface GatewayRouteProps extends GatewayRouteBaseProps { + /** + * The VirtualGateway this GatewayRoute is associated with + */ + readonly virtualGateway: IVirtualGateway; +} + +/** + * GatewayRoute represents a new or existing gateway route attached to a VirtualGateway and Mesh + * + * @see https://docs.aws.amazon.com/app-mesh/latest/userguide/gateway-routes.html + */ +export class GatewayRoute extends cdk.Resource implements IGatewayRoute { + /** + * Import an existing GatewayRoute given an ARN + */ + public static fromGatewayRouteArn(scope: Construct, id: string, gatewayRouteArn: string): IGatewayRoute { + return new ImportedGatewayRoute(scope, id, { gatewayRouteArn }); + } + + /** + * The name of the GatewayRoute + */ + public readonly gatewayRouteName: string; + + /** + * The Amazon Resource Name (ARN) for the GatewayRoute + */ + public readonly gatewayRouteArn: string; + + /** + * The VirtualGateway this GatewayRoute is a part of + */ + public readonly virtualGateway: IVirtualGateway; + + constructor(scope: Construct, id: string, props: GatewayRouteProps) { + super(scope, id, { + physicalName: props.gatewayRouteName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + }); + + this.virtualGateway = props.virtualGateway; + const routeSpecConfig = props.routeSpec.bind(this); + + const gatewayRoute = new CfnGatewayRoute(this, 'Resource', { + gatewayRouteName: this.physicalName, + meshName: props.virtualGateway.mesh.meshName, + spec: { + httpRoute: routeSpecConfig.httpSpecConfig, + http2Route: routeSpecConfig.http2SpecConfig, + grpcRoute: routeSpecConfig.grpcSpecConfig, + }, + virtualGatewayName: this.virtualGateway.virtualGatewayName, + }); + + this.gatewayRouteName = this.getResourceNameAttribute(gatewayRoute.attrGatewayRouteName); + this.gatewayRouteArn = this.getResourceArnAttribute(gatewayRoute.ref, { + service: 'appmesh', + resource: `mesh/${props.virtualGateway.mesh.meshName}/virtualRouter/${this.virtualGateway.virtualGatewayName}/gatewayRoute`, + resourceName: this.physicalName, + }); + } +} + +/** + * Interface with properties necessary to import a reusable GatewayRoute + */ +interface GatewayRouteAttributes { + /** + * The name of the GatewayRoute + */ + readonly gatewayRouteName?: string; + + /** + * The Amazon Resource Name (ARN) for the GatewayRoute + */ + readonly gatewayRouteArn?: string; + + /** + * The name of the mesh this GatewayRoute is associated with + */ + readonly meshName?: string; + + /** + * The name of the Virtual Gateway this GatewayRoute is associated with + */ + readonly virtualGateway?: IVirtualGateway; +} + +/** + * Represents an imported IGatewayRoute + */ +class ImportedGatewayRoute extends cdk.Resource implements IGatewayRoute { + /** + * The name of the GatewayRoute + */ + public gatewayRouteName: string; + + /** + * The Amazon Resource Name (ARN) for the GatewayRoute + */ + public gatewayRouteArn: string; + + /** + * The VirtualGateway the GatewayRoute belongs to + */ + public virtualGateway: IVirtualGateway; + + constructor(scope: Construct, id: string, props: GatewayRouteAttributes) { + super(scope, id); + if (props.gatewayRouteArn) { + this.gatewayRouteArn = props.gatewayRouteArn; + this.gatewayRouteName = cdk.Fn.select(4, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.gatewayRouteArn).resourceName!)); + this.virtualGateway = VirtualGateway.fromVirtualGatewayArn(this, 'virtualGateway', props.gatewayRouteArn); + } else { + throw new Error('Need gatewayRouteArn'); + } + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index ff36676e2ec1d..69cdfd8d2e3c6 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -6,3 +6,7 @@ export * from './shared-interfaces'; export * from './virtual-node'; export * from './virtual-router'; export * from './virtual-service'; +export * from './virtual-gateway'; +export * from './virtual-gateway-listener'; +export * from './gateway-route'; +export * from './gateway-route-spec'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts index 2aaded50f99bc..961128151b537 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts @@ -1,6 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnMesh } from './appmesh.generated'; +import { VirtualGateway, VirtualGatewayBaseProps } from './virtual-gateway'; import { VirtualNode, VirtualNodeBaseProps } from './virtual-node'; import { VirtualRouter, VirtualRouterBaseProps } from './virtual-router'; import { VirtualService, VirtualServiceBaseProps } from './virtual-service'; @@ -54,6 +55,11 @@ export interface IMesh extends cdk.IResource { * Adds a VirtualNode to the Mesh */ addVirtualNode(id: string, props?: VirtualNodeBaseProps): VirtualNode; + + /** + * Adds a VirtualGateway to the Mesh + */ + addVirtualGateway(id: string, props?: VirtualGatewayBaseProps): VirtualGateway; } /** @@ -99,6 +105,16 @@ abstract class MeshBase extends cdk.Resource implements IMesh { mesh: this, }); } + + /** + * Adds a VirtualGateway to the Mesh + */ + addVirtualGateway(id: string, props?: VirtualGatewayBaseProps): VirtualGateway { + return new VirtualGateway(this, id, { + ...props, + mesh: this, + }); + } } /** diff --git a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts new file mode 100644 index 0000000000000..0f63fdaf05253 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -0,0 +1,41 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnVirtualGateway, CfnVirtualNode } from '../appmesh.generated'; + +type AppMeshHealthCheck = CfnVirtualNode.HealthCheckProperty | CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty + +/** + * Validates health check properties, throws an error if they are misconfigured. + * + * @param healthCheck Healthcheck property from a Virtual Node or Virtual Gateway + */ +export function validateHealthChecks(healthCheck: AppMeshHealthCheck) { + (Object.keys(healthCheck) as Array) + .filter((key) => + HEALTH_CHECK_PROPERTY_THRESHOLDS[key] && + typeof healthCheck[key] === 'number' && + !cdk.Token.isUnresolved(healthCheck[key]), + ).map((key) => { + const [min, max] = HEALTH_CHECK_PROPERTY_THRESHOLDS[key]!; + const value = healthCheck[key]!; + + if (value < min) { + throw new Error(`The value of '${key}' is below the minimum threshold (expected >=${min}, got ${value})`); + } + if (value > max) { + throw new Error(`The value of '${key}' is above the maximum threshold (expected <=${max}, got ${value})`); + } + }); +} + +/** + * Minimum and maximum thresholds for HeathCheck numeric properties + * + * @see https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HealthCheckPolicy.html + */ +const HEALTH_CHECK_PROPERTY_THRESHOLDS: {[key in (keyof AppMeshHealthCheck)]?: [number, number]} = { + healthyThreshold: [2, 10], + intervalMillis: [5000, 300000], + port: [1, 65535], + timeoutMillis: [2000, 60000], + unhealthyThreshold: [2, 10], +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index 7cd55df23565d..39111389b0a03 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -1,5 +1,5 @@ import * as cdk from '@aws-cdk/core'; -import { CfnVirtualNode } from './appmesh.generated'; +import { CfnVirtualGateway, CfnVirtualNode } from './appmesh.generated'; /** * Enum of supported AppMesh protocols @@ -23,24 +23,28 @@ export interface HealthCheck { * @default 2 */ readonly healthyThreshold?: number; + /** * Interval in milliseconds to re-check * * @default 5 seconds */ readonly interval?: cdk.Duration; + /** * The path where the application expects any health-checks, this can also be the application path. * * @default / */ readonly path?: string; + /** * The TCP port number for the healthcheck * * @default - same as corresponding port mapping */ readonly port?: number; + /** * The protocol to use for the healthcheck, for convinience a const enum has been defined. * Protocol.HTTP or Protocol.TCP @@ -48,12 +52,14 @@ export interface HealthCheck { * @default - same as corresponding port mapping */ readonly protocol?: Protocol; + /** * Timeout in milli-seconds for the healthcheck to be considered a fail. * * @default 2 seconds */ readonly timeout?: cdk.Duration; + /** * Number of failed attempts before considering the node DOWN. * @@ -82,7 +88,7 @@ export interface PortMapping { } /** - * Represents the properties needed to define healthy and active listeners for nodes. + * Represents the properties needed to define healthy and active listeners for nodes */ export interface VirtualNodeListener { /** @@ -111,6 +117,13 @@ export interface AccessLogConfig { * @default - no access logging */ readonly virtualNodeAccessLog?: CfnVirtualNode.AccessLogProperty; + + /** + * VirtualGateway CFN configuration for Access Logging + * + * @default - no access logging + */ + readonly virtualGatewayAccessLog?: CfnVirtualGateway.VirtualGatewayAccessLogProperty; } /** @@ -156,6 +169,11 @@ class FileAccessLog extends AccessLog { path: this.filePath, }, }, + virtualGatewayAccessLog: { + file: { + path: this.filePath, + }, + }, }; } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts new file mode 100644 index 0000000000000..a690c022f8c32 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts @@ -0,0 +1,227 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnVirtualGateway } from './appmesh.generated'; +import { validateHealthChecks } from './private/utils'; +import { HealthCheck, Protocol } from './shared-interfaces'; + +/** + * Represents the properties needed to define HTTP Listeners for a VirtualGateway + */ +export interface HttpGatewayListenerProps { + /** + * Port to listen for connections on + * + * @default - 8080 + */ + readonly port?: number + + /** + * The health check information for the listener + * + * @default - no healthcheck + */ + readonly healthCheck?: HealthCheck; +} + +/** + * Represents the properties needed to define GRPC Listeners for a VirtualGateway + */ +export interface GrpcGatewayListenerProps { + /** + * Port to listen for connections on + * + * @default - 8080 + */ + readonly port?: number + + /** + * The health check information for the listener + * + * @default - no healthcheck + */ + readonly healthCheck?: HealthCheck; +} + +/** + * Properties for a VirtualGateway listener + */ +export interface VirtualGatewayListenerConfig { + /** + * Single listener config for a VirtualGateway + */ + readonly listener: CfnVirtualGateway.VirtualGatewayListenerProperty, +} + +/** + * Represents the properties needed to define listeners for a VirtualGateway + */ +export abstract class VirtualGatewayListener { + /** + * Returns an HTTP Listener for a VirtualGateway + */ + public static httpGatewayListener(props: HttpGatewayListenerProps = {}): VirtualGatewayListener { + return new HttpGatewayListener(props); + } + + /** + * Returns an HTTP2 Listener for a VirtualGateway + */ + public static http2GatewayListener(props: HttpGatewayListenerProps = {}): VirtualGatewayListener { + return new Http2GatewayListener(props); + } + + /** + * Returns a GRPC Listener for a VirtualGateway + */ + public static grpcGatewayListener(props: GrpcGatewayListenerProps = {}): VirtualGatewayListener { + return new GrpcGatewayListener(props); + } + + /** + * Protocol the listener implements + */ + protected abstract protocol: Protocol; + + /** + * Port to listen for connections on + */ + protected abstract port: number; + + /** + * Health checking strategy upstream nodes should use when communicating with the listener + */ + protected abstract healthCheck?: HealthCheck; + + /** + * Called when the GatewayListener type is initialized. Can be used to enforce + * mutual exclusivity + */ + public abstract bind(scope: cdk.Construct): VirtualGatewayListenerConfig; + + protected renderHealthCheck(hc: HealthCheck): CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty | undefined { + if (hc.protocol === Protocol.TCP) { + throw new Error('TCP health checks are not permitted for gateway listeners'); + } + + if (hc.protocol === Protocol.GRPC && hc.path) { + throw new Error('The path property cannot be set with Protocol.GRPC'); + } + + const protocol = hc.protocol? hc.protocol : this.protocol; + + const healthCheck: CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty = { + healthyThreshold: hc.healthyThreshold || 2, + intervalMillis: (hc.interval || cdk.Duration.seconds(5)).toMilliseconds(), // min + path: hc.path || ((protocol === Protocol.HTTP || protocol === Protocol.HTTP2) ? '/' : undefined), + port: hc.port || this.port, + protocol: hc.protocol || this.protocol, + timeoutMillis: (hc.timeout || cdk.Duration.seconds(2)).toMilliseconds(), + unhealthyThreshold: hc.unhealthyThreshold || 2, + }; + + validateHealthChecks(healthCheck); + + return healthCheck; + } +} + +/** + * Represents the properties needed to define an HTTP Listener for a VirtualGateway + */ +class HttpGatewayListener extends VirtualGatewayListener { + /** + * Port to listen for connections on + * + * @default - 8080 + */ + readonly port: number; + + /** + * Health checking strategy upstream nodes should use when communicating with the listener + * + * @default - no healthcheck + */ + readonly healthCheck?: HealthCheck; + + /** + * Protocol the listener implements + */ + protected protocol: Protocol = Protocol.HTTP; + + constructor(props: HttpGatewayListenerProps = {}) { + super(); + this.port = props.port ? props.port : 8080; + this.healthCheck = props.healthCheck; + } + + /** + * Called when the GatewayListener type is initialized. Can be used to enforce + * mutual exclusivity + */ + public bind(_scope: cdk.Construct): VirtualGatewayListenerConfig { + return { + listener: { + portMapping: { + port: this.port, + protocol: this.protocol, + }, + healthCheck: this.healthCheck ? this.renderHealthCheck(this.healthCheck): undefined, + }, + }; + } +} + +/** +* Represents the properties needed to define an HTTP2 Listener for a VirtualGateway +*/ +class Http2GatewayListener extends HttpGatewayListener { + constructor(props: HttpGatewayListenerProps = {}) { + super(props); + this.protocol = Protocol.HTTP2; + } +} + +/** + * Represents the properties needed to define a GRPC Listener for Virtual Gateway + */ +class GrpcGatewayListener extends VirtualGatewayListener { + /** + * Port to listen for connections on + * + * @default - 8080 + */ + readonly port: number; + + /** + * Health checking strategy upstream nodes should use when communicating with the listener + * + * @default - no healthcheck + */ + readonly healthCheck?: HealthCheck; + + /** + * Protocol the listener implements + */ + protected protocol: Protocol = Protocol.GRPC; + + constructor(props: HttpGatewayListenerProps = {}) { + super(); + this.port = props.port ? props.port : 8080; + this.healthCheck = props.healthCheck; + } + + /** + * Called when the GatewayListener type is initialized. Can be used to enforce + * mutual exclusivity + */ + public bind(_scope: cdk.Construct): VirtualGatewayListenerConfig { + return { + listener: { + portMapping: { + port: this.port, + protocol: Protocol.GRPC, + }, + healthCheck: this.healthCheck? this.renderHealthCheck(this.healthCheck): undefined, + }, + }; + } +} \ 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 new file mode 100644 index 0000000000000..51950f82dbb13 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts @@ -0,0 +1,226 @@ +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnVirtualGateway } from './appmesh.generated'; +import { GatewayRoute, GatewayRouteBaseProps } from './gateway-route'; + +import { IMesh, Mesh } from './mesh'; +import { AccessLog } from './shared-interfaces'; +import { VirtualGatewayListener, VirtualGatewayListenerConfig } from './virtual-gateway-listener'; + +/** + * Interface which all Virtual Gateway based classes must implement + */ +export interface IVirtualGateway extends cdk.IResource { + /** + * Name of the VirtualGateway + * + * @attribute + */ + readonly virtualGatewayName: string; + + /** + * The Amazon Resource Name (ARN) for the VirtualGateway + * + * @attribute + */ + readonly virtualGatewayArn: string; + + /** + * The mesh which the VirtualGateway belongs to + */ + readonly mesh: IMesh; + + /** + * Utility method to add a new GatewayRoute to the VirtualGateway + */ + addGatewayRoute(id: string, route: GatewayRouteBaseProps): GatewayRoute; +} + +/** + * Basic configuration properties for a VirtualGateway + */ +export interface VirtualGatewayBaseProps { + /** + * Name of the VirtualGateway + * + * @default - A name is automatically determined + */ + readonly virtualGatewayName?: string; + + /** + * Listeners for the VirtualGateway. Only one is supported. + * + * @default - Single HTTP listener on port 8080 + */ + readonly listeners?: VirtualGatewayListener[]; + + /** + * Access Logging Configuration for the VirtualGateway + * + * @default - no access logging + */ + readonly accessLog?: AccessLog; +} + +/** + * Properties used when creating a new VirtualGateway + */ +export interface VirtualGatewayProps extends VirtualGatewayBaseProps { + /** + * The mesh which the VirtualGateway belongs to + */ + readonly mesh: IMesh; +} + +abstract class VirtualGatewayBase extends cdk.Resource implements IVirtualGateway { + /** + * Name of the VirtualGateway + */ + public abstract readonly virtualGatewayName: string; + + /** + * The Amazon Resource Name (ARN) for the VirtualGateway + */ + public abstract readonly virtualGatewayArn: string; + + /** + * The name of the mesh which the VirtualGateway belongs to + */ + public abstract readonly mesh: IMesh; + + /** + * Utility method to add a new GatewayRoute to the VirtualGateway + */ + public addGatewayRoute(id: string, props: GatewayRouteBaseProps): GatewayRoute { + return new GatewayRoute(this, id, { + ...props, + virtualGateway: this, + }); + } +} + +/** + * VirtualGateway represents a newly defined App Mesh Virtual Gateway + * + * A virtual gateway allows resources that are outside of your mesh to communicate to resources that + * are inside of your mesh. + * + * @see https://docs.aws.amazon.com/app-mesh/latest/userguide/virtual_gateways.html + */ +export class VirtualGateway extends VirtualGatewayBase { + /** + * Import an existing VirtualGateway given an ARN + */ + public static fromVirtualGatewayArn(scope: Construct, id: string, virtualGatewayArn: string): IVirtualGateway { + return new ImportedVirtualGateway(scope, id, { virtualGatewayArn }); + } + + /** + * The name of the VirtualGateway + */ + public readonly virtualGatewayName: string; + + /** + * The Amazon Resource Name (ARN) for the VirtualGateway + */ + public readonly virtualGatewayArn: string; + + /** + * The Mesh that the VirtualGateway belongs to + */ + public readonly mesh: IMesh; + + protected readonly listeners = new Array(); + + constructor(scope: Construct, id: string, props: VirtualGatewayProps) { + super(scope, id, { + physicalName: props.virtualGatewayName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + }); + + this.mesh = props.mesh; + + if (!props.listeners) { + // Use listener default of http listener port 8080 if no listener is defined + this.listeners.push(VirtualGatewayListener.httpGatewayListener().bind(this)); + } else { + props.listeners.forEach(listener => this.listeners.push(listener.bind(this))); + } + + const accessLogging = props.accessLog?.bind(this); + + const node = new CfnVirtualGateway(this, 'Resource', { + virtualGatewayName: this.physicalName, + meshName: this.mesh.meshName, + spec: { + listeners: this.listeners.map(listener => listener.listener), + logging: accessLogging !== undefined ? { + accessLog: accessLogging.virtualGatewayAccessLog, + } : undefined, + }, + }); + + this.virtualGatewayName = this.getResourceNameAttribute(node.attrVirtualGatewayName); + this.virtualGatewayArn = this.getResourceArnAttribute(node.ref, { + service: 'appmesh', + resource: `mesh/${props.mesh.meshName}/virtualGateway`, + resourceName: this.physicalName, + }); + } +} + +/** + * Unterface with properties necessary to import a reusable VirtualGateway + */ +interface VirtualGatewayAttributes { + /** + * The name of the VirtualGateway + */ + readonly virtualGatewayName?: string; + + /** + * The Amazon Resource Name (ARN) belonging to the VirtualGateway + */ + readonly virtualGatewayArn?: string; + + /** + * The Mesh that the VirtualGateway belongs to + */ + readonly mesh?: IMesh; + + /** + * The name of the mesh that the VirtualGateway belongs to + */ + readonly meshName?: string; +} + +/** + * Used to import a VirtualGateway and read its properties + */ +class ImportedVirtualGateway extends VirtualGatewayBase { + /** + * The name of the VirtualGateway + */ + public readonly virtualGatewayName: string; + + /** + * The Amazon Resource Name (ARN) belonging to the VirtualGateway + */ + public readonly virtualGatewayArn: string; + + /** + * The Mesh that the VirtualGateway belongs to + */ + public readonly mesh: IMesh; + + constructor(scope: Construct, id: string, props: VirtualGatewayAttributes) { + super(scope, id); + if (props.virtualGatewayArn) { + const meshName = cdk.Fn.select(0, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.virtualGatewayArn).resourceName!)); + this.mesh = Mesh.fromMeshName(this, 'Mesh', meshName); + this.virtualGatewayArn = props.virtualGatewayArn; + this.virtualGatewayName = cdk.Fn.select(2, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.virtualGatewayArn).resourceName!)); + } else { + throw new Error('Need virtualGatewayArn'); + } + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts index 233ad2d0f216c..bfad388fa91ee 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts @@ -3,6 +3,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualNode } from './appmesh.generated'; import { IMesh } from './mesh'; +import { validateHealthChecks } from './private/utils'; import { AccessLog, HealthCheck, PortMapping, Protocol, VirtualNodeListener } from './shared-interfaces'; import { IVirtualService } from './virtual-service'; @@ -154,19 +155,6 @@ abstract class VirtualNodeBase extends cdk.Resource implements IVirtualNode { } } -/** - * Minimum and maximum thresholds for HeathCheck numeric properties - * - * @see https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HealthCheckPolicy.html - */ -const HEALTH_CHECK_PROPERTY_THRESHOLDS: {[key in (keyof CfnVirtualNode.HealthCheckProperty)]?: [number, number]} = { - healthyThreshold: [2, 10], - intervalMillis: [5000, 300000], - port: [1, 65535], - timeoutMillis: [2000, 60000], - unhealthyThreshold: [2, 10], -}; - function renderHealthCheck(hc: HealthCheck | undefined, pm: PortMapping): CfnVirtualNode.HealthCheckProperty | undefined { if (hc === undefined) { return undefined; } @@ -178,38 +166,25 @@ function renderHealthCheck(hc: HealthCheck | undefined, pm: PortMapping): CfnVir throw new Error('The path property cannot be set with Protocol.GRPC'); } + const protocol = hc.protocol ?? pm.protocol; + const healthCheck: CfnVirtualNode.HealthCheckProperty = { healthyThreshold: hc.healthyThreshold || 2, intervalMillis: (hc.interval || cdk.Duration.seconds(5)).toMilliseconds(), // min - path: hc.path || (hc.protocol === Protocol.HTTP ? '/' : undefined), + path: hc.path || (protocol === Protocol.HTTP ? '/' : undefined), port: hc.port || pm.port, protocol: hc.protocol || pm.protocol, timeoutMillis: (hc.timeout || cdk.Duration.seconds(2)).toMilliseconds(), unhealthyThreshold: hc.unhealthyThreshold || 2, }; - (Object.keys(healthCheck) as Array) - .filter((key) => - HEALTH_CHECK_PROPERTY_THRESHOLDS[key] && - typeof healthCheck[key] === 'number' && - !cdk.Token.isUnresolved(healthCheck[key]), - ).map((key) => { - const [min, max] = HEALTH_CHECK_PROPERTY_THRESHOLDS[key]!; - const value = healthCheck[key]!; - - if (value < min) { - throw new Error(`The value of '${key}' is below the minimum threshold (expected >=${min}, got ${value})`); - } - if (value > max) { - throw new Error(`The value of '${key}' is above the maximum threshold (expected <=${max}, got ${value})`); - } - }); + validateHealthChecks(healthCheck); return healthCheck; } /** - * VirtualNode represents a newly defined AppMesh VirtualNode + * VirtualNode represents a newly defined App Mesh VirtualNode * * Any inbound traffic that your virtual node expects should be specified as a * listener. Any outbound traffic that your virtual node expects to reach @@ -241,7 +216,7 @@ export class VirtualNode extends VirtualNodeBase { public readonly virtualNodeName: string; /** - * The Amazon Resource Name belonging to the VirtualNdoe + * The Amazon Resource Name belonging to the VirtualNode */ public readonly virtualNodeArn: string; @@ -316,7 +291,7 @@ interface VirtualNodeAttributes { } /** - * Used to import a VirtualNode and read it's properties + * Used to import a VirtualNode and read its properties */ class ImportedVirtualNode extends VirtualNodeBase { /** diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index e3c345d954997..2789d9085284c 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -112,6 +112,29 @@ "from-signature:@aws-cdk/aws-appmesh.VirtualNode.fromVirtualNodeName", "from-signature:@aws-cdk/aws-appmesh.VirtualRouter.fromVirtualRouterName", "from-signature:@aws-cdk/aws-appmesh.VirtualService.fromVirtualServiceName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteVirtualGatewayName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteVirtualGatewayName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteVirtualGatewayName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteVirtualGatewayName", "resource-attribute:@aws-cdk/aws-appmesh.IMesh.meshUid", "resource-attribute:@aws-cdk/aws-appmesh.IRoute.routeUid", "resource-attribute:@aws-cdk/aws-appmesh.IVirtualNode.virtualNodeUid", @@ -125,6 +148,10 @@ "resource-attribute:@aws-cdk/aws-appmesh.Route.routeResourceOwner", "resource-attribute:@aws-cdk/aws-appmesh.Route.routeUid", "resource-attribute:@aws-cdk/aws-appmesh.Route.routeVirtualRouterName", + "resource-attribute:@aws-cdk/aws-appmesh.VirtualGateway.virtualGatewayMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.VirtualGateway.virtualGatewayMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.VirtualGateway.virtualGatewayResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.VirtualGateway.virtualGatewayUid", "resource-attribute:@aws-cdk/aws-appmesh.VirtualNode.virtualNodeMeshName", "resource-attribute:@aws-cdk/aws-appmesh.VirtualNode.virtualNodeMeshOwner", "resource-attribute:@aws-cdk/aws-appmesh.VirtualNode.virtualNodeResourceOwner", 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 5b1b1657f0e20..e74a824311783 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -772,6 +772,151 @@ "VirtualNodeName": "meshstackmeshnode3C5835BCB" } }, + "meshgateway1B02387E8": { + "Type": "AWS::AppMesh::VirtualGateway", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Listeners": [ + { + "PortMapping": { + "Port": 8080, + "Protocol": "http" + } + } + ], + "Logging": { + "AccessLog": { + "File": { + "Path": "/dev/stdout" + } + } + } + }, + "VirtualGatewayName": "gateway1" + } + }, + "meshgateway1gateway1routehttpE8D6F433": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttpBA921D42", + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "HttpRoute": { + "Action": { + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "meshserviceE06ECED5", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Prefix": "/" + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + } + } + }, + "meshgateway1gateway1routehttp2FD69C306": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp255781963", + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "meshserviceE06ECED5", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Prefix": "/" + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + } + } + }, + "meshgateway1gateway1routegrpc76486062": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpcCD4D891D", + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "GrpcRoute": { + "Action": { + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "meshserviceE06ECED5", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "ServiceName": { + "Fn::GetAtt": [ + "meshserviceE06ECED5", + "VirtualServiceName" + ] + } + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + } + } + }, "service27C65CF7D": { "Type": "AWS::AppMesh::VirtualService", "Properties": { @@ -797,6 +942,37 @@ "Spec": {}, "VirtualServiceName": "service3.domain.local" } + }, + "gateway2BCE5C5E0": { + "Type": "AWS::AppMesh::VirtualGateway", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Listeners": [ + { + "HealthCheck": { + "HealthyThreshold": 2, + "IntervalMillis": 10000, + "Path": "/", + "Port": 443, + "Protocol": "http", + "TimeoutMillis": 2000, + "UnhealthyThreshold": 2 + }, + "PortMapping": { + "Port": 443, + "Protocol": "http" + } + } + ] + }, + "VirtualGatewayName": "meshstackgateway2BEC62D7C" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index df55025f1365c..322ae1cc608d6 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -116,3 +116,39 @@ router.addRoute('route-3', { }, ], }); + +const gateway = mesh.addVirtualGateway('gateway1', { + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + virtualGatewayName: 'gateway1', +}); + +new appmesh.VirtualGateway(stack, 'gateway2', { + mesh: mesh, + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), + }, + })], +}); + +gateway.addGatewayRoute('gateway1-route-http', { + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), +}); + +gateway.addGatewayRoute('gateway1-route-http2', { + routeSpec: appmesh.GatewayRouteSpec.http2RouteSpec({ + routeTarget: virtualService, + }), +}); + +gateway.addGatewayRoute('gateway1-route-grpc', { + routeSpec: appmesh.GatewayRouteSpec.grpcRouteSpec({ + routeTarget: virtualService, + match: { + serviceName: virtualService.virtualServiceName, + }, + }), +}); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts new file mode 100644 index 0000000000000..842e553dfa20c --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts @@ -0,0 +1,152 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; + +import * as appmesh from '../lib'; + +export = { + 'When creating a GatewayRoute': { + 'should have expected defaults'(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.httpGatewayListener()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + mesh: mesh, + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), + gatewayRouteName: 'gateway-http-route', + }); + + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2RouteSpec({ + routeTarget: virtualService, + }), + gatewayRouteName: 'gateway-http2-route', + }); + + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpcRouteSpec({ + routeTarget: virtualService, + match: { + serviceName: virtualService.virtualServiceName, + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Action: { + Target: { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['vs1732C2645', 'VirtualServiceName'], + }, + }, + }, + }, + Match: { + Prefix: '/', + }, + }, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route', + Spec: { + Http2Route: { + Action: { + Target: { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['vs1732C2645', 'VirtualServiceName'], + }, + }, + }, + }, + Match: { + Prefix: '/', + }, + }, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-grpc-route', + Spec: { + GrpcRoute: { + Action: { + Target: { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['vs1732C2645', 'VirtualServiceName'], + }, + }, + }, + }, + Match: { + ServiceName: { + 'Fn::GetAtt': ['vs1732C2645', 'VirtualServiceName'], + }, + }, + }, + }, + })); + test.done(); + }, + + 'should throw an exception if you start an http prefix match not with a /'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualService = mesh.addVirtualService('testVirtualService'); + test.throws(() => appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + match: { + prefixPath: 'wrong', + }, + }).bind(stack), + /Prefix Path must start with \'\/\', got: wrong/); + test.done(); + }, + }, + + 'Can export and import GatewayRoutes and perform actions'(test: Test) { + const app = new cdk.App(); + // GIVEN + const stack = new cdk.Stack(app, 'Imports', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + // WHEN + const gatewayRoute2 = appmesh.GatewayRoute.fromGatewayRouteArn( + stack, 'importedGatewayRoute2', 'arn:aws:appmesh:us-east-1:123456789012:mesh/test-mesh/virtualGateway/test-gateway/gatewayRoute/test-gateway-route'); + // THEN + test.equal(gatewayRoute2.gatewayRouteName, 'test-gateway-route'); + test.equal(gatewayRoute2.virtualGateway.virtualGatewayName, 'test-gateway'); + test.equal(gatewayRoute2.virtualGateway.mesh.meshName, 'test-mesh'); + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts new file mode 100644 index 0000000000000..1dd75b938ab04 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -0,0 +1,258 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; + +import * as appmesh from '../lib'; + +export = { + 'When creating a VirtualGateway': { + 'should have expected defaults'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + new appmesh.VirtualGateway(stack, 'testGateway', { + mesh: mesh, + }); + + new appmesh.VirtualGateway(stack, 'httpGateway', { + mesh: mesh, + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), + }, + })], + }); + + new appmesh.VirtualGateway(stack, 'http2Gateway', { + mesh: mesh, + listeners: [appmesh.VirtualGatewayListener.http2GatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), + }, + })], + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + PortMapping: { + Port: 8080, + Protocol: appmesh.Protocol.HTTP, + }, + }, + ], + }, + VirtualGatewayName: 'testGateway', + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + HealthCheck: { + HealthyThreshold: 2, + IntervalMillis: 10000, + Port: 443, + Protocol: appmesh.Protocol.HTTP, + TimeoutMillis: 2000, + UnhealthyThreshold: 2, + Path: '/', + }, + PortMapping: { + Port: 443, + Protocol: appmesh.Protocol.HTTP, + }, + }, + ], + }, + VirtualGatewayName: 'httpGateway', + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + HealthCheck: { + HealthyThreshold: 2, + IntervalMillis: 10000, + Port: 443, + Protocol: appmesh.Protocol.HTTP2, + TimeoutMillis: 2000, + UnhealthyThreshold: 2, + Path: '/', + }, + PortMapping: { + Port: 443, + Protocol: appmesh.Protocol.HTTP2, + }, + }, + ], + }, + VirtualGatewayName: 'http2Gateway', + })); + test.done(); + }, + + 'should have expected features'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + new appmesh.VirtualGateway(stack, 'testGateway', { + virtualGatewayName: 'test-gateway', + listeners: [appmesh.VirtualGatewayListener.grpcGatewayListener({ + port: 80, + healthCheck: { + }, + })], + mesh: mesh, + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + HealthCheck: { + HealthyThreshold: 2, + IntervalMillis: 5000, + Port: 80, + Protocol: appmesh.Protocol.GRPC, + TimeoutMillis: 2000, + UnhealthyThreshold: 2, + }, + PortMapping: { + Port: 80, + Protocol: appmesh.Protocol.GRPC, + }, + }, + ], + Logging: { + AccessLog: { + File: { + Path: '/dev/stdout', + }, + }, + }, + }, + VirtualGatewayName: 'test-gateway', + })); + test.done(); + }, + }, + + 'When adding a gateway route to existing VirtualGateway ': { + 'should create gateway route resource'(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, 'testGateway', { + virtualGatewayName: 'test-gateway', + mesh: mesh, + }); + + const virtualService = mesh.addVirtualService('virtualService', {}); + + virtualGateway.addGatewayRoute('testGatewayRoute', { + gatewayRouteName: 'test-gateway-route', + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'test-gateway-route', + Spec: { + HttpRoute: { + Action: { + Target: { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['meshvirtualService93460D43', 'VirtualServiceName'], + }, + }, + }, + }, + Match: { + Prefix: '/', + }, + }, + }, + })); + test.done(); + }, + }, + + 'When adding gateway routes to a VirtualGateway with existing gateway routes': { + 'should add gateway routes and not overwite'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualService = mesh.addVirtualService('virtualService', {}); + + const virtualGateway = mesh.addVirtualGateway('gateway'); + virtualGateway.addGatewayRoute('testGatewayRoute', { + gatewayRouteName: 'test-gateway-route', + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), + }); + virtualGateway.addGatewayRoute('testGatewayRoute2', { + gatewayRouteName: 'test-gateway-route-2', + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), + }); + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'test-gateway-route', + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'test-gateway-route-2', + })); + test.done(); + }, + }, + + 'Can export and import VirtualGateway and perform actions'(test: Test) { + const app = new cdk.App(); + // GIVEN + const stack = new cdk.Stack(app, 'Imports', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + // WHEN + const virtualGateway2 = appmesh.VirtualGateway.fromVirtualGatewayArn( + stack, 'importedGateway2', 'arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh/virtualGateway/test-gateway'); + + // THEN + test.equal(virtualGateway2.mesh.meshName, 'testMesh'); + test.equal(virtualGateway2.virtualGatewayName, 'test-gateway'); + test.done(); + }, +}; \ No newline at end of file