diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 74aead1f02a02..63203d6b365d1 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -298,6 +298,30 @@ router.addRoute('route-http', { }); ``` +Add an HTTP2 route that matches based on method, scheme and header: + +```ts +router.addRoute('route-http2', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [ + { + virtualNode: node, + }, + ], + match: { + prefixPath: '/', + method: appmesh.HttpRouteMatchMethod.POST, + 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'), + ] + }, + }), +}); +``` + Add a single route with multiple targets and split traffic 50/50 ```ts diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index 11f629c4aee91..add785c02c286 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -35,6 +35,255 @@ export interface HttpRouteMatch { * and you want the route to match requests to my-service.local/metrics, your prefix should be /metrics. */ readonly prefixPath: string; + + /** + * Specifies the client request headers to match on. All specified headers + * must match for the route to match. + * + * @default - do not match on headers + */ + readonly headers?: HttpHeaderMatch[]; + + /** + * The HTTP client request method to match on. + * + * @default - do not match on request method + */ + readonly method?: HttpRouteMatchMethod; + + /** + * The client request protocol to match on. Applicable only for HTTP2 routes. + * + * @default - do not match on HTTP2 request protocol + */ + 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 + */ +export enum HttpRouteProtocol { + /** + * Match HTTP requests + */ + HTTP = 'http', + + /** + * Match HTTPS requests + */ + 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, + }, + }; + } } /** @@ -47,10 +296,23 @@ export interface GrpcRouteMatch { readonly serviceName: string; } +/** + * Base options for all route specs. + */ +export interface RouteSpecOptionsBase { + /** + * The priority for the route. Routes are matched based on the specified + * value, where 0 is the highest priority. + * + * @default - no particular priority + */ + readonly priority?: number; +} + /** * Properties specific for HTTP Based Routes */ -export interface HttpRouteSpecOptions { +export interface HttpRouteSpecOptions extends RouteSpecOptionsBase { /** * The criterion for determining a request match for this Route * @@ -149,7 +411,7 @@ export enum TcpRetryEvent { /** * Properties specific for a TCP Based Routes */ -export interface TcpRouteSpecOptions { +export interface TcpRouteSpecOptions extends RouteSpecOptionsBase { /** * List of targets that traffic is routed to when a request matches the route */ @@ -166,7 +428,7 @@ export interface TcpRouteSpecOptions { /** * Properties specific for a GRPC Based Routes */ -export interface GrpcRouteSpecOptions { +export interface GrpcRouteSpecOptions extends RouteSpecOptionsBase { /** * The criterion for determining a request match for this Route */ @@ -274,6 +536,14 @@ export interface RouteSpecConfig { * @default - no tcp spec */ readonly tcpRouteSpec?: CfnRoute.TcpRouteProperty; + + /** + * The priority for the route. Routes are matched based on the specified + * value, where 0 is the highest priority. + * + * @default - no particular priority + */ + readonly priority?: number; } /** @@ -317,24 +587,11 @@ export abstract class RouteSpec { } class HttpRouteSpec extends RouteSpec { - /** - * Type of route you are creating - */ + public readonly priority?: number; public readonly protocol: Protocol; - - /** - * The criteria for determining a request match - */ public readonly match?: HttpRouteMatch; - - /** - * The criteria for determining a timeout configuration - */ public readonly timeout?: HttpTimeout; - /** - * List of targets that traffic is routed to when a request matches the route - */ public readonly weightedTargets: WeightedTarget[]; /** @@ -348,6 +605,7 @@ class HttpRouteSpec extends RouteSpec { this.match = props.match; this.weightedTargets = props.weightedTargets; this.timeout = props.timeout; + this.priority = props.priority; if (props.retryPolicy) { const httpRetryEvents = props.retryPolicy.httpRetryEvents ?? []; @@ -365,7 +623,7 @@ class HttpRouteSpec extends RouteSpec { } } - public bind(_scope: Construct): RouteSpecConfig { + 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}`); @@ -377,11 +635,15 @@ class HttpRouteSpec extends RouteSpec { }, match: { prefix: prefixPath, + headers: this.match?.headers?.map(header => header.bind(scope).httpRouteHeader), + method: this.match?.method, + scheme: this.match?.protocol, }, timeout: renderTimeout(this.timeout), retryPolicy: this.retryPolicy ? renderHttpRetryPolicy(this.retryPolicy) : undefined, }; return { + priority: this.priority, httpRouteSpec: this.protocol === Protocol.HTTP ? httpConfig : undefined, http2RouteSpec: this.protocol === Protocol.HTTP2 ? httpConfig : undefined, }; @@ -389,6 +651,11 @@ class HttpRouteSpec extends RouteSpec { } class TcpRouteSpec extends RouteSpec { + /** + * The priority for the route. + */ + public readonly priority?: number; + /* * List of targets that traffic is routed to when a request matches the route */ @@ -403,10 +670,12 @@ class TcpRouteSpec extends RouteSpec { super(); this.weightedTargets = props.weightedTargets; this.timeout = props.timeout; + this.priority = props.priority; } public bind(_scope: Construct): RouteSpecConfig { return { + priority: this.priority, tcpRouteSpec: { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), @@ -418,6 +687,11 @@ class TcpRouteSpec extends RouteSpec { } class GrpcRouteSpec extends RouteSpec { + /** + * The priority for the route. + */ + public readonly priority?: number; + public readonly weightedTargets: WeightedTarget[]; public readonly match: GrpcRouteMatch; public readonly timeout?: GrpcTimeout; @@ -432,6 +706,7 @@ class GrpcRouteSpec extends RouteSpec { this.weightedTargets = props.weightedTargets; this.match = props.match; this.timeout = props.timeout; + this.priority = props.priority; if (props.retryPolicy) { const grpcRetryEvents = props.retryPolicy.grpcRetryEvents ?? []; @@ -453,6 +728,7 @@ class GrpcRouteSpec extends RouteSpec { public bind(_scope: Construct): RouteSpecConfig { return { + priority: this.priority, grpcRouteSpec: { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), diff --git a/packages/@aws-cdk/aws-appmesh/lib/route.ts b/packages/@aws-cdk/aws-appmesh/lib/route.ts index 7b9bd2aeb94d2..7800e3e08e53f 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route.ts @@ -126,6 +126,7 @@ export class Route extends cdk.Resource implements IRoute { httpRoute: spec.httpRouteSpec, http2Route: spec.http2RouteSpec, grpcRoute: spec.grpcRouteSpec, + priority: spec.priority, }, }); 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 f951953924b44..c3139e2b75582 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -625,6 +625,124 @@ "RouteName": "route-3" } }, + "meshrouterroutematchingACC12F04": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode3D2A19CF2", + "VirtualNodeName" + ] + }, + "Weight": 1 + } + ] + }, + "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" + } + ], + "Method": "POST", + "Prefix": "/", + "Scheme": "https" + } + } + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-matching" + } + }, "meshrouterroutehttp2retryCC41345F": { "Type": "AWS::AppMesh::Route", "Properties": { @@ -676,6 +794,45 @@ "RouteName": "route-http2-retry" } }, + "meshrouterroute53F46B0FE": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode2092BA426", + "VirtualNodeName" + ] + }, + "Weight": 1 + } + ] + }, + "Match": { + "Prefix": "/" + } + }, + "Priority": 10 + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-5" + } + }, "meshrouterroutegrpcretry9BEB798A": { "Type": "AWS::AppMesh::Route", "Properties": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index c1e909e38d75b..4b62e8e12ee30 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -140,6 +140,29 @@ router.addRoute('route-3', { }), }); +router.addRoute('route-matching', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode: node3 }], + match: { + prefixPath: '/', + method: appmesh.HttpRouteMatchMethod.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), + ], + }, + }), +}); + router.addRoute('route-http2-retry', { routeSpec: appmesh.RouteSpec.http2({ weightedTargets: [{ virtualNode: node3 }], @@ -152,6 +175,13 @@ router.addRoute('route-http2-retry', { }), }); +router.addRoute('route-5', { + routeSpec: appmesh.RouteSpec.http2({ + priority: 10, + weightedTargets: [{ virtualNode: node2 }], + }), +}); + router.addRoute('route-grpc-retry', { routeSpec: appmesh.RouteSpec.grpc({ weightedTargets: [{ virtualNode: node3 }], diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index 43c2d942a669b..b3c1ae674a6f4 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -576,6 +576,278 @@ export = { }, }, + 'should match routes based on 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 + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + 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), + ], + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Http2Route: { + Match: { + Prefix: '/', + 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(); + }, + + 'should match routes based on method'(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 + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + prefixPath: '/', + method: appmesh.HttpRouteMatchMethod.GET, + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Http2Route: { + Match: { + Prefix: '/', + Method: 'GET', + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based on scheme'(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 + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + prefixPath: '/', + protocol: appmesh.HttpRouteProtocol.HTTP, + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Http2Route: { + Match: { + Prefix: '/', + Scheme: 'http', + }, + }, + }, + })); + + test.done(); + }, + + 'should allow route priority'(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 + router.addRoute('http2', { + routeSpec: appmesh.RouteSpec.http2({ + priority: 0, + weightedTargets: [{ virtualNode }], + }), + }); + router.addRoute('http', { + routeSpec: appmesh.RouteSpec.http({ + priority: 10, + weightedTargets: [{ virtualNode }], + }), + }); + router.addRoute('grpc', { + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + serviceName: 'test', + }, + }), + }); + router.addRoute('tcp', { + routeSpec: appmesh.RouteSpec.tcp({ + priority: 30, + weightedTargets: [{ virtualNode }], + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Priority: 0, + Http2Route: {}, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Priority: 10, + HttpRoute: {}, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Priority: 20, + GrpcRoute: {}, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Priority: 30, + TcpRoute: {}, + }, + })); + + test.done(); + }, + 'Can import Routes using an ARN'(test: Test) { const app = new cdk.App(); // GIVEN