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(elbv2): add fixed response support for application load balancers #2328

Merged
merged 2 commits into from
May 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/@aws-cdk/aws-elasticloadbalancingv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ listener.addTargets('ApplicationFleet', {
The security groups of the load balancer and the target are automatically
updated to allow the network traffic.

Use the `addFixedResponse()` method to add fixed response rules on the listener:

```ts
listener.addFixedResponse('Fixed', {
pathPattern: '/ok',
contentType: elbv2.ContentType.TEXT_PLAIN,
messageBody: 'OK',
statusCode: '200'
});
```

#### Conditions

It's possible to route traffic to targets based on conditions in the incoming
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ export interface BaseApplicationListenerRuleProps {
readonly priority: number;

/**
* Target groups to forward requests to
* Target groups to forward requests to. Only one of `targetGroups` or
* `fixedResponse` can be specified.
*/
readonly targetGroups?: IApplicationTargetGroup[];

/**
* Fixed response to return. Only one of `fixedResponse` or
* `targetGroups` can be specified.
*/
readonly fixedResponse?: FixedResponse;

/**
* Rule applies if the requested host matches the indicated host
*
Expand Down Expand Up @@ -54,6 +61,41 @@ export interface ApplicationListenerRuleProps extends BaseApplicationListenerRul
readonly listener: IApplicationListener;
}

/**
* The content type for a fixed response
*/
export enum ContentType {
TEXT_PLAIN = 'text/plain',
TEXT_CSS = 'text/css',
TEXT_HTML = 'text/html',
APPLICATION_JAVASCRIPT = 'application/javascript',
APPLICATION_JSON = 'application/json'
}

/**
* A fixed response
*/
export interface FixedResponse {
/**
* The HTTP response code (2XX, 4XX or 5XX)
*/
readonly statusCode: string;

/**
* The content type
*
* @default text/plain
*/
readonly contentType?: ContentType;

/**
* The message
*
* @default no message
*/
readonly messageBody?: string;
}

/**
* Define a new listener rule
*/
Expand All @@ -75,6 +117,10 @@ export class ApplicationListenerRule extends cdk.Construct {
throw new Error(`At least one of 'hostHeader' or 'pathPattern' is required when defining a load balancing rule.`);
}

if (props.targetGroups && props.fixedResponse) {
throw new Error('Cannot combine `targetGroups` with `fixedResponse`.');
}

this.listener = props.listener;

const resource = new CfnListenerRule(this, 'Resource', {
Expand All @@ -93,6 +139,10 @@ export class ApplicationListenerRule extends cdk.Construct {

(props.targetGroups || []).forEach(this.addTargetGroup.bind(this));

if (props.fixedResponse) {
this.addFixedResponse(props.fixedResponse);
}

this.listenerRuleArn = resource.ref;
}

Expand All @@ -114,6 +164,18 @@ export class ApplicationListenerRule extends cdk.Construct {
targetGroup.registerListener(this.listener, this);
}

/**
* Add a fixed response
*/
public addFixedResponse(fixedResponse: FixedResponse) {
validateFixedResponse(fixedResponse);

this.actions.push({
fixedResponseConfig: fixedResponse,
type: 'fixed-response'
});
}

/**
* Validate the rule
*/
Expand All @@ -137,3 +199,18 @@ export class ApplicationListenerRule extends cdk.Construct {
return ret;
}
}

/**
* Validate the status code and message body of a fixed response
*
* @internal
*/
export function validateFixedResponse(fixedResponse: FixedResponse) {
if (fixedResponse.statusCode && !/^(2|4|5)\d\d$/.test(fixedResponse.statusCode)) {
throw new Error('`statusCode` must be 2XX, 4XX or 5XX.');
}

if (fixedResponse.messageBody && fixedResponse.messageBody.length > 1024) {
throw new Error('`messageBody` cannot have more than 1024 characters.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HealthCheck } from '../shared/base-target-group';
import { ApplicationProtocol, SslPolicy } from '../shared/enums';
import { determineProtocolAndPort } from '../shared/util';
import { ApplicationListenerCertificate } from './application-listener-certificate';
import { ApplicationListenerRule } from './application-listener-rule';
import { ApplicationListenerRule, FixedResponse, validateFixedResponse } from './application-listener-rule';
import { IApplicationLoadBalancer } from './application-load-balancer';
import { ApplicationTargetGroup, IApplicationLoadBalancerTarget, IApplicationTargetGroup } from './application-target-group';

Expand Down Expand Up @@ -153,9 +153,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis
* At least one TargetGroup must be added without conditions.
*/
public addTargetGroups(id: string, props: AddApplicationTargetGroupsProps): void {
if ((props.hostHeader !== undefined || props.pathPattern !== undefined) !== (props.priority !== undefined)) {
throw new Error(`Setting 'pathPattern' or 'hostHeader' also requires 'priority', and vice versa`);
}
checkAddRuleProps(props);

if (props.priority !== undefined) {
// New rule
Expand Down Expand Up @@ -215,6 +213,35 @@ export class ApplicationListener extends BaseListener implements IApplicationLis
return group;
}

/**
* Add a fixed response
*/
public addFixedResponse(id: string, props: AddFixedResponseProps) {
checkAddRuleProps(props);

const fixedResponse: FixedResponse = {
statusCode: props.statusCode,
contentType: props.contentType,
messageBody: props.messageBody
};

validateFixedResponse(fixedResponse);

if (props.priority) {
new ApplicationListenerRule(this, id + 'Rule', {
listener: this,
priority: props.priority,
fixedResponse,
...props
});
} else {
this._addDefaultAction({
fixedResponseConfig: fixedResponse,
type: 'fixed-response'
});
}
}

/**
* Register that a connectable that has been added to this load balancer.
*
Expand Down Expand Up @@ -539,3 +566,15 @@ export interface AddApplicationTargetsProps extends AddRuleProps {
*/
readonly healthCheck?: HealthCheck;
}

/**
* Properties for adding a fixed response to a listener
*/
export interface AddFixedResponseProps extends AddRuleProps, FixedResponse {
}

function checkAddRuleProps(props: AddRuleProps) {
if ((props.hostHeader !== undefined || props.pathPattern !== undefined) !== (props.priority !== undefined)) {
throw new Error(`Setting 'pathPattern' or 'hostHeader' also requires 'priority', and vice versa`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ITargetGroup } from './base-target-group';
*/
export abstract class BaseListener extends cdk.Construct {
public readonly listenerArn: string;
private readonly defaultActions: any[] = [];
private readonly defaultActions: CfnListener.ActionProperty[] = [];

constructor(scope: cdk.Construct, id: string, additionalProps: any) {
super(scope, id);
Expand All @@ -30,12 +30,20 @@ export abstract class BaseListener extends cdk.Construct {
return [];
}

/**
* Add an action to the list of default actions of this listener
* @internal
*/
protected _addDefaultAction(action: CfnListener.ActionProperty) {
this.defaultActions.push(action);
}

/**
* Add a TargetGroup to the list of default actions of this listener
* @internal
*/
protected _addDefaultTargetGroup(targetGroup: ITargetGroup) {
this.defaultActions.push({
this._addDefaultAction({
targetGroupArn: targetGroup.targetGroupArn,
type: 'forward'
});
Expand Down
118 changes: 118 additions & 0 deletions packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,124 @@ export = {

test.done();
},

'Can add fixed responses'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.VpcNetwork(stack, 'VPC');
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', {
vpc
});
const listener = lb.addListener('Listener', {
port: 80
});

// WHEN
listener.addFixedResponse('Default', {
contentType: elbv2.ContentType.TEXT_PLAIN,
messageBody: 'Not Found',
statusCode: '404'
});
listener.addFixedResponse('Hello', {
priority: 10,
pathPattern: '/hello',
statusCode: '503'
});

// THEN
expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', {
DefaultActions: [
{
FixedResponseConfig: {
ContentType: 'text/plain',
MessageBody: 'Not Found',
StatusCode: '404'
},
Type: 'fixed-response'
}
]
}));

expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', {
Actions: [
{
FixedResponseConfig: {
StatusCode: '503'
},
Type: 'fixed-response'
}
]
}));

test.done();
},

'Throws with bad fixed responses': {

'status code'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.VpcNetwork(stack, 'VPC');
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', {
vpc
});
const listener = lb.addListener('Listener', {
port: 80
});

// THEN
test.throws(() => listener.addFixedResponse('Default', {
statusCode: '301'
}), /`statusCode`/);

test.done();
},

'message body'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.VpcNetwork(stack, 'VPC');
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', {
vpc
});
const listener = lb.addListener('Listener', {
port: 80
});

// THEN
test.throws(() => listener.addFixedResponse('Default', {
messageBody: 'a'.repeat(1025),
statusCode: '500'
}), /`messageBody`/);

test.done();
}
},

'Throws when specifying both target groups and fixed reponse'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = new ec2.VpcNetwork(stack, 'VPC');
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LoadBalancer', {
vpc
});
const listener = lb.addListener('Listener', {
port: 80
});

// THEN
test.throws(() => new elbv2.ApplicationListenerRule(stack, 'Rule', {
listener,
priority: 10,
pathPattern: '/hello',
targetGroups: [new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, port: 80 })],
fixedResponse: {
statusCode: '500'
}
}), /`targetGroups`.*`fixedResponse`/);

test.done();
}
};

class ResourceWithLBDependency extends cdk.CfnResource {
Expand Down