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.http({
weightedTargets: [
{
virtualNode: node,
},
],
match: {
prefixPath: '/node',
},
],
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
241 changes: 241 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,241 @@
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;
}

/**
* 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[];

/**
* The priority for the route. Routes are matched based on the specified value, where 0 is the highest priority.
*
* @default - none
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly priority?: number;
}

/**
* 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[];
}

/**
* 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;

/**
* The priority for the route. Routes are matched based on the specified value, where 0 is the highest priority
*
* @default - none
*/
readonly priority?: number;
}

/**
* 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);
}

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

/**
* The priority for the route. Routes are matched based on the specified value, where 0 is the highest priority
*/
abstract readonly priority?: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we exposing these - I don't see them used in Route...? I think we only need bind() in this class, no?

We should strive to minimize the public API surface as much as we can.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this field makes a bit more sense once we implement header based routing for HTTP.

Essentially it is used to disambiguate when a request matches multiple routes. I'll remove for this PR and we can add it back later!


/**
* 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[];

/**
* The priority for the route. Routes are matched based on the specified value, where 0 is the highest priority
*/
public readonly priority?: number;

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

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,
priority: this.priority,
};
}
}

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

public readonly priority?: number;

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

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

Loading