Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(appmesh): change Route's spec to a union-like class #11343

Merged
merged 12 commits into from
Nov 19, 2020
66 changes: 45 additions & 21 deletions packages/@aws-cdk/aws-appmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,39 +195,63 @@ The listeners property can be left blank and added later with the `node.addListe

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.

You can use the prefix parameter in your `route` specification 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.

If your `route` matches a request, you can distribute traffic to one or more target virtual nodes with relative weighting.

```typescript
router.addRoute('route', {
routeTargets: [
{
virtualNode,
weight: 1,
router.addRoute('route-http', {
routeSpec: appmesh.RouteSpec.http({
weightedTargets: [
{
virtualNode: node,
},
],
match: {
prefixPath: '/path-to-app',
},
],
prefix: `/path-to-app`,
routeType: RouteType.HTTP,
}),
});
```

Add a single route with multiple targets and split traffic 50/50

```typescript
router.addRoute('route', {
routeTargets: [
{
virtualNode,
weight: 50,
router.addRoute('route-http', {
routeSpec: appmesh.RouteSpec.http({
weightedTargets: [
{
virtualNode: node,
weight: 50,
},
{
virtualNode: node,
weight: 50,
},
],
match: {
prefixPath: '/path-to-app',
},
{
virtualNode2,
weight: 50,
}),
});
```

The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs.
The `tcp()`, `http()` and `http2()` methods provide the spec necessary to define a protocol specific spec.

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
router.addRoute('route-http', {
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
routeSpec: appmesh.RouteSpec.grpc({
weightedTargets: [
{
virtualNode: node,
},
],
match: {
serviceName: 'my-service.default.svc.cluster.local',
},
],
prefix: `/path-to-app`,
routeType: RouteType.HTTP,
}),
});
```

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-appmesh/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export * from './appmesh.generated';
export * from './mesh';
export * from './route';
export * from './route-spec';
export * from './shared-interfaces';
export * from './virtual-node';
export * from './virtual-router';
Expand Down
269 changes: 269 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/route-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import * as cdk from '@aws-cdk/core';
import { CfnRoute } from './appmesh.generated';
import { Protocol } from './shared-interfaces';
import { IVirtualNode } from './virtual-node';

/**
* Properties for the Weighted Targets in the route
*/
export interface WeightedTarget {
/**
* The VirtualNode the route points to
*/
readonly virtualNode: IVirtualNode;

/**
* The weight for the target
*
* @default 1
*/
readonly weight?: number;
}

/**
* The criterion for determining a request match for this GatewayRoute
*/
export interface HttpRouteMatch {
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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 GrpcRouteMatch {
/**
* The fully qualified domain name for the service to match from the request
*/
readonly serviceName: string;
}

/**
* Properties specific for HTTP Based Routes
*/
export interface HttpRouteSpecOptions {
/**
* The criterion for determining a request match for this Route
*
* @default - matches on '/'
*/
readonly match?: HttpRouteMatch;

/**
* List of targets that traffic is routed to when a request matches the route
*/
readonly weightedTargets: WeightedTarget[];
}

/**
* Properties specific for a TCP Based Routes
*/
export interface TcpRouteSpecOptions {
/**
* List of targets that traffic is routed to when a request matches the route
*/
readonly weightedTargets: WeightedTarget[];
}

/**
* Properties specific for a GRPC Based Routes
*/
export interface GrpcRouteSpecOptions {
/**
* The criterion for determining a request match for this Route
*
* @default - no default
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly match: GrpcRouteMatch;

/**
* List of targets that traffic is routed to when a request matches the route
*/
readonly weightedTargets: WeightedTarget[];
}

/**
* All Properties for GatewayRoute Specs
*/
export interface RouteSpecConfig {
/**
* The spec for an http route
*
* @default - no http spec
*/
readonly httpRouteSpec?: CfnRoute.HttpRouteProperty;

/**
* The spec for an http2 route
*
* @default - no http2 spec
*/
readonly http2RouteSpec?: CfnRoute.HttpRouteProperty;

/**
* The spec for a grpc route
*
* @default - no grpc spec
*/
readonly grpcRouteSpec?: CfnRoute.GrpcRouteProperty;
dfezzie marked this conversation as resolved.
Show resolved Hide resolved

/**
* The spec for a tcp route
*
* @default - no tcp spec
*/
readonly tcpRouteSpec?: CfnRoute.TcpRouteProperty;
}

/**
* Used to generate specs with different protocols for a RouteSpec
*/
export abstract class RouteSpec {
/**
* Creates an HTTP Based RouteSpec
*/
public static http(options: HttpRouteSpecOptions): RouteSpec {
return new HttpRouteSpec(options, Protocol.HTTP);
}

/**
* Creates an HTTP2 Based RouteSpec
*
*/
public static http2(options: HttpRouteSpecOptions): RouteSpec {
return new HttpRouteSpec(options, Protocol.HTTP2);
}

/**
* Creates a TCP Based RouteSpec
*/
public static tcp(options: TcpRouteSpecOptions): RouteSpec {
return new TcpRouteSpec(options);
}

/**
* Creates a GRPC Based RouteSpec
*/
public static grpc(options: GrpcRouteSpecOptions): RouteSpec {
return new GrpcRouteSpec(options);
}

/**
* List of targets that traffic is routed to when a request matches the route
*/
abstract readonly weightedTargets: WeightedTarget[];
dfezzie marked this conversation as resolved.
Show resolved Hide resolved

/**
* Called when the GatewayRouteSpec type is initialized. Can be used to enforce
* mutual exclusivity with future properties
*/
public abstract bind(scope: cdk.Construct): RouteSpecConfig;

/**
* Utility method to add weighted route targets to an existing route
*/
protected renderWeightedTargets() {
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
const renderedTargets: CfnRoute.WeightedTargetProperty[] = [];
for (const t of this.weightedTargets) {
renderedTargets.push({
virtualNode: t.virtualNode.virtualNodeName,
weight: t.weight || 1,
});
}
return renderedTargets;
}
}

class HttpRouteSpec extends RouteSpec {
/**
* Type of route you are creating
*/
public readonly protocol: Protocol;

/**
* The criteria for determining a request match
*/
public readonly match?: HttpRouteMatch;

/**
* List of targets that traffic is routed to when a request matches the route
*/
public readonly weightedTargets: WeightedTarget[];

constructor(props: HttpRouteSpecOptions, protocol: Protocol) {
super();
this.protocol = protocol;
this.match = props.match;
this.weightedTargets = props.weightedTargets;
}

public bind(_scope: cdk.Construct): RouteSpecConfig {
const prefixPath = this.match ? this.match.prefixPath : '/';
if (prefixPath[0] != '/') {
throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`);
}
const httpConfig: CfnRoute.HttpRouteProperty = {
action: {
weightedTargets: this.renderWeightedTargets(),
},
match: {
prefix: prefixPath,
},
};
return {
httpRouteSpec: this.protocol === Protocol.HTTP ? httpConfig : undefined,
http2RouteSpec: this.protocol === Protocol.HTTP2 ? httpConfig : undefined,
};
}
}

class TcpRouteSpec extends RouteSpec {
/*
* List of targets that traffic is routed to when a request matches the route
*/
public readonly weightedTargets: WeightedTarget[];

constructor(props: TcpRouteSpecOptions) {
super();
this.weightedTargets = props.weightedTargets;
}

public bind(_scope: cdk.Construct): RouteSpecConfig {
return {
tcpRouteSpec: {
action: {
weightedTargets: this.renderWeightedTargets(),
},
},
};
}
}

class GrpcRouteSpec extends RouteSpec {
public readonly weightedTargets: WeightedTarget[];
public readonly match: GrpcRouteMatch;

constructor(props: GrpcRouteSpecOptions) {
super();
this.weightedTargets = props.weightedTargets;
this.match = props.match;
}

public bind(_scope: cdk.Construct): RouteSpecConfig {
return {
grpcRouteSpec: {
action: {
weightedTargets: this.renderWeightedTargets(),
},
match: {
serviceName: this.match.serviceName,
},
},
};
}
}
Loading