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(aws-apigateway): add support for UsagePlan, ApiKey, UsagePlanKey… #1221

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
83 changes: 83 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import cdk = require('@aws-cdk/cdk');
import { cloudformation } from './apigateway.generated';
import { IRestApiResource } from "./resource";
import { RestApi } from './restapi';

export interface ApiKeyProps {
/**
* A list of resources this api key is associated with.
*/
resources?: IRestApiResource[];
/**
* AWS Marketplace customer identifier to distribute this key to.
akkaash marked this conversation as resolved.
Show resolved Hide resolved
*/
customerId?: string;
/**
* Purpose of the API Key
*/
description?: string;
/**
* Whether this API Key is enabled for use.
akkaash marked this conversation as resolved.
Show resolved Hide resolved
*/
enabled?: boolean;
/**
* Distinguish the key identifier from the key value
*/
generateDistinctId?: boolean;
/**
* Name of the key
*/
name?: string;
}

/**
* Creates an API Gateway ApiKey.
akkaash marked this conversation as resolved.
Show resolved Hide resolved
*
* An ApiKey can be distributed to API clients that are executing requests
* for Method resources that require an Api Key.
*/
export class ApiKey extends cdk.Construct {
public readonly keyId: string;
constructor(parent: cdk.Construct, id: string, props?: ApiKeyProps) {
super(parent, id);

const customerId = props ? props!.customerId : undefined;
akkaash marked this conversation as resolved.
Show resolved Hide resolved
const description = props ? props!.description : undefined;
const enabled = props ? props!.enabled : undefined;
const generateDistinctId = props ? props!.generateDistinctId : undefined;
const name = props ? props!.name : undefined;
const stageKeys = this.renderStageKeys(props ? props!.resources : undefined);

const apiKeyResourceProps: cloudformation.ApiKeyResourceProps = {
akkaash marked this conversation as resolved.
Show resolved Hide resolved
customerId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know why this couldn't just be

customerId: props.customerId,
description: props.description,
// etc

?

description,
enabled,
generateDistinctId,
name,
stageKeys
};

const resource: cloudformation.ApiKeyResource = new cloudformation.ApiKeyResource(this, 'Resource', apiKeyResourceProps);
akkaash marked this conversation as resolved.
Show resolved Hide resolved

this.keyId = resource.ref;
}

private renderStageKeys(resources: IRestApiResource[] | undefined): cloudformation.ApiKeyResource.StageKeyProperty[] | undefined {
if (!resources) {
return undefined;
}

const stageKeys = new Array<cloudformation.ApiKeyResource.StageKeyProperty>();
resources.forEach((resource: IRestApiResource) => {
akkaash marked this conversation as resolved.
Show resolved Hide resolved
const restApi: RestApi = resource.resourceApi;
const restApiId = restApi.restApiId;
const stageName = restApi!.deploymentStage!.stageName.toString();
akkaash marked this conversation as resolved.
Show resolved Hide resolved
stageKeys.push({
restApiId,
stageName
});
});

return stageKeys;
}
}
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export * from './deployment';
export * from './stage';
export * from './integrations';
export * from './lambda-api';
export * from './api-key';
export * from './usage-plan';
export * from './usage-plan-key';

// AWS::ApiGateway CloudFormation Resources:
export * from './apigateway.generated';
37 changes: 37 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/usage-plan-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import cdk = require('@aws-cdk/cdk');
import { ApiKey } from './api-key';
import { cloudformation } from './apigateway.generated';
import { UsagePlan } from './usage-plan';

export interface UsagePlanKeyProps {
/**
* Represents the clients to apply a Usage Plan
*/
apiKey: ApiKey,
/**
* Usage Plan to be associated.
*/
usagePlan: UsagePlan
}

/**
* Type of Usage Plan Key. Currently the only supported type is 'API_KEY'
*/
export enum UsagePlanKeyType {
ApiKey = 'API_KEY'
}

/**
* Associates client with an API Gateway Usage Plan.
*/
export class UsagePlanKey extends cdk.Construct {
akkaash marked this conversation as resolved.
Show resolved Hide resolved
constructor(parent: cdk.Construct, name: string, props: UsagePlanKeyProps) {
super(parent, name);

new cloudformation.UsagePlanKeyResource(this, 'Resource', {
keyId: props.apiKey.keyId,
keyType: UsagePlanKeyType.ApiKey,
usagePlanId: props.usagePlan.usagePlanId
});
}
}
176 changes: 176 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import cdk = require('@aws-cdk/cdk');
import { cloudformation } from './apigateway.generated';
import { Method } from './method';
import { IRestApiResource } from './resource';
import { Stage } from './stage';

/**
* Container for defining throttling parameters to API stages or methods.
* See link for more API Gateway's Request Throttling.
*
* @link https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html
*/
export interface ThrottleSettings {
/**
* Represents the steady-state rate for the API stage or method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like numeric fields to carry some form of a unit hint. At the very least, the documentation should mention it. Similar to previously, this probably should be a copy from the documentation in the CloudFormation documentation for the AWS::ApiGateway::UsagePlan resource.

Copy link
Author

@akkaash akkaash Nov 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that a reviewer would point this out tbh with you. I was of the opinion that there may (must) be a way we can enforce certain validation of the inputs as part of these number. For example, burstLimit is required to be an Integer value. If not, the App deployment would fail. Perhaps throwing an error during runtime before cfn is deployed, CDK emit an error with appropriate messaging. Think of it like cdk.ValidateNumber but for Integers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diligent code could validate types beyond what the TypeScript (really, JavaScript) types can represent (Javascript has no concept of an int type, but you can use Number.isInteger to check & do the right thing).

*/
rateLimit?: number
/**
* Represents the burst size (i.e. maximum bucket size) for the API stage or method.
*/
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 {
/**
* Maximum number of requests that can be made in a given time period.
*/
limit?: number,
/**
* Number of requests to reduce from the limit for the first time period.
*/
offset?: number,
/**
* Time period to which the maximum limit applies. Valid values are DAY, WEEK or MONTH.
*/
period?: Period
}

/**
* Represents per-method throttling for a resource.
*/
export interface ThrottlingPerMethod {
method: Method,
throttle: ThrottleSettings
}

/**
* Represents the API stages that a usage plan applies to.
*/
export interface UsagePlanPerApiStage {
api?: IRestApiResource,
stage?: Stage,
throttle?: ThrottlingPerMethod[]
}

export interface UsagePlanProps {
/**
* API Stages to be associated which the usage plan.
*/
apiStages?: UsagePlanPerApiStage[],
/**
* Represents usage plan purpose.
*/
description?: string,
/**
* Number of requests clients can make in a given time period.
*/
quota?: QuotaSettings
/**
* Overall throttle settings for the API.
*/
throttle?: ThrottleSettings,
/**
* Name for this usage plan.
*/
name?: string,
}

export class UsagePlan extends cdk.Construct {
public readonly usagePlanId: string;
constructor(parent: cdk.Construct, name: string, props?: UsagePlanProps) {
akkaash marked this conversation as resolved.
Show resolved Hide resolved
super(parent, name);
let resource: cloudformation.UsagePlanResource;
if (props !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting way of modeling defaults. Should this path also be taken if the user passes {}?

To be honest, this makes me a little uncomfortable. I'd rather you default props to {} and do processing that makes sense in both cases.

const overallThrottle: cloudformation.UsagePlanResource.ThrottleSettingsProperty = this.renderThrottle(props.throttle);
const quota: cloudformation.UsagePlanResource.QuotaSettingsProperty | undefined = this.renderQuota(props);
const apiStages: cloudformation.UsagePlanResource.ApiStageProperty[] | undefined = this.renderApiStages(props);

resource = new cloudformation.UsagePlanResource(this, 'Resource', {
apiStages,
description: props.description,
quota,
throttle: overallThrottle,
usagePlanName: props.name,
});
} else {
resource = new cloudformation.UsagePlanResource(this, 'Resource');
}

this.usagePlanId = resource.ref;
}

private renderApiStages(props: UsagePlanProps): cloudformation.UsagePlanResource.ApiStageProperty[] | undefined {
if (props.apiStages && props.apiStages.length > 0) {
const apiStages: cloudformation.UsagePlanResource.ApiStageProperty[] = [];
props.apiStages.forEach((value: UsagePlanPerApiStage) => {
const apiId = value.api ? value.api.resourceApi.restApiId : undefined;
const stage = value.stage ? value.stage.stageName.toString() : undefined;
const throttle = this.renderThrottlePerMethod(value.throttle);
apiStages.push({
apiId,
stage,
throttle
});
});
return apiStages;
}

return undefined;
}

private renderThrottlePerMethod(throttlePerMethod?: ThrottlingPerMethod[]): {
[key: string]: (cloudformation.UsagePlanResource.ThrottleSettingsProperty | cdk.Token)
} {
const ret: { [key: string]: (cloudformation.UsagePlanResource.ThrottleSettingsProperty | cdk.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.resourcePath}/${method.httpMethod}`;
ret[methodId] = this.renderThrottle(value.throttle);
});
}

return ret;
}

private renderQuota(props: UsagePlanProps): cloudformation.UsagePlanResource.QuotaSettingsProperty | undefined {
if (props.quota === undefined) {
return undefined;
}
return {
limit: props.quota ? props.quota.limit : undefined,
offset: props.quota ? props.quota.offset : undefined,
period: props.quota ? props.quota.period : undefined
};
}

private renderThrottle(throttleSettings?: ThrottleSettings): cloudformation.UsagePlanResource.ThrottleSettingsProperty {
const throttle: cloudformation.UsagePlanResource.ThrottleSettingsProperty = {};
if (throttleSettings !== undefined) {
const burstLimit: number|undefined = throttleSettings.burstLimit;
if (burstLimit) {
if (!Number.isInteger(burstLimit)) {
throw new Error('Throttle burst limit should be an integer');
}
throttle.burstLimit = Number.isInteger(burstLimit) ? burstLimit : undefined;
}
throttle.rateLimit = throttleSettings.rateLimit;
}
return throttle;
}
}
58 changes: 58 additions & 0 deletions packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,64 @@
]
}
}
},
"UsagePlanC18B28F1": {
"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"
},
"Throttle": {
"BurstLimit": 5,
"RateLimit": 50
},
"UsagePlanName": "Basic"
}
},
"ApiKeyF9DDEE66": {
"Type": "AWS::ApiGateway::ApiKey",
"Properties": {
"StageKeys": [
{
"RestApiId": {
"Ref": "myapi4C7BF186"
},
"StageName": {
"Ref": "myapiDeploymentStagebeta96434BEB"
}
}
]
}
},
"UsagePlanKey803D3BF7": {
"Type": "AWS::ApiGateway::UsagePlanKey",
"Properties": {
"KeyId": {
"Ref": "ApiKeyF9DDEE66"
},
"KeyType": "API_KEY",
"UsagePlanId": {
"Ref": "UsagePlanC18B28F1"
}
}
}
},
"Outputs": {
Expand Down
Loading