diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts index 69f84a40da5e4..523ba77a5e1cb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts @@ -59,6 +59,13 @@ export interface StageOptions { * @default - no custom domain and api mapping configuration */ readonly domainMapping?: DomainMappingOptions; + + /** + * Throttle settings for the routes of this stage + * + * @default - no throttling configuration + */ + readonly throttle?: ThrottleSettings; } /** @@ -70,3 +77,20 @@ export interface StageAttributes { */ readonly stageName: string; } + +/** + * Container for defining throttling parameters to API stages + */ +export interface ThrottleSettings { + /** + * The API request steady-state rate limit (average requests per second over an extended period of time) + * @default none + */ + readonly rateLimit?: number; + + /** + * The maximum API request rate limit over a time ranging from one to a few seconds. + * @default none + */ + readonly burstLimit?: number; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index ca40349975ec1..59024a3769f56 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -167,6 +167,10 @@ export class HttpStage extends HttpStageBase { apiId: props.httpApi.apiId, stageName: this.physicalName, autoDeploy: props.autoDeploy, + defaultRouteSettings: !props.throttle ? undefined : { + throttlingBurstLimit: props.throttle?.burstLimit, + throttlingRateLimit: props.throttle?.rateLimit, + }, }); this.stageName = this.physicalName; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts index 685850a746f4e..1b769966e7d48 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -91,6 +91,10 @@ export class WebSocketStage extends StageBase implements IWebSocketStage { apiId: props.webSocketApi.apiId, stageName: this.physicalName, autoDeploy: props.autoDeploy, + defaultRouteSettings: !props.throttle ? undefined : { + throttlingBurstLimit: props.throttle?.burstLimit, + throttlingRateLimit: props.throttle?.rateLimit, + }, }); if (props.domainMapping) { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.stage.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.stage.expected.json new file mode 100644 index 0000000000000..e5693781538c5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.stage.expected.json @@ -0,0 +1,24 @@ +{ + "Resources": { + "HttpApiF5A9A8A7": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpApi", + "ProtocolType": "HTTP" + } + }, + "HttpStageWithPropertiesC0AABA83": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "StageName": "$default", + "DefaultRouteSettings": { + "ThrottlingBurstLimit": 1000, + "ThrottlingRateLimit": 1000 + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.stage.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.stage.ts new file mode 100644 index 0000000000000..b1626871f0d08 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.stage.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-http-stage'); + +const httpApi = new apigw.HttpApi(stack, 'HttpApi', { createDefaultStage: false }); +new apigw.HttpStage(stack, 'HttpStageWithProperties', { + httpApi, + throttle: { + rateLimit: 1000, + burstLimit: 1000, + }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/aws-cdk-aws-apigatewayv2-http-stage.template.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/aws-cdk-aws-apigatewayv2-http-stage.template.json new file mode 100644 index 0000000000000..691ff7d1a8272 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/aws-cdk-aws-apigatewayv2-http-stage.template.json @@ -0,0 +1,24 @@ +{ + "Resources": { + "HttpApiF5A9A8A7": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpApi", + "ProtocolType": "HTTP" + } + }, + "HttpStageWithPropertiesC0AABA83": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "StageName": "$default", + "DefaultRouteSettings": { + "ThrottlingBurstLimit": 1000, + "ThrottlingRateLimit": 1000 + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..90bef2e09ad39 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"17.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..ebefdbef68c84 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/manifest.json @@ -0,0 +1,35 @@ +{ + "version": "17.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + }, + "metadata": {} + }, + "aws-cdk-aws-apigatewayv2-http-stage": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-aws-apigatewayv2-http-stage.template.json", + "validateOnSynth": false + }, + "metadata": { + "/aws-cdk-aws-apigatewayv2-http-stage/HttpApi/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "HttpApiF5A9A8A7" + } + ], + "/aws-cdk-aws-apigatewayv2-http-stage/HttpStageWithProperties/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "HttpStageWithPropertiesC0AABA83" + } + ] + }, + "displayName": "aws-cdk-aws-apigatewayv2-http-stage" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/tree.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/tree.json new file mode 100644 index 0000000000000..e897384e7b29f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.integ.snapshot/tree.json @@ -0,0 +1,87 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + }, + "aws-cdk-aws-apigatewayv2-http-stage": { + "id": "aws-cdk-aws-apigatewayv2-http-stage", + "path": "aws-cdk-aws-apigatewayv2-http-stage", + "children": { + "HttpApi": { + "id": "HttpApi", + "path": "aws-cdk-aws-apigatewayv2-http-stage/HttpApi", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-apigatewayv2-http-stage/HttpApi/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Api", + "aws:cdk:cloudformation:props": { + "name": "HttpApi", + "protocolType": "HTTP" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apigatewayv2.CfnApi", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apigatewayv2.HttpApi", + "version": "0.0.0" + } + }, + "HttpStageWithProperties": { + "id": "HttpStageWithProperties", + "path": "aws-cdk-aws-apigatewayv2-http-stage/HttpStageWithProperties", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-apigatewayv2-http-stage/HttpStageWithProperties/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Stage", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "HttpApiF5A9A8A7" + }, + "stageName": "$default", + "defaultRouteSettings": { + "throttlingBurstLimit": 1000, + "throttlingRateLimit": 1000 + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apigatewayv2.CfnStage", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apigatewayv2.HttpStage", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts index b617fe4613a51..07bf3b85846a2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts @@ -163,4 +163,31 @@ describe('HttpStage with domain mapping', () => { expect(t).toThrow(/domainUrl is not available when no API mapping is associated with the Stage/); }); -}); \ No newline at end of file + + test('correctly sets throttle settings', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: false, + }); + + // WHEN + new HttpStage(stack, 'DefaultStage', { + httpApi: api, + throttle: { + burstLimit: 1000, + rateLimit: 1000, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Stage', { + ApiId: stack.resolve(api.apiId), + StageName: '$default', + DefaultRouteSettings: { + ThrottlingBurstLimit: 1000, + ThrottlingRateLimit: 1000, + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.stage.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.stage.expected.json new file mode 100644 index 0000000000000..3f22d2c416ba9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.stage.expected.json @@ -0,0 +1,25 @@ +{ + "Resources": { + "WebSocketApi34BCF99B": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "WebSocketApi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + }, + "WebSocketStageC46B7E43": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "StageName": "dev", + "DefaultRouteSettings": { + "ThrottlingBurstLimit": 1000, + "ThrottlingRateLimit": 1000 + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.stage.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.stage.ts new file mode 100644 index 0000000000000..65fbefd3dac39 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.stage.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-websocket-stage'); + +const webSocketApi = new apigw.WebSocketApi(stack, 'WebSocketApi'); +new apigw.WebSocketStage(stack, 'WebSocketStage', { + webSocketApi, + stageName: 'dev', + throttle: { + rateLimit: 1000, + burstLimit: 1000, + }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/aws-cdk-aws-apigatewayv2-websocket-stage.template.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/aws-cdk-aws-apigatewayv2-websocket-stage.template.json new file mode 100644 index 0000000000000..7d5748e0cc4c2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/aws-cdk-aws-apigatewayv2-websocket-stage.template.json @@ -0,0 +1,25 @@ +{ + "Resources": { + "WebSocketApi34BCF99B": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "WebSocketApi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + }, + "WebSocketStageC46B7E43": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "StageName": "dev", + "DefaultRouteSettings": { + "ThrottlingBurstLimit": 1000, + "ThrottlingRateLimit": 1000 + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..90bef2e09ad39 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"17.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..6374c7befef0f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/manifest.json @@ -0,0 +1,35 @@ +{ + "version": "17.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + }, + "metadata": {} + }, + "aws-cdk-aws-apigatewayv2-websocket-stage": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-aws-apigatewayv2-websocket-stage.template.json", + "validateOnSynth": false + }, + "metadata": { + "/aws-cdk-aws-apigatewayv2-websocket-stage/WebSocketApi/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "WebSocketApi34BCF99B" + } + ], + "/aws-cdk-aws-apigatewayv2-websocket-stage/WebSocketStage/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "WebSocketStageC46B7E43" + } + ] + }, + "displayName": "aws-cdk-aws-apigatewayv2-websocket-stage" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/tree.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/tree.json new file mode 100644 index 0000000000000..0dc3bacf7e2f4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.integ.snapshot/tree.json @@ -0,0 +1,88 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + }, + "aws-cdk-aws-apigatewayv2-websocket-stage": { + "id": "aws-cdk-aws-apigatewayv2-websocket-stage", + "path": "aws-cdk-aws-apigatewayv2-websocket-stage", + "children": { + "WebSocketApi": { + "id": "WebSocketApi", + "path": "aws-cdk-aws-apigatewayv2-websocket-stage/WebSocketApi", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-apigatewayv2-websocket-stage/WebSocketApi/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Api", + "aws:cdk:cloudformation:props": { + "name": "WebSocketApi", + "protocolType": "WEBSOCKET", + "routeSelectionExpression": "$request.body.action" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apigatewayv2.CfnApi", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apigatewayv2.WebSocketApi", + "version": "0.0.0" + } + }, + "WebSocketStage": { + "id": "WebSocketStage", + "path": "aws-cdk-aws-apigatewayv2-websocket-stage/WebSocketStage", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-apigatewayv2-websocket-stage/WebSocketStage/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Stage", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "WebSocketApi34BCF99B" + }, + "stageName": "dev", + "defaultRouteSettings": { + "throttlingBurstLimit": 1000, + "throttlingRateLimit": 1000 + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apigatewayv2.CfnStage", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apigatewayv2.WebSocketStage", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts index b1af6af2e59bc..3699d1bad78c0 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -107,4 +107,30 @@ describe('WebSocketStage', () => { }); }); }); + + test('correctly sets throttle settings', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + new WebSocketStage(stack, 'DefaultStage', { + webSocketApi: api, + stageName: 'dev', + throttle: { + burstLimit: 1000, + rateLimit: 1000, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Stage', { + ApiId: stack.resolve(api.apiId), + StageName: 'dev', + DefaultRouteSettings: { + ThrottlingBurstLimit: 1000, + ThrottlingRateLimit: 1000, + }, + }); + }); });