Skip to content

Commit

Permalink
feat(appmesh): add Route matching on path, query parameters, metadata…
Browse files Browse the repository at this point in the history
…, and method name (#15470)

Adding new match properties for `Route`.
- For HTTP match, adding `path` and `queryParameters`. Remove `prefixPath`.
- For gRPC match, adding `metadata` and `method name`

BREAKING CHANGE: `prefixPath` property in `HttpRouteMatch` has been renamed to `path`, and its type changed from `string` to `HttpRoutePathMatch`

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Seiya6329 committed Jul 13, 2021
1 parent 246b393 commit eeeec5d
Show file tree
Hide file tree
Showing 10 changed files with 893 additions and 48 deletions.
52 changes: 42 additions & 10 deletions packages/@aws-cdk/aws-appmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,18 @@ A `route` is associated with a virtual router, and it's used to match requests f

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

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

For HTTP based routes, the match field can be used to match on
path (prefix, exact, or regex), HTTP method, scheme, HTTP headers, and query parameters.
By default, an HTTP based route will match all requests.

For gRPC based routes, the match field can be used to match on service name, method name, and metadata.
When specifying the method name, service name must also be specified.

For example, here's how to add an HTTP route that matches based on a prefix of the URL path:

```ts
router.addRoute('route-http', {
routeSpec: appmesh.RouteSpec.http({
Expand All @@ -415,13 +427,14 @@ router.addRoute('route-http', {
},
],
match: {
prefixPath: '/path-to-app',
// Path that is passed to this method must start with '/'.
path: appmesh.HttpRoutePathMatch.startsWith('/path-to-app'),
},
}),
});
```

Add an HTTP2 route that matches based on method, scheme and header:
Add an HTTP2 route that matches based on exact path, method, scheme, headers, and query parameters:

```ts
router.addRoute('route-http2', {
Expand All @@ -432,14 +445,18 @@ router.addRoute('route-http2', {
},
],
match: {
prefixPath: '/',
path: appmesh.HttpRoutePathMatch.exactly('/exact'),
method: appmesh.HttpRouteMethod.POST,
protocol: appmesh.HttpRouteProtocol.HTTPS,
headers: [
// All specified headers must match for the route to match.
appmesh.HeaderMatch.valueIs('Content-Type', 'application/json'),
appmesh.HeaderMatch.valueIsNot('Content-Type', 'application/json'),
]
],
queryParameters: [
// All specified query parameters must match for the route to match.
appmesh.QueryParameterMatch.valueIs('query-field', 'value')
],
},
}),
});
Expand All @@ -461,7 +478,7 @@ router.addRoute('route-http', {
},
],
match: {
prefixPath: '/path-to-app',
path: appmesh.HttpRoutePathMatch.startsWith('/path-to-app'),
},
}),
});
Expand Down Expand Up @@ -511,12 +528,27 @@ router.addRoute('route-grpc-retry', {
});
```

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.
Add an gRPC route that matches based on method name and metadata:

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 `/`.
The timeout field can also be specified for `idle` and `perRequest` timeouts.
```ts
router.addRoute('route-grpc-retry', {
routeSpec: appmesh.RouteSpec.grpc({
weightedTargets: [{ virtualNode: node }],
match: {
// When method name is specified, service name must be also specified.
methodName: 'methodname',
serviceName: 'servicename',
metadata: [
// All specified metadata must match for the route to match.
appmesh.HeaderMatch.valueStartsWith('Content-Type', 'application/'),
appmesh.HeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'),
],
},
}),
});
```

Add a gRPC route with time out:

```ts
router.addRoute('route-http', {
Expand Down
97 changes: 97 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/http-route-path-match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { CfnRoute } from './appmesh.generated';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';

/**
* The type returned from the `bind()` method in {@link HttpRoutePathMatch}.
*/
export interface HttpRoutePathMatchConfig {
/**
* Route configuration for matching on the complete URL path of the request.
*
* @default - no matching will be performed on the complete URL path
*/
readonly wholePathMatch?: CfnRoute.HttpPathMatchProperty;

/**
* Route configuration for matching on the prefix of the URL path of the request.
*
* @default - no matching will be performed on the prefix of the URL path
*/
readonly prefixPathMatch?: string;
}

/**
* Defines HTTP route matching based on the URL path of the request.
*/
export abstract class HttpRoutePathMatch {
/**
* The value of the path must match the specified value exactly.
* The provided `path` must start with the '/' character.
*
* @param path the exact path to match on
*/
public static exactly(path: string): HttpRoutePathMatch {
return new HttpRouteWholePathMatch({ exact: path });
}

/**
* The value of the path must match the specified regex.
*
* @param regex the regex used to match the path
*/
public static regex(regex: string): HttpRoutePathMatch {
return new HttpRouteWholePathMatch({ regex: regex });
}

/**
* The value of the path must match the specified prefix.
*
* @param prefix the value to use to match the beginning of the path part of the URL of the request.
* It must start with the '/' character. If provided as "/", matches all 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".
*/
public static startsWith(prefix: string): HttpRoutePathMatch {
return new HttpRoutePrefixPathMatch(prefix);
}

/**
* Returns the route path match configuration.
*/
public abstract bind(scope: Construct): HttpRoutePathMatchConfig;
}

class HttpRoutePrefixPathMatch extends HttpRoutePathMatch {
constructor(private readonly prefix: string) {
super();

if (this.prefix && this.prefix[0] !== '/') {
throw new Error(`Prefix Path for the match must start with \'/\', got: ${this.prefix}`);
}
}

bind(_scope: Construct): HttpRoutePathMatchConfig {
return {
prefixPathMatch: this.prefix,
};
}
}

class HttpRouteWholePathMatch extends HttpRoutePathMatch {
constructor(private readonly match: CfnRoute.HttpPathMatchProperty) {
super();

if (this.match.exact && this.match.exact[0] !== '/') {
throw new Error(`Exact Path for the match must start with \'/\', got: ${this.match.exact}`);
}
}

bind(_scope: Construct): HttpRoutePathMatchConfig {
return {
wholePathMatch: this.match,
};
}
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export * from './tls-validation';
export * from './tls-client-policy';
export * from './http-route-method';
export * from './header-match';
export * from './query-parameter-match';
export * from './http-route-path-match';
31 changes: 29 additions & 2 deletions packages/@aws-cdk/aws-appmesh/lib/private/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Token, TokenComparison } from '@aws-cdk/core';
import { CfnVirtualNode } from '../appmesh.generated';
import { HeaderMatch } from '../header-match';
import { ListenerTlsOptions } from '../listener-tls-options';
import { QueryParameterMatch } from '../query-parameter-match';
import { GrpcRouteMatch } from '../route-spec';
import { TlsClientPolicy } from '../tls-client-policy';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
Expand Down Expand Up @@ -95,13 +97,38 @@ export function renderMeshOwner(resourceAccount: string, meshAccount: string) :
}

/**
* This is the helper method to validate the length of match array when it is specified.
* This is the helper method to validate the length of HTTP match array when it is specified.
*/
export function validateMatchArrayLength(headers?: HeaderMatch[]) {
export function validateHttpMatchArrayLength(headers?: HeaderMatch[], queryParameters?: QueryParameterMatch[]) {
const MIN_LENGTH = 1;
const MAX_LENGTH = 10;

if (headers && (headers.length < MIN_LENGTH || headers.length > MAX_LENGTH)) {
throw new Error(`Number of headers provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${headers.length}`);
}

if (queryParameters && (queryParameters.length < MIN_LENGTH || queryParameters.length > MAX_LENGTH)) {
throw new Error(`Number of query parameters provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${queryParameters.length}`);
}
}

/**
* This is the helper method to validate the length of gRPC match array when it is specified.
*/
export function validateGrpcMatchArrayLength(metadata?: HeaderMatch[]): void {
const MIN_LENGTH = 1;
const MAX_LENGTH = 10;

if (metadata && (metadata.length < MIN_LENGTH || metadata.length > MAX_LENGTH)) {
throw new Error(`Number of metadata provided for matching must be between ${MIN_LENGTH} and ${MAX_LENGTH}, got: ${metadata.length}`);
}
}

/**
* This is the helper method to validate at least one of gRPC match type is defined.
*/
export function validateGrpcMatch(match: GrpcRouteMatch): void {
if (match.serviceName === undefined && match.metadata === undefined && match.methodName === undefined) {
throw new Error('At least one gRPC match property must be provided');
}
}
54 changes: 54 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/query-parameter-match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { CfnRoute } from './appmesh.generated';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';

/**
* Configuration for `QueryParameterMatch`
*/
export interface QueryParameterMatchConfig {
/**
* Route CFN configuration for route query parameter match.
*/
readonly queryParameterMatch: CfnRoute.QueryParameterProperty;
}

/**
* Used to generate query parameter matching methods.
*/
export abstract class QueryParameterMatch {
/**
* The value of the query parameter with the given name in the request must match the
* specified value exactly.
*
* @param queryParameterName the name of the query parameter to match against
* @param queryParameterValue The exact value to test against
*/
static valueIs(queryParameterName: string, queryParameterValue: string): QueryParameterMatch {
return new QueryParameterMatchImpl(queryParameterName, { exact: queryParameterValue });
}

/**
* Returns the query parameter match configuration.
*/
public abstract bind(scope: Construct): QueryParameterMatchConfig;
}

class QueryParameterMatchImpl extends QueryParameterMatch {
constructor(
private readonly queryParameterName: string,
private readonly matchProperty: CfnRoute.HttpQueryParameterMatchProperty,
) {
super();
}

bind(_scope: Construct): QueryParameterMatchConfig {
return {
queryParameterMatch: {
match: this.matchProperty,
name: this.queryParameterName,
},
};
}
}
Loading

0 comments on commit eeeec5d

Please sign in to comment.