From 203f1145aee128508e206d3370ba409f5a4fb0ec Mon Sep 17 00:00:00 2001 From: Ronald Luitwieler Date: Tue, 28 May 2019 09:38:37 +0200 Subject: [PATCH] feat(apigateway): support for UsagePlan, ApiKey, UsagePlanKey (#2564) Add support for UsagePlan, ApiKey, UsagePlanKey. Fixes #723. --- install.sh | 3 +- packages/@aws-cdk/aws-apigateway/README.md | 36 +++ .../@aws-cdk/aws-apigateway/lib/api-key.ts | 112 ++++++++ packages/@aws-cdk/aws-apigateway/lib/index.ts | 2 + .../@aws-cdk/aws-apigateway/lib/restapi.ts | 18 ++ packages/@aws-cdk/aws-apigateway/lib/stage.ts | 2 +- .../@aws-cdk/aws-apigateway/lib/usage-plan.ts | 266 ++++++++++++++++++ packages/@aws-cdk/aws-apigateway/lib/util.ts | 6 + .../test/integ.restapi.expected.json | 60 +++- .../aws-apigateway/test/integ.restapi.ts | 25 +- .../aws-apigateway/test/test.api-key.ts | 46 +++ .../aws-apigateway/test/test.usage-plan.ts | 133 +++++++++ 12 files changed, 704 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/api-key.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.api-key.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.usage-plan.ts diff --git a/install.sh b/install.sh index 054ad67eee528..a8b58bc91a6c1 100755 --- a/install.sh +++ b/install.sh @@ -16,5 +16,6 @@ npm ci --global-style export PATH=node_modules/.bin:$PATH echo "=============================================================================================" -echo "bootstrapping..." +echo "cleanup and start bootstrapping..." +lerna clean --yes lerna bootstrap --reject-cycles --ci diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index a333707703efb..fe9324a3b4dbe 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -104,6 +104,42 @@ book.addMethod('GET', getBookIntegration, { }); ``` +The following example shows how to use an API Key with a usage plan: + +```ts +const hello = new lambda.Function(this, 'hello', { + runtime: lambda.Runtime.NodeJS810, + handler: 'hello.handler', + code: lambda.Code.asset('lambda') +}); + +const api = new apigateway.RestApi(this, 'hello-api', { }); +const integration = new apigateway.LambdaIntegration(hello); + +const v1 = api.root.addResource('v1'); +const echo = v1.addResource('echo'); +const echoMethod = echo.addMethod('GET', integration, { apiKeyRequired: true }); +const key = api.addApiKey('ApiKey'); + +const plan = api.addUsagePlan('UsagePlan', { + name: 'Easy', + apiKey: key +}); + +plan.addApiStage({ + stage: api.deploymentStage, + throttle: [ + { + method: echoMethod, + throttle: { + rateLimit: 10, + burstLimit: 2 + } + } + ] +}); +``` + #### Default Integration and Method Options The `defaultIntegration` and `defaultMethodOptions` properties can be used to diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts new file mode 100644 index 0000000000000..1baf475f57f1b --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts @@ -0,0 +1,112 @@ +import { Construct, IResource as IResourceBase, Resource } from '@aws-cdk/cdk'; +import { CfnApiKey } from './apigateway.generated'; +import { ResourceOptions } from "./resource"; +import { RestApi } from './restapi'; + +/** + * API keys are alphanumeric string values that you distribute to + * app developer customers to grant access to your API + */ +export interface ApiKeyAttributes { + /** + * The API key ID. + * @attribute + */ + readonly keyId: string; +} + +/** + * API keys are alphanumeric string values that you distribute to + * app developer customers to grant access to your API + */ +export interface IApiKey extends IResourceBase { + /** + * The API key ID. + * @attribute + */ + readonly keyId: string; +} + +/** + * ApiKey Properties. + */ +export interface ApiKeyProps extends ResourceOptions { + /** + * A list of resources this api key is associated with. + * @default none + */ + readonly resources?: RestApi[]; + + /** + * An AWS Marketplace customer identifier to use when integrating with the AWS SaaS Marketplace. + * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-customerid + * @default none + */ + readonly customerId?: string; + + /** + * A description of the purpose of the API key. + * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-description + * @default none + */ + readonly description?: string; + + /** + * Indicates whether the API key can be used by clients. + * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-enabled + * @default true + */ + readonly enabled?: boolean; + + /** + * Specifies whether the key identifier is distinct from the created API key value. + * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-generatedistinctid + * @default false + */ + readonly generateDistinctId?: boolean; + + /** + * A name for the API key. If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the API key name. + * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-name + * @default automically generated name + */ + readonly name?: string; +} + +/** + * An API Gateway ApiKey. + * + * An ApiKey can be distributed to API clients that are executing requests + * for Method resources that require an Api Key. + */ +export class ApiKey extends Resource implements IApiKey { + public readonly keyId: string; + + constructor(scope: Construct, id: string, props: ApiKeyProps = { }) { + super(scope, id); + + const resource = new CfnApiKey(this, 'Resource', { + customerId: props.customerId, + description: props.description, + enabled: props.enabled || true, + generateDistinctId: props.generateDistinctId, + name: props.name, + stageKeys: this.renderStageKeys(props.resources) + }); + + this.keyId = resource.ref; + } + + private renderStageKeys(resources: RestApi[] | undefined): CfnApiKey.StageKeyProperty[] | undefined { + if (!resources) { + return undefined; + } + + return resources.map((resource: RestApi) => { + const restApi = resource; + const restApiId = restApi.restApiId; + const stageName = restApi.deploymentStage!.stageName.toString(); + return { restApiId, stageName }; + }); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index 24a34330f16c2..e4d77bb5bf238 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -6,6 +6,8 @@ export * from './deployment'; export * from './stage'; export * from './integrations'; export * from './lambda-api'; +export * from './api-key'; +export * from './usage-plan'; export * from './vpc-link'; export * from './methodresponse'; export * from './model'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index a274d3e346802..28c0483f81a06 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,11 +1,13 @@ import iam = require('@aws-cdk/aws-iam'); import { CfnOutput, Construct, IResource as IResourceBase, Resource } from '@aws-cdk/cdk'; +import { ApiKey, IApiKey } from './api-key'; import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { Deployment } from './deployment'; import { Integration } from './integration'; import { Method, MethodOptions } from './method'; import { IResource, ResourceBase, ResourceOptions } from './resource'; import { Stage, StageOptions } from './stage'; +import { UsagePlan, UsagePlanProps } from './usage-plan'; export interface IRestApi extends IResourceBase { /** @@ -249,6 +251,22 @@ export class RestApi extends Resource implements IRestApi { return this.deploymentStage.urlForPath(path); } + /** + * Adds a usage plan. + */ + public addUsagePlan(id: string, props: UsagePlanProps): UsagePlan { + return new UsagePlan(this, id, props); + } + + /** + * Add an ApiKey + */ + public addApiKey(id: string): IApiKey { + return new ApiKey(this, id, { + resources: [this] + }); + } + /** * @returns The "execute-api" ARN. * @default "*" returns the execute API ARN for all methods/resources in diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index 06c065fbdf1e7..2fe3e24e33b43 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -163,7 +163,7 @@ export class Stage extends Resource { */ public readonly stageName: string; - private readonly restApi: IRestApi; + public readonly restApi: IRestApi; private enableCacheCluster?: boolean; constructor(scope: Construct, id: string, props: StageProps) { diff --git a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts new file mode 100644 index 0000000000000..0f22312c28860 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts @@ -0,0 +1,266 @@ +import { Token } from '@aws-cdk/cdk'; +import { Construct, Resource } from '@aws-cdk/cdk'; +import { IApiKey } from './api-key'; +import { CfnUsagePlan, CfnUsagePlanKey } from './apigateway.generated'; +import { Method } from './method'; +import { IRestApi } from './restapi'; +import { Stage } from './stage'; +import { validateInteger } from './util'; + +/** + * Container for defining throttling parameters to API stages or methods. + * @link https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html + */ +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; +} + +/** + * Time period for which quota settings apply. + */ +export enum Period { + Day = 'DAY', + Week = 'WEEK', + Month = 'MONTH' +} + +/** + * Specifies the maximum number of requests that clients can make to API Gateway APIs. + */ +export interface QuotaSettings { + /** + * The maximum number of requests that users can make within the specified time period. + * @default none + */ + readonly limit?: number; + + /** + * For the initial time period, the number of requests to subtract from the specified limit. + * @default none + */ + readonly offset?: number; + + /** + * The time period for which the maximum limit of requests applies. + * @default none + */ + readonly period?: Period; +} + +/** + * Represents per-method throttling for a resource. + */ +export interface ThrottlingPerMethod { + /** + * [disable-awslint:ref-via-interface] + * The method for which you specify the throttling settings. + * @default none + */ + readonly method: Method, + + /** + * Specifies the overall request rate (average requests per second) and burst capacity. + * @default none + */ + readonly throttle: ThrottleSettings +} + +/** + * Type of Usage Plan Key. Currently the only supported type is 'API_KEY' + */ +export enum UsagePlanKeyType { + ApiKey = 'API_KEY' +} + +/** + * Represents the API stages that a usage plan applies to. + */ +export interface UsagePlanPerApiStage { + + /** + * @default none + */ + readonly api?: IRestApi, + + /** + * + * [disable-awslint:ref-via-interface] + * @default none + */ + readonly stage?: Stage, + + /** + * @default none + */ + readonly throttle?: ThrottlingPerMethod[] +} + +export interface UsagePlanProps { + /** + * API Stages to be associated which the usage plan. + * @default none + */ + readonly apiStages?: UsagePlanPerApiStage[], + + /** + * Represents usage plan purpose. + * @default none + */ + readonly description?: string, + + /** + * Number of requests clients can make in a given time period. + * @default none + */ + readonly quota?: QuotaSettings + + /** + * Overall throttle settings for the API. + * @default none + */ + readonly throttle?: ThrottleSettings, + + /** + * Name for this usage plan. + * @default none + */ + readonly name?: string + + /** + * ApiKey to be associated with the usage plan. + * @default none + */ + readonly apiKey?: IApiKey +} + +export class UsagePlan extends Resource { + /** + * @attribute + */ + public readonly usagePlanId: string; + + private readonly apiStages = new Array(); + + constructor(scope: Construct, id: string, props: UsagePlanProps = { }) { + super(scope, id); + let resource: CfnUsagePlan; + + resource = new CfnUsagePlan(this, 'Resource', { + apiStages: new Token(() => this.renderApiStages(this.apiStages)), + description: props.description, + quota: this.renderQuota(props), + throttle: this.renderThrottle(props.throttle), + usagePlanName: props.name, + }); + + this.apiStages.push(...(props.apiStages || [])); + + this.usagePlanId = resource.ref; + + // Add ApiKey when + if (props.apiKey) { + this.addApiKey(props.apiKey); + } + } + + /** + * Adds an ApiKey. + * + * @param apiKey + */ + public addApiKey(apiKey: IApiKey): void { + new CfnUsagePlanKey(this, 'UsagePlanKeyResource', { + keyId: apiKey.keyId, + keyType: UsagePlanKeyType.ApiKey, + usagePlanId: this.usagePlanId + }); + } + + /** + * Adds an apiStage. + * @param apiStage + */ + public addApiStage(apiStage: UsagePlanPerApiStage) { + this.apiStages.push(apiStage); + } + + /** + * + * @param props + */ + private renderApiStages(apiStages: UsagePlanPerApiStage[] | undefined): CfnUsagePlan.ApiStageProperty[] | undefined { + if (apiStages && apiStages.length > 0) { + const stages: CfnUsagePlan.ApiStageProperty[] = []; + apiStages.forEach((apiStage: UsagePlanPerApiStage) => { + stages.push(this.createStage(apiStage)); + }); + return stages; + } + return undefined; + } + + private createStage(apiStage: UsagePlanPerApiStage): CfnUsagePlan.ApiStageProperty { + const stage = apiStage.stage ? apiStage.stage.stageName.toString() : undefined; + const apiId = apiStage.stage ? apiStage.stage.restApi.restApiId : undefined; + const throttle = this.renderThrottlePerMethod(apiStage.throttle); + return { + apiId, + stage, + throttle + }; + } + + private renderQuota(props: UsagePlanProps) { + if (props.quota === undefined) { + return undefined; + } else { + const limit = props.quota ? props.quota.limit : undefined; + validateInteger(limit, 'Throttle quota limit'); + const ret = { + limit: limit ? limit : undefined, + offset: props.quota ? props.quota.offset : undefined, + period: props.quota ? props.quota.period : undefined + }; + return ret; + } + } + + private renderThrottle(props: ThrottleSettings | undefined): (CfnUsagePlan.ThrottleSettingsProperty | Token) { + let ret: CfnUsagePlan.ThrottleSettingsProperty | Token; + if (props !== undefined) { + const burstLimit = props.burstLimit; + validateInteger(burstLimit, 'Throttle burst limit'); + const rateLimit = props.rateLimit; + validateInteger(rateLimit, 'Throttle rate limit'); + + ret = { + burstLimit: burstLimit ? burstLimit : undefined, + rateLimit: rateLimit ? rateLimit : undefined + }; + } + return ret!; + } + + private renderThrottlePerMethod(throttlePerMethod?: ThrottlingPerMethod[]) { + const ret: { [key: string]: (CfnUsagePlan.ThrottleSettingsProperty | Token) } = {}; + if (throttlePerMethod && throttlePerMethod.length > 0) { + throttlePerMethod.forEach((value: ThrottlingPerMethod) => { + const method: Method = value.method; + // this methodId is resource path and method for example /GET or /pets/GET + const methodId = `${method.resource.path}/${method.httpMethod}`; + ret[methodId] = this.renderThrottle(value.throttle); + }); + } + return ret; + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/util.ts b/packages/@aws-cdk/aws-apigateway/lib/util.ts index 5217f1a3bcf49..b42557a60a7c1 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/util.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/util.ts @@ -67,3 +67,9 @@ export function parseAwsApiCall(path?: string, action?: string, actionParams?: { throw new Error(`Either "path" or "action" are required`); } + +export function validateInteger(property: number | undefined, messagePrefix: string) { + if (property && !Number.isInteger(property)) { + throw new Error(`${messagePrefix} should be an integer`); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index 7e095288177d3..e5298cbfc2bb2 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -6,7 +6,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB498cbe233ce6b744150b3f2d80a9c3dd24": { + "myapiDeployment92F2CB4919460d935da8177bcfbc418506e514ff": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -37,7 +37,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "0.5", "DeploymentId": { - "Ref": "myapiDeployment92F2CB498cbe233ce6b744150b3f2d80a9c3dd24" + "Ref": "myapiDeployment92F2CB4919460d935da8177bcfbc418506e514ff" }, "Description": "beta stage", "MethodSettings": [ @@ -148,6 +148,7 @@ "RestApiId": { "Ref": "myapi4C7BF186" }, + "ApiKeyRequired": true, "AuthorizationType": "NONE", "Integration": { "IntegrationHttpMethod": "POST", @@ -623,6 +624,61 @@ ] } } + }, + "myapiUsagePlan56F9C4F2": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "myapi4C7BF186" + }, + "Stage": { + "Ref": "myapiDeploymentStagebeta96434BEB" + }, + "Throttle": { + "/v1/toys/GET": { + "BurstLimit": 2, + "RateLimit": 10 + } + } + } + ], + "Description": "Free tier monthly usage plan", + "Quota": { + "Limit": 10000, + "Period": "MONTH" + }, + "UsagePlanName": "Basic" + } + }, + "myapiUsagePlanUsagePlanKeyResource050D133F": { + "Type": "AWS::ApiGateway::UsagePlanKey", + "Properties": { + "KeyId": { + "Ref": "myapiApiKey43446CCF" + }, + "KeyType": "API_KEY", + "UsagePlanId": { + "Ref": "myapiUsagePlan56F9C4F2" + } + } + }, + "myapiApiKey43446CCF": { + "Type": "AWS::ApiGateway::ApiKey", + "Properties": { + "Enabled": true, + "StageKeys": [ + { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "StageName": { + "Ref": "myapiDeploymentStagebeta96434BEB" + } + } + ] + } } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index 60c3fb55b437c..57562329ebc98 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -33,7 +33,7 @@ class Test extends cdk.Stack { const integration = new apigateway.LambdaIntegration(handler); const toys = v1.addResource('toys'); - toys.addMethod('GET', integration); + const getToysMethod: apigateway.Method = toys.addMethod('GET', integration, { apiKeyRequired: true }); toys.addMethod('POST'); toys.addMethod('PUT'); @@ -52,6 +52,29 @@ class Test extends cdk.Stack { body: JSON.stringify(event) }); } + + const key = api.addApiKey('ApiKey'); + const plan = api.addUsagePlan('UsagePlan', { + name: 'Basic', + apiKey: key, + description: 'Free tier monthly usage plan', + quota: { + limit: 10000, + period: apigateway.Period.Month + } + }); + plan.addApiStage({ + stage: api.deploymentStage, + throttle: [ + { + method: getToysMethod, + throttle: { + rateLimit: 10, + burstLimit: 2 + } + } + ] + }); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.api-key.ts b/packages/@aws-cdk/aws-apigateway/test/test.api-key.ts new file mode 100644 index 0000000000000..41806d0f2288a --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.api-key.ts @@ -0,0 +1,46 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from "nodeunit"; +import apigateway = require('../lib'); + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new apigateway.ApiKey(stack, 'my-api-key'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', undefined, ResourcePart.CompleteDefinition)); + // should have an api key with no props defined. + + test.done(); + }, + + 'specify props for apiKey'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + api.root.addMethod('GET'); // api must have atleast one method. + + // WHEN + new apigateway.ApiKey(stack, 'test-api-key', { + customerId: 'test-customer', + resources: [api] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', { + CustomerId: 'test-customer', + StageKeys: [ + { + RestApiId: { Ref: "testapiD6451F70" }, + StageName: { Ref: "testapiDeploymentStagetest5869DF71" } + } + ] + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.usage-plan.ts b/packages/@aws-cdk/aws-apigateway/test/test.usage-plan.ts new file mode 100644 index 0000000000000..f8e89653bf311 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.usage-plan.ts @@ -0,0 +1,133 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from "nodeunit"; +import apigateway = require('../lib'); + +const RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlan'; +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api', { cloudWatchRole: false, deploy: false }); + api.root.addMethod('GET'); // Need at least one method on the api + const usagePlanName = 'Pro'; + const usagePlanDescription = 'Pro Usage Plan with no throttling limits'; + + // WHEN + new apigateway.UsagePlan(stack, 'my-usage-plan', { + name: usagePlanName, + description: usagePlanDescription + }); + + // THEN + expect(stack).to(haveResource(RESOURCE_TYPE, { + UsagePlanName: usagePlanName, + Description: usagePlanDescription + }, ResourcePart.Properties)); + + test.done(); + }, + + 'usage plan with throttling limits'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + const method: apigateway.Method = api.root.addMethod('GET'); // Need at least one method on the api + const usagePlanName = 'Basic'; + const usagePlanDescription = 'Basic Usage Plan with no throttling limits'; + + // WHEN + new apigateway.UsagePlan(stack, 'my-usage-plan', { + name: usagePlanName, + description: usagePlanDescription, + apiStages: [ + { + stage: api.deploymentStage, + throttle: [ + { + method, + throttle: { + burstLimit: 20, + rateLimit: 10 + } + } + ] + } + ] + }); + + // THEN + expect(stack).to(haveResource(RESOURCE_TYPE, { + UsagePlanName: usagePlanName, + Description: usagePlanDescription, + ApiStages: [ + { + ApiId: { + Ref: 'myapi4C7BF186' + }, + Stage: { + Ref: 'myapiDeploymentStagetest4A4AB65E' + }, + Throttle: { + '//GET': { + BurstLimit: 20, + RateLimit: 10 + } + } + } + ] + }, ResourcePart.Properties)); + + test.done(); + + }, + + 'usage plan with quota limits'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new apigateway.UsagePlan(stack, 'my-usage-plan', { + quota: { + limit: 10000, + period: apigateway.Period.Month + } + }); + + // THEN + expect(stack).to(haveResource(RESOURCE_TYPE, { + Quota: { + Limit: 10000, + Period: 'MONTH' + } + }, ResourcePart.Properties)); + + test.done(); + + }, + + 'UsagePlanKey'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', { + name: 'Basic', + }); + const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key'); + + // WHEN + usagePlan.addApiKey(apiKey); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::UsagePlanKey', { + KeyId: { + Ref: 'myapikey1B052F70' + }, + KeyType: 'API_KEY', + UsagePlanId: { + Ref: 'myusageplan23AA1E32' + } + }, ResourcePart.Properties)); + + test.done(); + } +};