From 750bc8bf45777afc0f289dc706df0c352a314375 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 6 May 2019 13:11:36 +0200 Subject: [PATCH] feat(elbv2): add fixed response support for application load balancers (#2328) It follows the same pattern as the one used for `addTargetGroups` with the check on `priority`. This allows for the following scenario: an infrastructure stack exporting an application listener with a default fixed response and multiple application stacks registering their services with specific paths/headers on the imported application listener. --- .../aws-elasticloadbalancingv2/README.md | 11 ++ .../lib/alb/application-listener-rule.ts | 79 +++++++++++- .../lib/alb/application-listener.ts | 47 ++++++- .../lib/shared/base-listener.ts | 12 +- .../test/alb/test.listener.ts | 118 ++++++++++++++++++ 5 files changed, 260 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index 8b373664a9c78..6c53f01bb94e2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -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 diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts index 55b5e3a14d2cf..3520f5a0aadc9 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -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 * @@ -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 */ @@ -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', { @@ -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; } @@ -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 */ @@ -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.'); + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index ab6a663d211ef..c58b9a6f5a09a 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -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'; @@ -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 @@ -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. * @@ -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`); + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index a5772ff7a8304..0d20e2dd60967 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -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); @@ -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' }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts index d12486eaa68a2..acdad7562e2c7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts @@ -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 {