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 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
92 changes: 92 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,92 @@
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.
* @default none
*/
resources?: IRestApiResource[];

/**
* 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
*/
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
*/
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
*/
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
*/
generateDistinctId?: boolean;

/**
* A name for the API key.
* @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-name
* @default none
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you mean to say "automically generated name" instead of "no name", right?

*/
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 cdk.Construct {
public readonly keyId: string;

constructor(parent: cdk.Construct, id: string, props: ApiKeyProps = {}) {
super(parent, id);

const customerId = props && props.customerId;
Copy link
Contributor

Choose a reason for hiding this comment

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

All these don't need props && since props always has a value.

const description = props && props.description;
const enabled = props && props.enabled;
const generateDistinctId = props && props.generateDistinctId;
const name = props && props.name;
const stageKeys = this.renderStageKeys(props && props.resources);

const resource = new cloudformation.ApiKeyResource(this, 'Resource', {
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
});

this.keyId = resource.ref;
}

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

return resources.map((resource: IRestApiResource) => {
const restApi: RestApi = resource.resourceApi;
const restApiId = restApi.restApiId;
const stageName = restApi.deploymentStage!.stageName.toString();
return { restApiId, stageName };
});
}
}
2 changes: 2 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,8 @@ export * from './deployment';
export * from './stage';
export * from './integrations';
export * from './lambda-api';
export * from './api-key';
export * from './usage-plan';

// AWS::ApiGateway CloudFormation Resources:
export * from './apigateway.generated';
198 changes: 198 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,198 @@
import cdk = require('@aws-cdk/cdk');
import { ApiKey } from './api-key';
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.
* @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)
*/
rateLimit?: number;
Copy link
Contributor

Choose a reason for hiding this comment

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

averageRequestsPerSecond?

avgRequestsPerSec?


/**
* The maximum API request rate limit over a time ranging from one to a few seconds.
*/
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.
*/
limit?: number;

/**
* For the initial time period, the number of requests to subtract from the specified limit.
*/
offset?: number;

/**
* The time period for which the maximum limit of requests applies.
*/
period?: Period;
}

/**
* Represents per-method throttling for a resource.
*/
export interface ThrottlingPerMethod {
method: Method,
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 {
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 = this.renderThrottle(props.throttle);
const quota = this.renderQuota(props);
const apiStages = 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;
}

public addApiKey(apiKey: ApiKey): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs docs

new cloudformation.UsagePlanKeyResource(this, 'UsagePlanKeyResource', {
keyId: apiKey.keyId,
keyType: UsagePlanKeyType.ApiKey,
usagePlanId: this.usagePlanId
});
}

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"
}
},
"UsagePlanUsagePlanKeyResourceFB108041": {
"Type": "AWS::ApiGateway::UsagePlanKey",
"Properties": {
"KeyId": {
"Ref": "ApiKeyF9DDEE66"
},
"KeyType": "API_KEY",
"UsagePlanId": {
"Ref": "UsagePlanC18B28F1"
}
}
},
"ApiKeyF9DDEE66": {
"Type": "AWS::ApiGateway::ApiKey",
"Properties": {
"StageKeys": [
{
"RestApiId": {
"Ref": "myapi4C7BF186"
},
"StageName": {
"Ref": "myapiDeploymentStagebeta96434BEB"
}
}
]
}
}
},
"Outputs": {
Expand Down
Loading