From e36f4a730b70de051b8b35eff655067e97a48114 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 30 Aug 2018 21:56:12 +0300 Subject: [PATCH 01/15] Snapshot --- packages/@aws-cdk/aws-apigateway/lib/body.ts | 3 + .../@aws-cdk/aws-apigateway/lib/deployment.ts | 161 +++++++++++ packages/@aws-cdk/aws-apigateway/lib/index.ts | 9 + .../aws-apigateway/lib/integrations.ts | 4 + .../@aws-cdk/aws-apigateway/lib/method.ts | 105 +++++++ .../@aws-cdk/aws-apigateway/lib/resource.ts | 97 +++++++ .../aws-apigateway/lib/restapi-ref.ts | 33 +++ .../@aws-cdk/aws-apigateway/lib/restapi.ts | 271 ++++++++++++++++++ packages/@aws-cdk/aws-apigateway/lib/stage.ts | 80 ++++++ .../test/integ.restapi.defaults.ts | 13 + .../aws-apigateway/test/integ.restapi.ts | 36 +++ .../aws-apigateway/test/test.apigateway.ts | 195 ++++++++++++- packages/@aws-cdk/cdk/lib/core/construct.ts | 2 +- packages/@aws-cdk/cdk/package-lock.json | 12 +- 14 files changed, 1008 insertions(+), 13 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/body.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/deployment.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/method.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/resource.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/restapi.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/stage.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/body.ts b/packages/@aws-cdk/aws-apigateway/lib/body.ts new file mode 100644 index 0000000000000..0d3b3ae373058 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/body.ts @@ -0,0 +1,3 @@ +export class RestApiBody { + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts new file mode 100644 index 0000000000000..6c931e5b5727d --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -0,0 +1,161 @@ +import cdk = require('@aws-cdk/cdk'); +import crypto = require('crypto'); +import { cloudformation, DeploymentId } from './apigateway.generated'; +import { RestApiRef } from './restapi-ref'; + +export interface CommonDeploymentProps { + /** + * A description of the purpose of the API Gateway deployment. + */ + description?: string; + + /** + * When an API Gateway model is updated, a new deployment will automatically be created. + * If this is true (default), the old API Gateway Deployment resource will not be deleted. + * This will allow manually reverting back to a previous deployment in case for example + * + * @default true + */ + retainDeployments?: boolean; +} + +export interface DeploymentProps extends CommonDeploymentProps { + /** + * The Rest API to deploy. + */ + api: RestApiRef; +} + +/** + * A Deployment of a REST API. + * + * An immutable representation of a RestApi resource that can be called by users + * using Stages. A deployment must be associated with a Stage for it to be + * callable over the Internet. + * + * Normally, you don't need to define deployments manually. The RestApi + * construct manages a Deployment resource that represents the latest model. It + * can be accessed through `restApi.latestDeployment` (unless `deploy: false` is + * set when defining the `RestApi`). + * + * If you manually define this resource, you will need to know that since + * deployments are immutable, as long as the resource's logical ID doesn't + * change, the deployment will represent the snapshot in time in which the + * resource was created. This means that if you modify the RestApi model (i.e. + * add methods or resources), these changes will not be reflected unless a new + * deployment resource is created. + * + * To achieve this behavior, the method `addToLogicalId(data)` can be used to + * augment the logical ID generated for the deployment resource such that it + * will include arbitrary data. This is done automatically for the + * `restApi.latestDeployment` deployment. + * + * Furthermore, since a deployment does not reference any of the REST API + * resources and methods, CloudFormation will likely provision it before these + * resources are created, which means that it will represent a "half-baked" + * model. Use the `addDependency(dep)` method to circumvent that. This is done + * automatically for the `restApi.latestDeployment` deployment. + */ +export class Deployment extends cdk.Construct { + public readonly deploymentId: DeploymentId; + public readonly api: RestApiRef; + + private readonly resource: LatestDeploymentResource; + + constructor(parent: cdk.Construct, id: string, props: DeploymentProps) { + super(parent, id); + + this.resource = new LatestDeploymentResource(this, 'Resource', { + description: props.description, + restApiId: props.api.restApiId, + }); + + if (props.retainDeployments === undefined || props.retainDeployments) { + this.resource.options.deletionPolicy = cdk.DeletionPolicy.Retain; + } + + this.api = props.api; + this.deploymentId = new DeploymentId(() => this.resource.ref); + } + + /** + * Adds a dependency for this deployment. Should be called by all resources and methods + * so they are provisioned before this Deployment. + */ + public addDependency(dep: cdk.IDependable) { + this.resource.addDependency(dep); + } + + /** + * Adds a component to the hash that determines this + * AWS::ApiGateway::Deployment resource's logical ID. This should be called + * by constructs of the API Gateway model that want to invalidate the + * deployment when their settings change. The component will be resolve()ed + * during synthesis so tokens are welcome. + */ + public addToLogicalId(data: any) { + this.resource.addToLogicalId(data); + } +} + +class LatestDeploymentResource extends cloudformation.DeploymentResource { + private originalLogicalId: string; + private customLogicalId?: string; + private hashComponents = new Array(); + + constructor(parent: cdk.Construct, id: string, props: cloudformation.DeploymentResourceProps) { + super(parent, id, props); + + this.originalLogicalId = this.logicalId; + + Object.defineProperties(this, { + logicalId: { + get: () => { + if (!this.customLogicalId) { + throw new Error('The logical ID of this resource cannot be evaluated eagerly. Use: new cdk.Token(() => foo.logicalId)'); + } + return this.customLogicalId; + } + }, + ref: { + get: () => new cdk.Token(() => ({ Ref: this.customLogicalId })) + }, + }); + } + + /** + * Allows adding arbitrary data to the hashed logical ID of this deployment. + * This can be used to couple the deployment to the API Gateway model. + */ + public addToLogicalId(data: unknown) { + + // if the construct is locked, it means we are already synthesizing and then + // we can't modify the hash because we might have already calculated it. + if (this.locked) { + throw new Error('Cannot modify the logical ID when the construct is locked'); + } + + this.hashComponents.push(data); + } + + /** + * Hooks into synthesis to calculate a logical ID that hashes all the components + * add via `addToLogicalId`. + */ + public toCloudFormation() { + // if hash components were added to the deployment, we use them to calculate + // a logical ID for the deployment resource. + if (this.hashComponents.length === 0) { + this.customLogicalId = this.originalLogicalId; + } else { + const md5 = crypto.createHash('md5'); + this.hashComponents + .map(c => cdk.resolve(c)) + .forEach(c => md5.update(JSON.stringify(c))); + + this.customLogicalId = this.originalLogicalId + md5.digest("hex"); + } + + return super.toCloudFormation(); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index 7dbb5e9fe70c8..6e8849a927eb1 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -1,2 +1,11 @@ +export * from './restapi'; +export * from './body'; +export * from './restapi-ref'; +export * from './resource'; +export * from './method'; +export * from './integrations'; +export * from './deployment'; +export * from './stage'; + // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations.ts new file mode 100644 index 0000000000000..b416a70223945 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations.ts @@ -0,0 +1,4 @@ + +export abstract class MethodIntegration { + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts new file mode 100644 index 0000000000000..b744d832c182e --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -0,0 +1,105 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation, MethodId } from './apigateway.generated'; +import { MethodIntegration } from './integrations'; +import { IRestApiResource } from './resource'; + +const ALLOWED_METHODS = [ 'ANY', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ]; + +export interface MethodProps { + /** + * The resource this method is associated with. For root resource methods, + * specify the `RestApi` object. + */ + resource: IRestApiResource; + + /** + * The HTTP method ("GET", "POST", "PUT", ...) that clients use to call this method. + */ + httpMethod: string; + + /** + * The backend system that the method calls when it receives a request. + */ + integration?: MethodIntegration; + + /** + * A friendly operation name for the method. For example, you can assign the + * OperationName of ListPets for the GET /pets method. + */ + operationName?: string; + + /** + * Method authorization. + * Use `MethodAuthorization.None` or `MethodAuthorization.IAM` + * @default None + */ + authorization?: MethodAuthorization; + + /** + * Indicates whether the method requires clients to submit a valid API key. + * @default false + */ + apiKeyRequired?: boolean; + + // TODO: + // - Authorization (AuthorizationType, AuthorizerId) + // - RequestValidatorId + // - RequestModels + // - RequestParameters + // - MethodResponses +} + +export class Method extends cdk.Construct { + public readonly methodId: MethodId; + + constructor(parent: cdk.Construct, id: string, props: MethodProps) { + super(parent, id); + + if (!ALLOWED_METHODS.includes(props.httpMethod.toUpperCase())) { + throw new Error(`Invalid HTTP method "${props.httpMethod}". Allowed methods: ${ALLOWED_METHODS.join(',')}`); + } + + const auth = props.authorization || MethodAuthorization.None; + + const resource = new cloudformation.MethodResource(this, 'Resource', { + resourceId: props.resource.resourceId, + restApiId: props.resource.resourceApi.restApiId, + httpMethod: props.httpMethod, + operationName: props.operationName, + apiKeyRequired: props.apiKeyRequired, + authorizationType: auth.authorizationType, + authorizerId: auth.authorizerId, + integration: { + type: 'MOCK' + } + }); + + this.methodId = resource.ref; + + props.resource.resourceApi._attachMethod(this); + + const deployment = props.resource.resourceApi.latestDeployment; + if (deployment) { + deployment.addDependency(resource); + deployment.addToLogicalId({ + method: { + resourceId: props.resource.resourceId, + httpMethod: props.httpMethod, + operationName: props.operationName, + apiKeyRequired: props.apiKeyRequired, + authorizationType: auth.authorizationType, + authorizerId: auth.authorizerId + } + }); + } + } +} + +export class MethodAuthorization { + public static IAM = new MethodAuthorization('AWS_IAM'); + public static None = new MethodAuthorization('NONE'); + + constructor( + public readonly authorizationType: string, + public readonly authorizerId?: string) { } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts new file mode 100644 index 0000000000000..0a0068ae08016 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -0,0 +1,97 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation, ResourceId } from './apigateway.generated'; +import { Method } from './method'; +import { RestApi } from './restapi'; + +export interface IRestApiResource { + /** + * The rest API that this resource is part of. + * + * The reason we need the RestApi object itself and not just the ID is because the model + * is being tracked by the top-level RestApi object for the purpose of calculating it's + * hash to determine the ID of the deployment. This allows us to automatically update + * the deployment when the model of the REST API changes. + */ + readonly resourceApi: RestApi; + + /** + * The ID of the resource. + */ + readonly resourceId: ResourceId; + + /** + * Defines a new child resource where this resource is the parent. + * @param pathPart The path part for the child resource + * @returns A Resource object + */ + newResource(pathPart: string): Resource; + + /** + * Defines a new method for this resource. + * @param httpMethod The HTTP method + */ + newMethod(httpMethod: string): Method; +} + +export interface ResourceProps { + /** + * The parent resource of this resource. You can either pass another + * `Resource` object or a `RestApi` object here. + */ + parent: IRestApiResource; + + /** + * A path name for the resource. + */ + pathPart: string; +} + +export class Resource extends cdk.Construct implements IRestApiResource { + public readonly resourceApi: RestApi; + public readonly resourceId: ResourceId; + + constructor(parent: cdk.Construct, id: string, props: ResourceProps) { + super(parent, id); + + validateResourcePathPart(props.pathPart); + + const resource = new cloudformation.Resource(this, 'Resource', { + restApiId: props.parent.resourceApi.restApiId, + parentId: props.parent.resourceId, + pathPart: props.pathPart + }); + + this.resourceId = resource.ref; + this.resourceApi = props.parent.resourceApi; + + const deployment = props.parent.resourceApi.latestDeployment; + if (deployment) { + deployment.addDependency(resource); + deployment.addToLogicalId({ + resource: { + resourceId: props.parent.resourceId, + pathPath: props.pathPart + } + }); + } + } + + public newResource(pathPart: string): Resource { + return new Resource(this, pathPart, { parent: this, pathPart }); + } + + public newMethod(httpMethod: string): Method { + return new Method(this, httpMethod, { resource: this, httpMethod }); + } +} + +function validateResourcePathPart(part: string) { + // strip {} which indicate this is a parameter + if (part.startsWith('{') && part.endsWith('}')) { + part = part.substr(1, part.length - 2); + } + + if (!/^[a-zA-Z0-9\.\_\-]+$/.test(part)) { + throw new Error(`Resource's path part only allow a-zA-Z0-9._- and curly braces at the beginning and the end: ${part}`); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts new file mode 100644 index 0000000000000..8b1e9ce93390b --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts @@ -0,0 +1,33 @@ +import cdk = require('@aws-cdk/cdk'); +import { RestApiId } from './apigateway.generated'; + +export interface RestApiRefProps { + restApiId: RestApiId; +} + +export abstract class RestApiRef extends cdk.Construct { + public static import(parent: cdk.Construct, id: string, props: RestApiRefProps) { + return new ImportedRestApi(parent, id, props); + } + + /** + * The ID of this API Gateway RestApi. + */ + public abstract restApiId: RestApiId; + + public export(): RestApiRefProps { + return { + restApiId: new RestApiId(new cdk.Output(this, 'RestApiId', { value: this.restApiId }).makeImportValue()), + }; + } +} + +class ImportedRestApi extends RestApiRef { + public restApiId: RestApiId; + + constructor(parent: cdk.Construct, id: string, props: RestApiRefProps) { + super(parent, id); + + this.restApiId = props.restApiId; + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts new file mode 100644 index 0000000000000..f47e80822d436 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -0,0 +1,271 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation, ResourceId, RestApiId } from './apigateway.generated'; +import { RestApiBody } from './body'; +import { CommonDeploymentProps, Deployment } from './deployment'; +import { Method } from './method'; +import { IRestApiResource, Resource } from './resource'; +import { RestApiRef } from './restapi-ref'; +import { CommonStageProps, Stage } from './stage'; + +export interface RestApiProps { + /** + * Indicates if a Deployment should be automatically created for this API, + * and recreated when the API model (resources, methods) changes. + * + * Since API Gateway deployments are immutable, When this option is enabled + * (by default), an AWS::ApiGateway::Deployment resource will automatically + * created with a logical ID that hashes the API model (methods, resources + * and options). This means that when the model changes, the logical ID of + * this CloudFormation resource will change, and a new deployment will be + * created. + * + * If this is set, `latestDeployment` will refer to the `Deployment` object + * and `deploymentStage` will refer to a `Stage` that points to this + * deployment. To customize the stage options, use the `autoDeployStage` + * property. + * + * @default true + */ + autoDeploy?: boolean; + + /** + * Options for the latest deployment resource. + * @default CommonDeploymentProps defaults + */ + autoDeployOptions?: CommonDeploymentProps; + + /** + * Options for the API Gateway stage that will always point to the latest + * deployment when `autoDeploy` is enabled. If `autoDeploy` is disabled, + * this value cannot be set. + * + * @default CommonStageProps defaults + */ + autoDeployStageOptions?: CommonStageProps; + + /** + * A name for the API Gateway RestApi resource. + * + * @default If this is not specified, and `body` (Open API definition) + * doesn't include a name, the ID of the RestApi construct will be used. + * Since this name doesn't need to be unique, that should be fine. + */ + name?: string; + + /** + * Custom header parameters for the request. + * @see https://docs.aws.amazon.com/cli/latest/reference/apigateway/import-rest-api.html + */ + parameters?: { [key: string]: string }; + + /** + * An OpenAPI specification that defines a set of RESTful APIs. + */ + body?: RestApiBody; + + /** + * A policy document that contains the permissions for this RestApi + */ + policy?: cdk.PolicyDocument; + + /** + * A description of the purpose of this API Gateway RestApi resource. + * @default No description + */ + description?: string; + + /** + * The source of the API key for metering requests according to a usage + * plan. + * @default undefined metering is disabled + */ + apiKeySourceType?: ApiKeySourceType; + + /** + * The list of binary media mine-types that are supported by the RestApi + * resource, such as "image/png" or "application/octet-stream" + * + * @default By default, RestApi supports only UTF-8-encoded text payloads + */ + binaryMediaTypes?: string[]; + + /** + * A list of the endpoint types of the API. Use this property when creating + * an API. + */ + endpointTypes?: EndpointType[]; + + /** + * Indicates whether to roll back the resource if a warning occurs while API + * Gateway is creating the RestApi resource. + * + * @default false + */ + failOnWarnings?: boolean; + + /** + * A nullable integer that is used to enable compression (with non-negative + * between 0 and 10485760 (10M) bytes, inclusive) or disable compression + * (when undefined) on an API. When compression is enabled, compression or + * decompression is not applied on the payload if the payload size is + * smaller than this value. Setting it to zero allows compression for any + * payload size. + * + * @default undefined compression is disabled + */ + minimumCompressionSize?: number; + + /** + * The ID of the API Gateway RestApi resource that you want to clone. + */ + cloneFrom?: RestApiRef; +} + +export class RestApi extends RestApiRef implements IRestApiResource { + /** + * The ID of this API Gateway RestApi. + */ + public restApiId: RestApiId; + + /** + * The ID of the root resource of this RestApi. To be used as a parent for + * all top-level resources. + */ + public resourceId: ResourceId; + + /** + * Points to /this/ RestApi. + */ + public resourceApi: RestApi; + + /** + * API Gateway deployment that represents the latest changes of the API. + * This resource will be automatically updated every time the REST API model changes. + * This will be undefined if `autoDeploy` is false. + */ + public latestDeployment?: Deployment; + + /** + * API Gateway stage that points to the latest deployment (if defined). + * This will be undefined if `autoDeploy` is false. + */ + public deploymentStage?: Stage; + + private readonly methods = new Array(); + + constructor(parent: cdk.Construct, id: string, props: RestApiProps = { }) { + super(parent, id); + + // if 'body' (open api definition) is defined, it's okay for name to be undefined + // otherwise, use the construct id as name (there are no restrictions on name, so that should be fine) + const name = props.body ? props.name : id; + + const bodyProps = this.renderBody(props.body); + + const resource = new cloudformation.RestApiResource(this, 'Resource', { + restApiName: name, + description: props.description, + policy: props.policy, + failOnWarnings: props.failOnWarnings, + minimumCompressionSize: props.minimumCompressionSize, + binaryMediaTypes: props.binaryMediaTypes, + endpointConfiguration: props.endpointTypes ? { types: props.endpointTypes } : undefined, + apiKeySourceType: props.apiKeySourceType, + cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, + parameters: props.parameters, + body: bodyProps && bodyProps.body, + bodyS3Location: bodyProps && bodyProps.bodyS3Location, + }); + + this.restApiId = resource.ref; + this.resourceId = new ResourceId(resource.restApiRootResourceId); // they are the same + this.resourceApi = this; + + this.configureAutoDeploy(props); + + // TODO - determine which field of RestApi need to be added to the hash + // of the Deployment resource - which are part of the model? + } + + public newResource(pathPart: string): Resource { + return new Resource(this, pathPart, { parent: this, pathPart }); + } + + public newMethod(httpMethod: string): Method { + return new Method(this, httpMethod, { resource: this, httpMethod }); + } + + public _attachMethod(method: Method) { + this.methods.push(method); + } + + public validate() { + if (this.methods.length === 0) { + return [ `The REST API doesn't contain any methods` ]; + } + + return []; + } + + private configureAutoDeploy(props: RestApiProps) { + const autoDeploy = props.autoDeploy === undefined ? true : props.autoDeploy; + if (autoDeploy) { + this.latestDeployment = new Deployment(this, 'LatestDeployment', { + api: this, + ...props.autoDeployOptions + }); + + this.deploymentStage = new Stage(this, 'DeploymentStage', { + deployment: this.latestDeployment, + ...props.autoDeployStageOptions + }); + } else { + if (props.autoDeployStageOptions) { + throw new Error(`Cannot set 'autoDeployStageOptions' if 'autoDeploy' is disabled`); + } + if (props.autoDeployOptions) { + throw new Error(`Cannot set 'autoDeployOptions' if 'autoDeploy' is disabled`); + } + } + } + + private renderBody(body?: RestApiBody): { body?: object, bodyS3Location?: cloudformation.RestApiResource.S3LocationProperty } | undefined { + if (!body) { + return undefined; + } + + return { + body: { }, + bodyS3Location : { } + }; + } +} + +export enum ApiKeySourceType { + /** + * To read the API key from the `X-API-Key` header of a request. + */ + Header = 'HEADER', + + /** + * To read the API key from the `UsageIdentifierKey` from a custom authorizer. + */ + Authorizer = 'AUTHORIZER', +} + +export enum EndpointType { + /** + * For an edge-optimized API and its custom domain name. + */ + Edge = 'EDGE', + + /** + * For a regional API and its custom domain name. + */ + Regional = 'REGIONAL', + + /** + * For a private API and its custom domain name. + */ + Private = 'PRIVATE' +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts new file mode 100644 index 0000000000000..5611b7f9974e9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -0,0 +1,80 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation, StageName } from './apigateway.generated'; +import { Deployment } from './deployment'; + +export interface CommonStageProps { + /** + * The name of the stage, which API Gateway uses as the first path segment + * in the invoked Uniform Resource Identifier (URI). + * + * @default "prod" + */ + stageName?: string; + + /** + * Indicates whether cache clustering is enabled for the stage. + */ + cacheClusterEnabled?: boolean; + + /** + * The stage's cache cluster size. + */ + cacheClusterSize?: string; + + /** + * The identifier of the client certificate that API Gateway uses to call + * your integration endpoints in the stage. + * + * @default None + */ + clientCertificateId?: string; + + /** + * A description of the purpose of the stage. + */ + description?: string; + + /** + * The version identifier of the API documentation snapshot. + */ + documentationVersion?: string; + + /** + * A map that defines the stage variables. Variable names must consist of + * alphanumeric characters, and the values must match the following regular + * expression: [A-Za-z0-9-._~:/?#&=,]+. + */ + variables?: { [key: string]: string }; + + // TODO: + // - MethodSettings +} + +export interface StageProps extends CommonStageProps { + /** + * The deployment that this stage points to. + */ + deployment: Deployment; +} + +export class Stage extends cdk.Construct { + public readonly stageName: StageName; + + constructor(parent: cdk.Construct, id: string, props: StageProps) { + super(parent, id); + + const resource = new cloudformation.StageResource(this, 'Resource', { + stageName: props.stageName || 'prod', + cacheClusterEnabled: props.cacheClusterEnabled, + cacheClusterSize: props.cacheClusterSize, + clientCertificateId: props.clientCertificateId, + deploymentId: props.deployment.deploymentId, + restApiId: props.deployment.api.restApiId, + description: props.description, + documentationVersion: props.documentationVersion, + variables: props.variables, + }); + + this.stageName = resource.ref; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts new file mode 100644 index 0000000000000..7e01f54b58d0c --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts @@ -0,0 +1,13 @@ +import cdk = require('@aws-cdk/cdk'); +import apigateway = require('../lib'); + +const app = new cdk.App(process.argv); + +const stack = new cdk.Stack(app, 'test-apigateway-restapi-defaults'); + +const api = new apigateway.RestApi(stack, 'my-api'); + +// at least one method is required +api.newMethod('GET'); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts new file mode 100644 index 0000000000000..e92ee08879b5f --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -0,0 +1,36 @@ +import cdk = require('@aws-cdk/cdk'); +import apigateway = require('../lib'); + +class Test extends cdk.Stack { + constructor(parent: cdk.App, id: string) { + super(parent, id); + + const api = new apigateway.RestApi(this, 'my-api', { + minimumCompressionSize: 0, + autoDeployStageOptions: { + stageName: 'test', + description: 'testing stage' + } + }); + + const v1 = api.newResource('api'); + + const toys = v1.newResource('toys'); + toys.newMethod('GET'); + toys.newMethod('POST'); + toys.newMethod('PUT'); + + const appliances = v1.newResource('appliances'); + appliances.newMethod('GET'); + + const books = v1.newResource('books'); + books.newMethod('GET'); + books.newMethod('POST'); + } +} + +const app = new cdk.App(process.argv); + +new Test(app, 'test-apigateway-restapi'); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts b/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts index db4c843199541..79793ebd5d49d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts @@ -1,8 +1,191 @@ -import { Test, testCase } from 'nodeunit'; +import { expect } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); +import { Stack, App } from '@aws-cdk/cdk'; -exports = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); +// tslint:disable:max-line-length +// tslint:disable:object-literal-key-quotes + +export = { + '"name" is defaulted to construct id'(test: Test) { + const stack = new cdk.Stack(); + new apigateway.RestApi(stack, 'my-first-api', { autoDeploy: false }); + expect(stack).toMatch({ + "Resources": { + "myfirstapi5827A5AA": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "my-first-api" + } + } + } + }); + + test.done(); + }, + + '"name" can be undefined if "body" is specified'(test: Test) { + const stack = new cdk.Stack(); + new apigateway.RestApi(stack, 'bla', { + autoDeploy: false, + body: new apigateway.RestApiBody() + }); + expect(stack).toMatch({ + "Resources": { + "blaBE223B94": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": {}, + "BodyS3Location": {} + } + } + } + }); + test.done(); + }, + + 'minimal setup (just a name)'(test: Test) { + const stack = new cdk.Stack(); + + new apigateway.RestApi(stack, 'my-api'); + + expect(stack).toMatch({ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "my-api" + } + }, + "myapiLatestDeployment24E142F7": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + } + }, + "DeletionPolicy": "Retain" + }, + "myapiDeploymentStage252BF8C8": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiLatestDeployment24E142F7" + }, + "StageName": "prod" + } + } + } + }); + + test.done(); + }, + + 'fails in synthesis if there are no methods'(test: Test) { + const app = new App(); + const stack = new Stack(app, 'my-stack'); + + const api = new apigateway.RestApi(stack, 'API'); + + api.newResource('foo'); + api.newResource('bar').newResource('goo'); + + test.throws(() => app.synthesizeStack(stack.name), /The REST API doesn't contain any methods/); + test.done(); + }, + + 'newChildResource can be used on IRestApiResource to form a tree'(test: Test) { + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'restapi', { + autoDeploy: false, + name: 'my-rest-api' + }); + + const foo = api.newResource('foo'); + api.newResource('bar'); + + foo.newResource('{hello}'); + + expect(stack).toMatch({ + "Resources": { + "restapiC5611D27": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "restapi" + } + }, + "restapifooF697E056": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "restapiC5611D27", + "RootResourceId" + ] + }, + "PathPart": "foo", + "RestApiId": { + "Ref": "restapiC5611D27" + } + } + }, + "restapifoohello6E7449A9": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "restapifooF697E056" + }, + "PathPart": "{hello}", + "RestApiId": { + "Ref": "restapiC5611D27" + } + } + }, + "restapibar1F6A2522": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "restapiC5611D27", + "RootResourceId" + ] + }, + "PathPart": "bar", + "RestApiId": { + "Ref": "restapiC5611D27" + } + } + } + } + }); + + test.done(); + }, + + 'resource path cannot use "/"'(test: Test) { + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'restapi', { name: 'my-rest-api' }); + test.throws(() => api.newResource('foo/')); + test.done(); + }, + + 'fails if autoDeployStageOptions is set with autoDeploy disabled'(test: Test) { + const stack = new cdk.Stack(); + test.throws(() => { + new apigateway.RestApi(stack, 'myapi', { autoDeploy: false, autoDeployStageOptions: { stageName: 'foo' }}); + }, `Cannot set 'autoDeployStageOptions' if 'autoDeploy' is disabled`); + test.done(); + }, + + 'fails if autoDeployOptions is set with autoDeploy disabled'(test: Test) { + const stack = new cdk.Stack(); + test.throws(() => { + new apigateway.RestApi(stack, 'myapi', { autoDeploy: false, autoDeployOptions: { retainDeployments: false }}); + }, `Cannot set 'autoDeployOptions' if 'autoDeploy' is disabled`); test.done(); - } -}); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 16fd813a89130..1c70beddf6c84 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -376,7 +376,7 @@ export class Construct { * Returns true if this construct or any of it's parent constructs are * locked. */ - private get locked() { + protected get locked() { if (this._locked) { return true; } diff --git a/packages/@aws-cdk/cdk/package-lock.json b/packages/@aws-cdk/cdk/package-lock.json index 59b67394eae31..5d9a20916b9c9 100644 --- a/packages/@aws-cdk/cdk/package-lock.json +++ b/packages/@aws-cdk/cdk/package-lock.json @@ -12,7 +12,7 @@ "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-0.1.7.tgz", "integrity": "sha1-rcMgD6RxzCEbDaf1ZrcemLnWc0c=", "requires": { - "es5-ext": "0.8.2" + "es5-ext": "0.8.x" } }, "difflib": { @@ -20,7 +20,7 @@ "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", "integrity": "sha1-teMDYabbAjF21WKJLbhZQKcY9H4=", "requires": { - "heap": "0.2.6" + "heap": ">= 0.2.0" } }, "dreamopt": { @@ -28,7 +28,7 @@ "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.6.0.tgz", "integrity": "sha1-2BPM2sjTnYrVJndVFKE92mZNa0s=", "requires": { - "wordwrap": "1.0.0" + "wordwrap": ">=0.0.2" } }, "es5-ext": { @@ -51,9 +51,9 @@ "resolved": "https://registry.npmjs.org/json-diff/-/json-diff-0.3.1.tgz", "integrity": "sha1-bbw64tJeB1p/1xvNmHRFhmb7aBs=", "requires": { - "cli-color": "0.1.7", - "difflib": "0.2.4", - "dreamopt": "0.6.0" + "cli-color": "~0.1.6", + "difflib": "~0.2.1", + "dreamopt": "~0.6.0" } }, "wordwrap": { From 46c832ad6e0b6ecb1348ebd9ebf3558ac01231d1 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 3 Sep 2018 16:38:08 +0300 Subject: [PATCH 02/15] Lambda integration works --- .../aws-apigateway/lib/integrations.ts | 338 ++++++++++++++++++ .../@aws-cdk/aws-apigateway/lib/method.ts | 134 +++++-- .../@aws-cdk/aws-apigateway/lib/resource.ts | 23 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 46 ++- packages/@aws-cdk/aws-apigateway/lib/stage.ts | 132 ++++++- packages/@aws-cdk/aws-apigateway/lib/util.ts | 69 ++++ packages/@aws-cdk/aws-apigateway/package.json | 4 +- .../test/integ.restapi.defaults.ts | 2 +- .../aws-apigateway/test/integ.restapi.ts | 55 ++- .../aws-apigateway/test/test.apigateway.ts | 35 +- .../@aws-cdk/aws-apigateway/test/test.util.ts | 68 ++++ .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 17 +- .../@aws-cdk/cdk/test/core/test.tokens.ts | 23 +- 13 files changed, 875 insertions(+), 71 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/util.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.util.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations.ts index b416a70223945..13a3d8f27b1ce 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations.ts @@ -1,4 +1,342 @@ +import iam = require('@aws-cdk/aws-iam'); +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { Method } from './method'; +import { parseAwsApiCall } from './util'; + +export interface IntegrationOptions { + + /** + * A list of request parameters whose values API Gateway caches. + */ + cacheKeyParameters?: string[]; + + /** + * An API-specific tag group of related cached parameters. + */ + cacheNamespace?: string; + + /** + * Specifies how to handle request payload content type conversions. + * @default If this property isn't defined, the request payload is passed + * through from the method request to the integration request without + * modification, provided that the `passthroughBehaviors` property is + * configured to support payload pass-through. + */ + contentHandling?: ContentHandling; + + /** + * An IAM role that API Gateway assumes. + * + * Mutually exclusive with `credentialsPassThrough`. + * + * @default A role is not assumed + */ + credentialsRole?: iam.Role; + + /** + * Requires that the caller's identity be passed through from the request. + * + * @default Caller identity is not passed through + */ + credentialsPassthrough?: boolean; + + /** + * Specifies the pass-through behavior for incoming requests based on the + * Content-Type header in the request, and the available mapping templates + * specified as the requestTemplates property on the Integration resource. + * There are three valid values: WHEN_NO_MATCH, WHEN_NO_TEMPLATES, and + * NEVER. + */ + passthroughBehavior?: PassthroughBehavior + + /** + * The request parameters that API Gateway sends with the backend request. + * Specify request parameters as key-value pairs (string-to-string + * mappings), with a destination as the key and a source as the value. + * + * Specify the destination by using the following pattern + * integration.request.location.name, where location is querystring, path, + * or header, and name is a valid, unique parameter name. + * + * The source must be an existing method request parameter or a static + * value. You must enclose static values in single quotation marks and + * pre-encode these values based on their destination in the request. + */ + requestParameters?: { [dest: string]: string }; + + /** + * A map of Apache Velocity templates that are applied on the request + * payload. The template that API Gateway uses is based on the value of the + * Content-Type header that's sent by the client. The content type value is + * the key, and the template is the value (specified as a string), such as + * the following snippet: + * + * { "application/json": "{\n \"statusCode\": \"200\"\n}" } + * + * @see http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html + */ + requestTemplates?: { [contentType: string]: string }; + + // TODO: + // - IntegrationResponses + // +} + +export interface IntegrationProps { + /** + * Specifies an API method integration type. + */ + type: IntegrationType; + + /** + * The Uniform Resource Identifier (URI) for the integration. + * + * - If you specify MOCK for the `type` property, this is not required. + * - If you specify HTTP for the `type` property, specify the API endpoint URL. + * - If you specify MOCK for the `type` property, don't specify this property. + * - If you specify AWS for the `type` property, specify an AWS service that + * follows this form: + * `arn:aws:apigateway:region:subdomain.service|service:path|action/service_api.` + * For example, a Lambda function URI follows this form: + * arn:aws:apigateway:region:lambda:path/path. The path is usually in the + * form /2015-03-31/functions/LambdaFunctionARN/invocations. + * + * @see https://docs.aws.amazon.com/apigateway/api-reference/resource/integration/#uri + */ + uri?: any; + + /** + * The integration's HTTP method type. + * Required unless you use a MOCK integration. + */ + integrationHttpMethod?: string; + + /** + * Integration options. + */ + options?: IntegrationOptions; +} export abstract class MethodIntegration { + constructor(readonly props: IntegrationProps) { + + } + + public attachToMethod(_method: Method) { + return; + } +} + +export class MockMethodIntegration extends MethodIntegration { + constructor(options?: IntegrationOptions) { + super({ + type: IntegrationType.Mock, + options + }); + } +} + +export enum AwsApiType { + Path = 'path', + Action = 'action' +} + +export interface AwsIntegrationProps { + /** + * Use AWS_PROXY integration. + * + * @default false + */ + proxy?: boolean; + + /** + * The name of the integrated AWS service (e.g. `s3`) + */ + service: string; + + /** + * A designated subdomain supported by certain AWS service for fast + * host-name lookup. + */ + subdomain?: string; + + /** + * The path to use for path-base APIs. + * + * For example, for S3 GET, you can set path to `bucket/key`. + * For lambda, you can set path to `2015-03-31/functions/${function-arn}/invocations` + * + * Mutually exclusive with the `action` options. + */ + path?: string; + + /** + * The AWS action to perform in the integration. + * + * Use `actionParams` to specify key-value params for the action. + * + * Mutually exclusive with `path`. + */ + action?: string; + + /** + * Parameters for the action. + * + * `action` must be set, and `path` must be undefined. + * The action params will be URL encoded. + */ + actionParameters?: { [key: string]: string }; + + /** + * Integration options. + */ + options?: IntegrationOptions +} + +export class AwsIntegration extends MethodIntegration { + constructor(props: AwsIntegrationProps) { + const backend = props.subdomain ? `${props.subdomain}.${props.service}` : props.service; + const type = props.proxy ? IntegrationType.AwsProxy : IntegrationType.Aws; + const { apiType, apiValue } = parseAwsApiCall(props.path, props.action, props.actionParameters); + super({ + type, + integrationHttpMethod: 'POST', + uri: cdk.Arn.fromComponents({ + service: 'apigateway', + account: backend, + resource: apiType, + sep: '/', + resourceName: apiValue, + }), + options: props.options, + }); + } +} + +export interface LambdaIntegrationOptions extends IntegrationOptions { + /** + * Use proxy integration or normal (request/response mapping) integration. + * @default true + */ + proxy?: boolean; + + /** + * Allow invoking method from AWS Console UI (for testing purposes). + * + * This will add another permission to the AWS Lambda resource policy which + * will allow the `test-invoke-stage` stage to invoke this handler. If this + * is set to `false`, the function will only be usable from the deployment + * endpoint. + * + * @default true + */ + enableTestInvoke?: boolean; +} + +export class LambdaMethodIntegration extends AwsIntegration { + private readonly handler: lambda.FunctionRef; + private readonly enableTestInvoke: boolean; + + constructor(handler: lambda.FunctionRef, options: LambdaIntegrationOptions = { }) { + const proxy = options.proxy === undefined ? true : options.proxy; + + super({ + proxy, + service: 'lambda', + path: `2015-03-31/functions/${handler.functionArn}/invocations`, + options + }); + + this.handler = handler; + this.enableTestInvoke = options.enableTestInvoke === undefined ? true : false; + } + + public attachToMethod(method: Method) { + const principal = new cdk.ServicePrincipal('apigateway.amazonaws.com'); + + this.handler.addPermission(method.methodArn.toString(), { + principal, + sourceArn: method.methodArn + }); + + // add permission to invoke from the console + if (this.enableTestInvoke) { + this.handler.addPermission(method.testMethodArn.toString(), { + principal, + sourceArn: method.testMethodArn + }); + } + } +} + +export enum ContentHandling { + /** + * Converts a request payload from a base64-encoded string to a binary blob. + */ + ConvertToBinary = 'CONVERT_TO_BINARY', + + /** + * Converts a request payload from a binary blob to a base64-encoded string. + */ + ConvertToText = 'CONVERT_TO_TEXT' +} + +export enum IntegrationType { + /** + * For integrating the API method request with an AWS service action, + * including the Lambda function-invoking action. With the Lambda + * function-invoking action, this is referred to as the Lambda custom + * integration. With any other AWS service action, this is known as AWS + * integration. + */ + Aws = 'AWS', + + /** + * For integrating the API method request with the Lambda function-invoking + * action with the client request passed through as-is. This integration is + * also referred to as the Lambda proxy integration + */ + AwsProxy = 'AWS_PROXY', + + /** + * For integrating the API method request with an HTTP endpoint, including a + * private HTTP endpoint within a VPC. This integration is also referred to + * as the HTTP custom integration. + */ + Http = 'HTTP', + + /** + * For integrating the API method request with an HTTP endpoint, including a + * private HTTP endpoint within a VPC, with the client request passed + * through as-is. This is also referred to as the HTTP proxy integration + */ + HttpProxy = 'HTTP_PROXY', + + /** + * For integrating the API method request with API Gateway as a "loop-back" + * endpoint without invoking any backend. + */ + Mock = 'MOCK' +} + +export enum PassthroughBehavior { + /** + * Passes the request body for unmapped content types through to the + * integration back end without transformation. + */ + WhenNoMatch = 'WHEN_NO_MATCH', + + /** + * Rejects unmapped content types with an HTTP 415 'Unsupported Media Type' + * response + */ + Never = 'NEVER', + /** + * Allows pass-through when the integration has NO content types mapped to + * templates. However if there is at least one content type defined, + * unmapped content types will be rejected with the same 415 response. + */ + WhenNoTemplates = 'WHEN_NO_TEMPLATES' } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index b744d832c182e..5289a480eccc1 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -1,21 +1,11 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation, MethodId } from './apigateway.generated'; -import { MethodIntegration } from './integrations'; +import { MethodIntegration, MockMethodIntegration } from './integrations'; import { IRestApiResource } from './resource'; +import { RestApi } from './restapi'; +import { validateHttpMethod } from './util'; -const ALLOWED_METHODS = [ 'ANY', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ]; - -export interface MethodProps { - /** - * The resource this method is associated with. For root resource methods, - * specify the `RestApi` object. - */ - resource: IRestApiResource; - - /** - * The HTTP method ("GET", "POST", "PUT", ...) that clients use to call this method. - */ - httpMethod: string; +export interface MethodOptions { /** * The backend system that the method calls when it receives a request. @@ -30,7 +20,6 @@ export interface MethodProps { /** * Method authorization. - * Use `MethodAuthorization.None` or `MethodAuthorization.IAM` * @default None */ authorization?: MethodAuthorization; @@ -49,29 +38,52 @@ export interface MethodProps { // - MethodResponses } +export interface MethodProps { + /** + * The resource this method is associated with. For root resource methods, + * specify the `RestApi` object. + */ + resource: IRestApiResource; + + /** + * The HTTP method ("GET", "POST", "PUT", ...) that clients use to call this method. + */ + httpMethod: string; + + /** + * Method options. + */ + options?: MethodOptions; +} + export class Method extends cdk.Construct { public readonly methodId: MethodId; + private readonly resource: IRestApiResource; + private readonly restApi: RestApi; + private readonly httpMethod: string; + constructor(parent: cdk.Construct, id: string, props: MethodProps) { super(parent, id); - if (!ALLOWED_METHODS.includes(props.httpMethod.toUpperCase())) { - throw new Error(`Invalid HTTP method "${props.httpMethod}". Allowed methods: ${ALLOWED_METHODS.join(',')}`); - } + this.resource = props.resource; + this.restApi = props.resource.resourceApi; + this.httpMethod = props.httpMethod; + + validateHttpMethod(this.httpMethod); - const auth = props.authorization || MethodAuthorization.None; + const options = props.options || { }; + const auth = options.authorization || MethodAuthorization.None; const resource = new cloudformation.MethodResource(this, 'Resource', { resourceId: props.resource.resourceId, - restApiId: props.resource.resourceApi.restApiId, + restApiId: this.restApi.restApiId, httpMethod: props.httpMethod, - operationName: props.operationName, - apiKeyRequired: props.apiKeyRequired, + operationName: options.operationName, + apiKeyRequired: options.apiKeyRequired, authorizationType: auth.authorizationType, authorizerId: auth.authorizerId, - integration: { - type: 'MOCK' - } + integration: this.renderIntegration(options.integration), }); this.methodId = resource.ref; @@ -85,14 +97,82 @@ export class Method extends cdk.Construct { method: { resourceId: props.resource.resourceId, httpMethod: props.httpMethod, - operationName: props.operationName, - apiKeyRequired: props.apiKeyRequired, + operationName: options.operationName, + apiKeyRequired: options.apiKeyRequired, authorizationType: auth.authorizationType, authorizerId: auth.authorizerId } }); } } + + /** + * Returns an execute-api ARN for this method: + * + * arn:aws:execute-api:{region}:{account}:{restApiId}/{stage}/{method}/{path} + * + * NOTE: {stage} will refer to the `restApi.deploymentStage`, which will + * automatically set if auto-deploy is enabled. + */ + public get methodArn(): cdk.Arn { + if (!this.restApi.deploymentStage) { + throw new Error('There is no stage associated with this restApi. Either use `autoDeploy` or explicitly assign `deploymentStage`'); + } + + return this.methodArnForStage(this.restApi.deploymentStage.stageName.toString()); + } + + /** + * Returns an execute-api ARN for this method's "test-invoke-stage" stage. + * This stage is used by the AWS Console UI when testing the method. + */ + public get testMethodArn(): cdk.Arn { + return this.methodArnForStage('test-invoke-stage'); + } + + private methodArnForStage(stage: string) { + return cdk.Arn.fromComponents({ + service: 'execute-api', + resource: this.restApi.restApiId, + sep: '/', + resourceName: `${stage}/${this.httpMethod}${this.resource.resourcePath}` + }); + } + + private renderIntegration(integration?: MethodIntegration): cloudformation.MethodResource.IntegrationProperty { + if (!integration) { + return this.renderIntegration(new MockMethodIntegration()); + } + + integration.attachToMethod(this); + + const options = integration.props.options || { }; + + let credentials; + if (options.credentialsPassthrough && options.credentialsRole) { + throw new Error(`'credentialsPassthrough' and 'credentialsRole' are mutually exclusive`); + } + + if (options.credentialsRole) { + credentials = options.credentialsRole.roleArn; + } else if (options.credentialsPassthrough) { + // arn:aws:iam::*:user/* + credentials = cdk.Arn.fromComponents({ service: 'iam', region: '', account: '*', resource: 'user', sep: '/', resourceName: '*' }); + } + + return { + type: integration.props.type, + uri: integration.props.uri, + cacheKeyParameters: options.cacheKeyParameters, + cacheNamespace: options.cacheNamespace, + contentHandling: options.contentHandling, + integrationHttpMethod: integration.props.integrationHttpMethod, + requestParameters: options.requestParameters, + requestTemplates: options.requestTemplates, + passthroughBehavior: options.passthroughBehavior, + credentials, + }; + } } export class MethodAuthorization { diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 0a0068ae08016..93d0af67bf954 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -1,6 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation, ResourceId } from './apigateway.generated'; -import { Method } from './method'; +import { Method, MethodOptions } from './method'; import { RestApi } from './restapi'; export interface IRestApiResource { @@ -19,18 +19,23 @@ export interface IRestApiResource { */ readonly resourceId: ResourceId; + /** + * The full path of this resuorce. + */ + readonly resourcePath: string; + /** * Defines a new child resource where this resource is the parent. * @param pathPart The path part for the child resource * @returns A Resource object */ - newResource(pathPart: string): Resource; + addResource(pathPart: string): Resource; /** * Defines a new method for this resource. * @param httpMethod The HTTP method */ - newMethod(httpMethod: string): Method; + onMethod(httpMethod: string, options?: MethodOptions): Method; } export interface ResourceProps { @@ -49,6 +54,7 @@ export interface ResourceProps { export class Resource extends cdk.Construct implements IRestApiResource { public readonly resourceApi: RestApi; public readonly resourceId: ResourceId; + public readonly resourcePath: string; constructor(parent: cdk.Construct, id: string, props: ResourceProps) { super(parent, id); @@ -64,6 +70,11 @@ export class Resource extends cdk.Construct implements IRestApiResource { this.resourceId = resource.ref; this.resourceApi = props.parent.resourceApi; + // render resource path (special case for root) + this.resourcePath = props.parent.resourcePath; + if (!this.resourcePath.endsWith('/')) { this.resourcePath += '/'; } + this.resourcePath += props.pathPart; + const deployment = props.parent.resourceApi.latestDeployment; if (deployment) { deployment.addDependency(resource); @@ -76,12 +87,12 @@ export class Resource extends cdk.Construct implements IRestApiResource { } } - public newResource(pathPart: string): Resource { + public addResource(pathPart: string): Resource { return new Resource(this, pathPart, { parent: this, pathPart }); } - public newMethod(httpMethod: string): Method { - return new Method(this, httpMethod, { resource: this, httpMethod }); + public onMethod(httpMethod: string, options?: MethodOptions): Method { + return new Method(this, httpMethod, { resource: this, httpMethod, options }); } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index f47e80822d436..c2bf8dcc396fc 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,3 +1,4 @@ +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { cloudformation, ResourceId, RestApiId } from './apigateway.generated'; import { RestApiBody } from './body'; @@ -5,7 +6,7 @@ import { CommonDeploymentProps, Deployment } from './deployment'; import { Method } from './method'; import { IRestApiResource, Resource } from './resource'; import { RestApiRef } from './restapi-ref'; -import { CommonStageProps, Stage } from './stage'; +import { Stage, StageOptions } from './stage'; export interface RestApiProps { /** @@ -41,7 +42,7 @@ export interface RestApiProps { * * @default CommonStageProps defaults */ - autoDeployStageOptions?: CommonStageProps; + autoDeployStageOptions?: StageOptions; /** * A name for the API Gateway RestApi resource. @@ -125,18 +126,23 @@ export class RestApi extends RestApiRef implements IRestApiResource { /** * The ID of this API Gateway RestApi. */ - public restApiId: RestApiId; + public readonly restApiId: RestApiId; /** * The ID of the root resource of this RestApi. To be used as a parent for * all top-level resources. */ - public resourceId: ResourceId; + public readonly resourceId: ResourceId; /** * Points to /this/ RestApi. */ - public resourceApi: RestApi; + public readonly resourceApi: RestApi; + + /** + * The full path of this resource. + */ + public readonly resourcePath = '/'; /** * API Gateway deployment that represents the latest changes of the API. @@ -147,7 +153,9 @@ export class RestApi extends RestApiRef implements IRestApiResource { /** * API Gateway stage that points to the latest deployment (if defined). - * This will be undefined if `autoDeploy` is false. + * + * If `autoDeploy` is disabled, you will need to explicitly assign this value in order to + * set up integrations. */ public deploymentStage?: Stage; @@ -185,13 +193,15 @@ export class RestApi extends RestApiRef implements IRestApiResource { // TODO - determine which field of RestApi need to be added to the hash // of the Deployment resource - which are part of the model? + + this.configureCloudWatchRole(resource); } - public newResource(pathPart: string): Resource { + public addResource(pathPart: string): Resource { return new Resource(this, pathPart, { parent: this, pathPart }); } - public newMethod(httpMethod: string): Method { + public onMethod(httpMethod: string): Method { return new Method(this, httpMethod, { resource: this, httpMethod }); } @@ -229,6 +239,26 @@ export class RestApi extends RestApiRef implements IRestApiResource { } } + private configureCloudWatchRole(apiResource: cloudformation.RestApiResource) { + const role = new iam.Role(this, 'CloudWatchRole', { + assumedBy: new cdk.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicyArns: [ cdk.Arn.fromComponents({ + service: 'iam', + region: '', + account: 'aws', + resource: 'policy', + sep: '/', + resourceName: 'service-role/AmazonAPIGatewayPushToCloudWatchLogs' + }) ] + }); + + const resource = new cloudformation.AccountResource(this, 'Account', { + cloudWatchRoleArn: role.roleArn + }); + + resource.addDependency(apiResource); + } + private renderBody(body?: RestApiBody): { body?: object, bodyS3Location?: cloudformation.RestApiResource.S3LocationProperty } | undefined { if (!body) { return undefined; diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index 5611b7f9974e9..b968c0f041da6 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -1,8 +1,9 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation, StageName } from './apigateway.generated'; import { Deployment } from './deployment'; +import { parseMethodOptionsPath } from './util'; -export interface CommonStageProps { +export interface StageOptions { /** * The name of the stage, which API Gateway uses as the first path segment * in the invoked Uniform Resource Identifier (URI). @@ -18,6 +19,7 @@ export interface CommonStageProps { /** * The stage's cache cluster size. + * @default 0.5 */ cacheClusterSize?: string; @@ -46,35 +48,155 @@ export interface CommonStageProps { */ variables?: { [key: string]: string }; - // TODO: - // - MethodSettings + /** + * Default deployment options for all methods. You can indicate deployment + * options for specific resources/methods via `customMethodOptions`. + */ + methodOptions?: MethodDeploymentOptions + + /** + * Method deployment options for specific resources/methods. These will + * override common options defined in `StageOptions#methodOptions`. + * + * @param path is {resource_path}/{http_method} (i.e. /api/toys/GET) for an + * individual method override. You can use `*` for both {resource_path} and {http_method} + * to define options for all methods/resources. + */ + + customMethodOptions?: { [path: string]: MethodDeploymentOptions }; } -export interface StageProps extends CommonStageProps { +export interface StageProps extends StageOptions { /** * The deployment that this stage points to. */ deployment: Deployment; } +export enum MethodLoggingLevel { + Off = 'OFF', + Error = 'ERROR', + Info = 'INFO' +} + +export interface MethodDeploymentOptions { + /** + * Specifies whether Amazon CloudWatch metrics are enabled for this method. + * @default false + */ + metricsEnabled?: boolean; + + /** + * Specifies the logging level for this method, which effects the log + * entries pushed to Amazon CloudWatch Logs. + * @default Off + */ + loggingLevel?: MethodLoggingLevel; + + /** + * Specifies whether data trace logging is enabled for this method, which + * effects the log entries pushed to Amazon CloudWatch Logs. + * @default false + */ + dataTraceEnabled?: boolean; + + /** + * Specifies the throttling burst limit. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html + */ + throttlingBurstLimit?: number; + + /** + * Specifies the throttling rate limit. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html + */ + throttlingRateLimit?: number; + + /** + * Specifies whether responses should be cached and returned for requests. A + * cache cluster must be enabled on the stage for responses to be cached. + */ + cachingEnabled?: boolean; + + /** + * Specifies the time to live (TTL), in seconds, for cached responses. The + * higher the TTL, the longer the response will be cached. + */ + cacheTtlSeconds?: number; + + /** + * Indicates whether the cached responses are encrypted. + * @default false + */ + cacheDataEncrypted?: boolean; +} + export class Stage extends cdk.Construct { public readonly stageName: StageName; constructor(parent: cdk.Construct, id: string, props: StageProps) { super(parent, id); + let cacheClusterSize; + if (props.cacheClusterEnabled) { + cacheClusterSize = props.cacheClusterSize || '0.5'; + } else { + if (props.cacheClusterSize) { + throw new Error(`Cannot specify cacheClusterSize if cacheCluster is not enabled`); + } + } + const resource = new cloudformation.StageResource(this, 'Resource', { stageName: props.stageName || 'prod', cacheClusterEnabled: props.cacheClusterEnabled, - cacheClusterSize: props.cacheClusterSize, + cacheClusterSize, clientCertificateId: props.clientCertificateId, deploymentId: props.deployment.deploymentId, restApiId: props.deployment.api.restApiId, description: props.description, documentationVersion: props.documentationVersion, variables: props.variables, + methodSettings: this.renderMethodSettings(props), }); this.stageName = resource.ref; } + + private renderMethodSettings(props: StageProps): cloudformation.StageResource.MethodSettingProperty[] | undefined { + const settings = new Array(); + + if (props.methodOptions) { + settings.push(renderEntry('/*/*', props.methodOptions)); + } + + if (props.customMethodOptions) { + for (const path of Object.keys(props.customMethodOptions)) { + settings.push(renderEntry(path, props.customMethodOptions[path])); + } + } + + return settings.length === 0 ? undefined : settings; + + function renderEntry(path: string, options: MethodDeploymentOptions): cloudformation.StageResource.MethodSettingProperty { + if (options.cachingEnabled) { + if (!props.cacheClusterEnabled) { + throw new Error(`Cannot enable caching for method ${path} since cache cluster is not enabled on stage`); + } + } + + const { httpMethod, resourcePath } = parseMethodOptionsPath(path); + + return { + httpMethod, resourcePath, + cacheDataEncrypted: options.cacheDataEncrypted, + cacheTtlInSeconds: options.cacheTtlSeconds, + cachingEnabled: options.cachingEnabled, + dataTraceEnabled: options.dataTraceEnabled, + loggingLevel: options.loggingLevel, + metricsEnabled: options.metricsEnabled, + throttlingBurstLimit: options.throttlingBurstLimit, + throttlingRateLimit: options.throttlingRateLimit, + }; + } + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/util.ts b/packages/@aws-cdk/aws-apigateway/lib/util.ts new file mode 100644 index 0000000000000..0c2a5ba25b73a --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/util.ts @@ -0,0 +1,69 @@ +import { format as formatUrl } from 'url'; +const ALLOWED_METHODS = [ 'ANY', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ]; + +export function validateHttpMethod(method: string, messagePrefix: string = '') { + if (!ALLOWED_METHODS.includes(method.toUpperCase())) { + throw new Error(`${messagePrefix}Invalid HTTP method "${method}". Allowed methods: ${ALLOWED_METHODS.join(',')}`); + } +} + +export function parseMethodOptionsPath(originalPath: string): { resourcePath: string, httpMethod: string } { + if (!originalPath.startsWith('/')) { + throw new Error(`Method options path must start with '/': ${originalPath}`); + } + + const path = originalPath.substr(1); // trim trailing '/' + + const components = path.split('/'); + + if (components.length < 2) { + throw new Error(`Method options path must include at least two components: /{resource}/{method} (i.e. /foo/bar/GET): ${path}`); + } + + const httpMethod = components.pop()!.toUpperCase(); // last component is an HTTP method + if (httpMethod !== '*') { + validateHttpMethod(httpMethod, `${originalPath}: `); + } + + let resourcePath = '/~1' + components.join('~1'); + if (components.length === 1 && components[0] === '*') { + resourcePath = '/*'; + } else if (components.length === 1 && components[0] === '') { + resourcePath = '/'; + } + + return { + httpMethod, + resourcePath + }; +} + +export function parseAwsApiCall(path?: string, action?: string, actionParams?: { [key: string]: string }): { apiType: string, apiValue: string } { + if (actionParams && !action) { + throw new Error(`"actionParams" requires that "action" will be set`); + } + + if (path && action) { + throw new Error(`"path" and "action" are mutually exclusive (path="${path}", action="${action}")`); + } + + if (path) { + return { + apiType: 'path', + apiValue: path + }; + } + + if (action) { + if (actionParams) { + action += '&' + formatUrl({ query: actionParams }).substr(1); + } + + return { + apiType: 'action', + apiValue: action + }; + } + + throw new Error(`Either "path" or "action" are required`); +} diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 5670f75631835..c1faaf25070ee 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -52,7 +52,9 @@ "pkglint": "^0.8.2" }, "dependencies": { - "@aws-cdk/cdk": "^0.8.2" + "@aws-cdk/cdk": "^0.8.2", + "@aws-cdk/aws-iam": "^0.8.2", + "@aws-cdk/aws-lambda": "^0.8.2" }, "homepage": "https://github.com/awslabs/aws-cdk" } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts index 7e01f54b58d0c..ce9f182480901 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts @@ -8,6 +8,6 @@ const stack = new cdk.Stack(app, 'test-apigateway-restapi-defaults'); const api = new apigateway.RestApi(stack, 'my-api'); // at least one method is required -api.newMethod('GET'); +api.onMethod('GET'); process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index e92ee08879b5f..55e82530b6e8e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -1,3 +1,4 @@ +import lambda = require('@aws-cdk/aws-lambda'); import cdk = require('@aws-cdk/cdk'); import apigateway = require('../lib'); @@ -8,24 +9,56 @@ class Test extends cdk.Stack { const api = new apigateway.RestApi(this, 'my-api', { minimumCompressionSize: 0, autoDeployStageOptions: { + cacheClusterEnabled: true, stageName: 'test', - description: 'testing stage' + description: 'testing stage', + methodOptions: { + loggingLevel: apigateway.MethodLoggingLevel.Info, + dataTraceEnabled: true + }, + customMethodOptions: { + '/api/appliances/GET': { + cachingEnabled: true + } + } } }); - const v1 = api.newResource('api'); + const handler = new lambda.Function(this, 'MyHandler', { + runtime: lambda.Runtime.NodeJS610, + code: lambda.Code.inline(`exports.handler = ${handlerCode}`), + handler: 'index.handler', + }); + + const v1 = api.addResource('api'); + + const toys = v1.addResource('toys'); + + toys.onMethod('GET', { + integration: new apigateway.LambdaMethodIntegration(handler) + }); + + toys.onMethod('POST'); + toys.onMethod('PUT'); + + const appliances = v1.addResource('appliances'); + appliances.onMethod('GET'); - const toys = v1.newResource('toys'); - toys.newMethod('GET'); - toys.newMethod('POST'); - toys.newMethod('PUT'); + const books = v1.addResource('books'); + books.onMethod('GET'); + books.onMethod('POST'); - const appliances = v1.newResource('appliances'); - appliances.newMethod('GET'); + function handlerCode(event: any, _: any, callback: any) { + // tslint:disable-next-line:no-console + console.log(JSON.stringify(event, undefined, 2)); - const books = v1.newResource('books'); - books.newMethod('GET'); - books.newMethod('POST'); + return callback(undefined, { + isBase64Encoded: false, + statusCode: 200, + headers: { }, + body: 'hi' + }); + } } } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts b/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts index 79793ebd5d49d..65362b4156a5e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts @@ -1,8 +1,8 @@ import { expect } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); +import { App, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import apigateway = require('../lib'); -import { Stack, App } from '@aws-cdk/cdk'; // tslint:disable:max-line-length // tslint:disable:object-literal-key-quotes @@ -91,8 +91,8 @@ export = { const api = new apigateway.RestApi(stack, 'API'); - api.newResource('foo'); - api.newResource('bar').newResource('goo'); + api.addResource('foo'); + api.addResource('bar').addResource('goo'); test.throws(() => app.synthesizeStack(stack.name), /The REST API doesn't contain any methods/); test.done(); @@ -105,10 +105,10 @@ export = { name: 'my-rest-api' }); - const foo = api.newResource('foo'); - api.newResource('bar'); + const foo = api.addResource('foo'); + api.addResource('bar'); - foo.newResource('{hello}'); + foo.addResource('{hello}'); expect(stack).toMatch({ "Resources": { @@ -166,10 +166,31 @@ export = { test.done(); }, + 'resourcePath returns the full path of the resource within the API'(test: Test) { + const stack = new cdk.Stack(); + + const api = new apigateway.RestApi(stack, 'restapi'); + + const r1 = api.addResource('r1'); + const r11 = r1.addResource('r1_1'); + const r12 = r1.addResource('r1_2'); + const r121 = r12.addResource('r1_2_1'); + const r2 = api.addResource('r2'); + + test.deepEqual(api.resourcePath, '/'); + test.deepEqual(r1.resourcePath, '/r1'); + test.deepEqual(r11.resourcePath, '/r1/r1_1'); + test.deepEqual(r12.resourcePath, '/r1/r1_2'); + test.deepEqual(r121.resourcePath, '/r1/r1_2/r1_2_1'); + test.deepEqual(r2.resourcePath, '/r2'); + + test.done(); + }, + 'resource path cannot use "/"'(test: Test) { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'restapi', { name: 'my-rest-api' }); - test.throws(() => api.newResource('foo/')); + test.throws(() => api.addResource('foo/')); test.done(); }, diff --git a/packages/@aws-cdk/aws-apigateway/test/test.util.ts b/packages/@aws-cdk/aws-apigateway/test/test.util.ts new file mode 100644 index 0000000000000..77b72b1dce825 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.util.ts @@ -0,0 +1,68 @@ +import { Test } from 'nodeunit'; +import { parseAwsApiCall, parseMethodOptionsPath } from '../lib/util'; + +export = { + parseMethodResourcePath: { + 'fails if path does not start with a /'(test: Test) { + test.throws(() => parseMethodOptionsPath('foo'), /Method options path must start with \'\/\'/); + test.done(); + }, + + 'fails if there are less than two components'(test: Test) { + test.throws(() => parseMethodOptionsPath('/'), /Method options path must include at least two components/); + test.throws(() => parseMethodOptionsPath('/foo'), /Method options path must include at least two components/); + test.throws(() => parseMethodOptionsPath('/foo/'), /Invalid HTTP method ""/); + test.done(); + }, + + 'fails if a non-supported http method is used'(test: Test) { + test.throws(() => parseMethodOptionsPath('/foo/bar'), /Invalid HTTP method "BAR"/); + test.done(); + }, + + 'extracts resource path and method correctly'(test: Test) { + test.deepEqual(parseMethodOptionsPath('/foo/GET'), { resourcePath: '/~1foo', httpMethod: 'GET' }); + test.deepEqual(parseMethodOptionsPath('/foo/bar/GET'), { resourcePath: '/~1foo~1bar', httpMethod: 'GET' }); + test.deepEqual(parseMethodOptionsPath('/foo/*/GET'), { resourcePath: '/~1foo~1*', httpMethod: 'GET' }); + test.deepEqual(parseMethodOptionsPath('/*/GET'), { resourcePath: '/*', httpMethod: 'GET' }); + test.deepEqual(parseMethodOptionsPath('/*/*'), { resourcePath: '/*', httpMethod: '*' }); + test.deepEqual(parseMethodOptionsPath('//POST'), { resourcePath: '/', httpMethod: 'POST' }); + test.done(); + } + }, + + parseAwsApiCall: { + 'fails if "actionParams" is set but "action" is undefined'(test: Test) { + test.throws(() => parseAwsApiCall(undefined, undefined, { foo: '123' }), /"actionParams" requires that "action" will be set/); + test.done(); + }, + + 'fails since "action" and "path" are mutually exclusive'(test: Test) { + test.throws(() => parseAwsApiCall('foo', 'bar'), /"path" and "action" are mutually exclusive \(path="foo", action="bar"\)/); + test.done(); + }, + + 'fails if "path" and "action" are both undefined'(test: Test) { + test.throws(() => parseAwsApiCall(), /Either "path" or "action" are required/); + test.done(); + }, + + '"path" mode'(test: Test) { + test.deepEqual(parseAwsApiCall('my/path'), { apiType: 'path', apiValue: 'my/path' }); + test.done(); + }, + + '"action" mode with no parameters'(test: Test) { + test.deepEqual(parseAwsApiCall(undefined, 'MyAction'), { apiType: 'action', apiValue: 'MyAction' }); + test.done(); + }, + + '"action" mode with parameters (url-encoded)'(test: Test) { + test.deepEqual(parseAwsApiCall(undefined, 'GetObject', { Bucket: 'MyBucket', Key: 'MyKey' }), { + apiType: 'action', + apiValue: 'GetObject&Bucket=MyBucket&Key=MyKey' + }); + test.done(); + } + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index 30981b7edab34..200338e392ee6 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts @@ -1,16 +1,25 @@ import { AwsAccountId, AwsPartition, AwsRegion, FnConcat, Token } from '..'; import { FnSelect, FnSplit } from '../cloudformation/fn'; +import { CloudFormationToken } from './cloudformation-token'; /** * An Amazon Resource Name (ARN). * http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html */ -export class Arn extends Token { +export class Arn extends CloudFormationToken { /** * Creates an ARN from components. - * If any component is the empty string, - * an empty string will be inserted into the generated ARN - * at the location that component corresponds to. + * + * If `partition`, `region` or `account` are not specified, the stack's + * partition, region and account will be used. + * + * If any component is the empty string, an empty string will be inserted + * into the generated ARN at the location that component corresponds to. + * + * The ARN will be formatted as follows: + * + * arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} + * */ public static fromComponents(components: ArnComponents) { const partition = components.partition == null diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index f62623f11bc2c..c0e38503e1ad0 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { CloudFormationToken, isToken, resolve, Token } from '../../lib'; +import { CloudFormationToken, isToken, resolve, Token, Arn, FnConcat, Ref } from '../../lib'; import { evaluateCFN } from '../cloudformation/evaluate-cfn'; export = { @@ -242,6 +242,27 @@ export = { test.done(); }, + + 'repro'(test: Test) { + // GIVEN + const token = new CloudFormationToken({ Ref: 'Other' }); + + // WHEN + const s = { + foo: `Hello, ${token}` + }; + + // const x = Arn.fromComponents({ + // service: 'apigateway', + // account: 'lambda', + // resource: 'path', + // resourceName: `2015-03-31/functions/${token}/invocations` + // }); + // console.log(x); + console.log(resolve(s)); + + test.done(); + } }; class Promise2 extends Token { From f33339ae041dca46936b429afb20a6c544ca54a0 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 4 Sep 2018 15:41:56 +0300 Subject: [PATCH 03/15] Getting ready for PR --- packages/@aws-cdk/aws-apigateway/README.md | 177 ++++- packages/@aws-cdk/aws-apigateway/lib/body.ts | 3 - .../@aws-cdk/aws-apigateway/lib/deployment.ts | 25 +- packages/@aws-cdk/aws-apigateway/lib/index.ts | 7 +- .../lib/{integrations.ts => integration.ts} | 232 ++---- .../aws-apigateway/lib/integrations/aws.ts | 81 ++ .../aws-apigateway/lib/integrations/http.ts | 50 ++ .../aws-apigateway/lib/integrations/lambda.ts | 72 ++ .../aws-apigateway/lib/integrations/mock.ts | 22 + .../@aws-cdk/aws-apigateway/lib/method.ts | 115 +-- .../@aws-cdk/aws-apigateway/lib/resource.ts | 22 +- .../aws-apigateway/lib/restapi-ref.ts | 2 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 203 +++-- packages/@aws-cdk/aws-apigateway/lib/stage.ts | 46 +- packages/@aws-cdk/aws-apigateway/package.json | 1 + .../test/integ.restapi.books.ts | 58 ++ .../test/integ.restapi.defaults.expected.json | 135 ++++ .../test/integ.restapi.defaults.ts | 2 +- .../test/integ.restapi.expected.json | 723 ++++++++++++++++++ .../aws-apigateway/test/integ.restapi.ts | 40 +- .../aws-apigateway/test/test.apigateway.ts | 177 +++-- packages/@aws-cdk/cdk/lib/core/tokens.ts | 9 +- .../@aws-cdk/cdk/test/core/test.tokens.ts | 31 +- 23 files changed, 1826 insertions(+), 407 deletions(-) delete mode 100644 packages/@aws-cdk/aws-apigateway/lib/body.ts rename packages/@aws-cdk/aws-apigateway/lib/{integrations.ts => integration.ts} (59%) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/mock.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 488e67b16c110..b390c5a029fab 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -1,2 +1,175 @@ -## The CDK Construct Library for AWS API Gateway -This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. +## CDK Construct Library for Amazon API Gateway + +Amazon API Gateway is a fully managed service that makes it easy for developers +to publish, maintain, monitor, and secure APIs at any scale. Create an API to +access data, business logic, or functionality from your back-end services, such +as applications running on Amazon Elastic Compute Cloud (Amazon EC2), code +running on AWS Lambda, or any web application. + +### Defining APIs + +APIs are defined as a hierarchy of resources and methods. `addResource` and +`addMethod` can be used to build this hierarchy. The root of this hierarchy is +the `RestApi` object (which can also be treated as a resource). + +For example, the following code defines an API that includes the following HTTP +endpoints: `GET /books`, `POST /books`, `GET /books/{book_id}`, `DELETE /books/{book_id}`. + +```ts +const api = new apigateway.RestApi(this, 'books-api'); + +api.addMethod('GET'); + +const books = api.addResource('books'); +books.addMethod('GET'); +books.addMethod('POST'); + +const book = api.addResource('{book_id}'); +book.addMethod('GET'); +book.addMethod('DELETE'); +``` + +### Backend Integrations + +Methods are associated with backend integrations, which are invoked when this +method is called. API Gateway supports the following integrations: + + * `MockIntegration` - can be used to test APIs. This is the default integration + if one is not specified. + * `LambdaIntegration` - can be used to invoke an AWS Lambda function. + * `AwsIntegration` - can be used to invoke arbitrary AWS service APIs. + * `HttpIntegration` - can be used to invoke HTTP endpoints. + +The following example shows how to integrate the `GET /book/{book_id}` method to +an AWS Lambda function: + +```ts +const getBookHandler = new lambda.Function(...); +const getBookIntegration = new apigateway.LambdaIntegration(getBookHandler); +book.addMethod('GET', getBookIntegration); +``` + +Integration options can be optionally be specified: + +```ts +const getBookIntegration = new apigateway.LambdaIntegration(getBookHandler, { + contentHandling: apigateway.ContentHandling.ConvertToText, // convert to base64 + credentialsPassthrough: true, // use caller identity to invoke the function +}); +``` + +Method options can optionally be specified when adding methods: + +```ts +book.addMethod('GET', getBookIntegration, { + authorizationType: apigateway.AuthorizationType.IAM, + apiKeyRequired: true +}); +``` + +#### Default Integration + +The `defaultIntegration` can be used to configure a default integration at the +API level. This integration will be used if an integration was not specified. + +> If not defined, the default integration is `MockIntegration` + +The following example defines the `booksBackend` integration as a default +integration. This means that all API methods that do not explicitly define an +integration will be routed to this AWS Lambda function. + +```ts +const booksBackend = new apigateway.LambdaIntegration(...); +const api = new apigateway.RestApi(this, 'books', { + defaultIntegration: booksBackend +}); + +const books = new api.addResource('books'); +books.addMethod('GET'); // integrated with `booksBackend` +books.addMethod('POST'); // integrated with `booksBackend` + +const book = books.addResource('{book_id}'); +book.addMethod('GET'); // integrated with `booksBackend` +``` + +### Deployments + +By default, the `RestApi` construct will automatically create an API Gateway +[Deployment] and a "prod" [Stage] which represent the API configuration you defined in +your CDK app. This means that when you deploy your app, your API can be accessed +from the public internet via the stage URL. + +The URL of your API can be obtained from the attribute `restApi.url`, and is +also exported as an `Output` from your stack, so it's printed when you `cdk +deploy` your app: + +``` +$ cdk deploy +... +books.booksapiEndpointE230E8D5 = https://6lyktd4lpk.execute-api.us-east-1.amazonaws.com/prod/ +``` + +To disable this behavior, you can set `{ deploy: false }` when creating your +API. This means that the API will not be deployed and a stage will not be +created for it. You will need to manually define a `apigateway.Deployment` and +`apigateway.Stage` resources. + +Use the `deployOptions` property to customize the deployment options of your +API. + +The following example will configure API Gateway to emit logs and data traces to +AWS CloudWatch for all API calls: + +> By default, an IAM role will be created and associated with API Gateway to +allow it to write logs and metrics to AWS CloudWatch `cloudWatchRole` is set to +`false`. + +```ts +const api = new apigateway.RestApi(this, 'books', { + deployOptions: { + loggingLevel: apigateway.MethodLoggingLevel.Info, + dataTraceEnabled: true + } +}) +``` + +#### Deeper dive: invalidation of deployments + +API Gateway deployments are an immutable snapshot of the API. This means that we +want to automatically create a new deployment resource every time the API model +defined in our CDK app changes. + +In order to achieve that, the AWS CloudFormation logical ID of the +`AWS::ApiGateway::Deployment` resource is dynamically calculated by hashing the +API configuration (resources, methods). This means that when the configuration +changes (i.e. a resource or method are added, configuration is changed), a new +logical ID will be assigned to the deployment resource. This will cause +CloudFormation to create a new deployment resource. + +By default, old deployments are _retained_, which means users can use the AWS +API Gateway Web Console to point the stage to an old deployment. To disable +this behavior, set the `retainDeployments` property to `false`. + +[Deployment]: https://docs.aws.amazon.com/apigateway/api-reference/resource/deployment/ +[Stage]: https://docs.aws.amazon.com/apigateway/api-reference/resource/stage/ + +### TODO + +The following features are not supported yet by this library: + +- [ ] Swagger/Open API models +- [ ] [Authorizers](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-authorizer.html) +- [ ] Method options: [`RequestValidatorId`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestvalidatorid), [`RequestModels`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestmodels), [`RequestParameters`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestparameters), [`MethodResponses`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-methodresponses) +- [ ] [Custom domains](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-domainname.html) +- [ ] [API keys](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html) +- [ ] [Base path mapping](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-basepathmapping.html) +- [ ] [Client certificates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-clientcertificate.html) +- [ ] Documentation ([part](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-documentationpart.html) and [version](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-documentationversion.html)) +- [ ] [Model](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-model.html) +- [ ] [Request validators](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-requestvalidator.html) +- [ ] [Usage plans](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-usageplan.html) +- [ ] [VPC Links](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-vpclink.html) + +---- + +This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/body.ts b/packages/@aws-cdk/aws-apigateway/lib/body.ts deleted file mode 100644 index 0d3b3ae373058..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/lib/body.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class RestApiBody { - -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 6c931e5b5727d..0777ae9c28cdf 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -3,7 +3,12 @@ import crypto = require('crypto'); import { cloudformation, DeploymentId } from './apigateway.generated'; import { RestApiRef } from './restapi-ref'; -export interface CommonDeploymentProps { +export interface DeploymentProps { + /** + * The Rest API to deploy. + */ + api: RestApiRef; + /** * A description of the purpose of the API Gateway deployment. */ @@ -19,13 +24,6 @@ export interface CommonDeploymentProps { retainDeployments?: boolean; } -export interface DeploymentProps extends CommonDeploymentProps { - /** - * The Rest API to deploy. - */ - api: RestApiRef; -} - /** * A Deployment of a REST API. * @@ -87,11 +85,12 @@ export class Deployment extends cdk.Construct { } /** - * Adds a component to the hash that determines this - * AWS::ApiGateway::Deployment resource's logical ID. This should be called - * by constructs of the API Gateway model that want to invalidate the - * deployment when their settings change. The component will be resolve()ed - * during synthesis so tokens are welcome. + * Adds a component to the hash that determines this Deployment resource's + * logical ID. + * + * This should be called by constructs of the API Gateway model that want to + * invalidate the deployment when their settings change. The component will + * be resolve()ed during synthesis so tokens are welcome. */ public addToLogicalId(data: any) { this.resource.addToLogicalId(data); diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index 6e8849a927eb1..be22abcf90558 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -1,11 +1,14 @@ export * from './restapi'; -export * from './body'; export * from './restapi-ref'; export * from './resource'; export * from './method'; -export * from './integrations'; +export * from './integration'; export * from './deployment'; export * from './stage'; +export * from './integrations/lambda'; +export * from './integrations/aws'; +export * from './integrations/mock'; + // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations.ts b/packages/@aws-cdk/aws-apigateway/lib/integration.ts similarity index 59% rename from packages/@aws-cdk/aws-apigateway/lib/integrations.ts rename to packages/@aws-cdk/aws-apigateway/lib/integration.ts index 13a3d8f27b1ce..3ce05347116ab 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integration.ts @@ -1,11 +1,7 @@ import iam = require('@aws-cdk/aws-iam'); -import lambda = require('@aws-cdk/aws-lambda'); -import cdk = require('@aws-cdk/cdk'); import { Method } from './method'; -import { parseAwsApiCall } from './util'; export interface IntegrationOptions { - /** * A list of request parameters whose values API Gateway caches. */ @@ -18,7 +14,8 @@ export interface IntegrationOptions { /** * Specifies how to handle request payload content type conversions. - * @default If this property isn't defined, the request payload is passed + * + * @default none if this property isn't defined, the request payload is passed * through from the method request to the integration request without * modification, provided that the `passthroughBehaviors` property is * configured to support payload pass-through. @@ -78,9 +75,23 @@ export interface IntegrationOptions { */ requestTemplates?: { [contentType: string]: string }; - // TODO: - // - IntegrationResponses - // + /** + * The response that API Gateway provides after a method's backend completes + * processing a request. API Gateway intercepts the response from the + * backend so that you can control how API Gateway surfaces backend + * responses. For example, you can map the backend status codes to codes + * that you define. + */ + integrationResponses?: IntegrationResponse[]; + + /** + * The templates that are used to transform the integration response body. + * Specify templates as key-value pairs (string-to-string mappings), with a + * content type as the key and a template as the value. + * + * @see http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html + */ + selectionPattern?: string; } export interface IntegrationProps { @@ -96,8 +107,7 @@ export interface IntegrationProps { * - If you specify HTTP for the `type` property, specify the API endpoint URL. * - If you specify MOCK for the `type` property, don't specify this property. * - If you specify AWS for the `type` property, specify an AWS service that - * follows this form: - * `arn:aws:apigateway:region:subdomain.service|service:path|action/service_api.` + * follows this form: `arn:aws:apigateway:region:subdomain.service|service:path|action/service_api.` * For example, a Lambda function URI follows this form: * arn:aws:apigateway:region:lambda:path/path. The path is usually in the * form /2015-03-31/functions/LambdaFunctionARN/invocations. @@ -118,155 +128,21 @@ export interface IntegrationProps { options?: IntegrationOptions; } -export abstract class MethodIntegration { - constructor(readonly props: IntegrationProps) { +/** + * Base class for backend integrations for an API Gateway method. + * + * Use one of the concrete classes such as `MockIntegration`, `AwsIntegration`, `LambdaIntegration` + * or implement on your own by specifying the set of props. + */ +export class Integration { + constructor(readonly props: IntegrationProps) { } - } - - public attachToMethod(_method: Method) { - return; - } -} - -export class MockMethodIntegration extends MethodIntegration { - constructor(options?: IntegrationOptions) { - super({ - type: IntegrationType.Mock, - options - }); - } -} - -export enum AwsApiType { - Path = 'path', - Action = 'action' -} - -export interface AwsIntegrationProps { /** - * Use AWS_PROXY integration. - * - * @default false - */ - proxy?: boolean; - - /** - * The name of the integrated AWS service (e.g. `s3`) - */ - service: string; - - /** - * A designated subdomain supported by certain AWS service for fast - * host-name lookup. - */ - subdomain?: string; - - /** - * The path to use for path-base APIs. - * - * For example, for S3 GET, you can set path to `bucket/key`. - * For lambda, you can set path to `2015-03-31/functions/${function-arn}/invocations` - * - * Mutually exclusive with the `action` options. - */ - path?: string; - - /** - * The AWS action to perform in the integration. - * - * Use `actionParams` to specify key-value params for the action. - * - * Mutually exclusive with `path`. - */ - action?: string; - - /** - * Parameters for the action. - * - * `action` must be set, and `path` must be undefined. - * The action params will be URL encoded. - */ - actionParameters?: { [key: string]: string }; - - /** - * Integration options. + * Can be overridden by subclasses to allow the integration to interact with the method + * being integrated, access the REST API object, method ARNs, etc. */ - options?: IntegrationOptions -} - -export class AwsIntegration extends MethodIntegration { - constructor(props: AwsIntegrationProps) { - const backend = props.subdomain ? `${props.subdomain}.${props.service}` : props.service; - const type = props.proxy ? IntegrationType.AwsProxy : IntegrationType.Aws; - const { apiType, apiValue } = parseAwsApiCall(props.path, props.action, props.actionParameters); - super({ - type, - integrationHttpMethod: 'POST', - uri: cdk.Arn.fromComponents({ - service: 'apigateway', - account: backend, - resource: apiType, - sep: '/', - resourceName: apiValue, - }), - options: props.options, - }); - } -} - -export interface LambdaIntegrationOptions extends IntegrationOptions { - /** - * Use proxy integration or normal (request/response mapping) integration. - * @default true - */ - proxy?: boolean; - - /** - * Allow invoking method from AWS Console UI (for testing purposes). - * - * This will add another permission to the AWS Lambda resource policy which - * will allow the `test-invoke-stage` stage to invoke this handler. If this - * is set to `false`, the function will only be usable from the deployment - * endpoint. - * - * @default true - */ - enableTestInvoke?: boolean; -} - -export class LambdaMethodIntegration extends AwsIntegration { - private readonly handler: lambda.FunctionRef; - private readonly enableTestInvoke: boolean; - - constructor(handler: lambda.FunctionRef, options: LambdaIntegrationOptions = { }) { - const proxy = options.proxy === undefined ? true : options.proxy; - - super({ - proxy, - service: 'lambda', - path: `2015-03-31/functions/${handler.functionArn}/invocations`, - options - }); - - this.handler = handler; - this.enableTestInvoke = options.enableTestInvoke === undefined ? true : false; - } - - public attachToMethod(method: Method) { - const principal = new cdk.ServicePrincipal('apigateway.amazonaws.com'); - - this.handler.addPermission(method.methodArn.toString(), { - principal, - sourceArn: method.methodArn - }); - - // add permission to invoke from the console - if (this.enableTestInvoke) { - this.handler.addPermission(method.testMethodArn.toString(), { - principal, - sourceArn: method.testMethodArn - }); - } + public bind(_method: Method) { + return; } } @@ -339,4 +215,46 @@ export enum PassthroughBehavior { * unmapped content types will be rejected with the same 415 response. */ WhenNoTemplates = 'WHEN_NO_TEMPLATES' -} \ No newline at end of file +} + +export interface IntegrationResponse { + /** + * The status code that API Gateway uses to map the integration response to + * a MethodResponse status code. + */ + statusCode: string; + + /** + * Specifies how to handle request payload content type conversions. + * + * @default none the request payload is passed through from the method + * request to the integration request without modification. + */ + contentHandling?: ContentHandling; + + /** + * The response parameters from the backend response that API Gateway sends + * to the method response. + * + * Use the destination as the key and the source as the value: + * + * - The destination must be an existing response parameter in the + * MethodResponse property. + * - The source must be an existing method request parameter or a static + * value. You must enclose static values in single quotation marks and + * pre-encode these values based on the destination specified in the + * request. + * + * @see http://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html + */ + responseParameters?: { [destination: string]: string }; + + /** + * The templates that are used to transform the integration response body. + * Specify templates as key-value pairs, with a content type as the key and + * a template as the value. + * + * @see http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html + */ + responseTemplates?: { [contentType: string]: string }; +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts new file mode 100644 index 0000000000000..feebddd35aba4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -0,0 +1,81 @@ +import cdk = require('@aws-cdk/cdk'); +import { Integration, IntegrationOptions, IntegrationType } from '../integration'; +import { parseAwsApiCall } from '../util'; + +export interface AwsIntegrationProps { + /** + * Use AWS_PROXY integration. + * + * @default false + */ + proxy?: boolean; + + /** + * The name of the integrated AWS service (e.g. `s3`) + */ + service: string; + + /** + * A designated subdomain supported by certain AWS service for fast + * host-name lookup. + */ + subdomain?: string; + + /** + * The path to use for path-base APIs. + * + * For example, for S3 GET, you can set path to `bucket/key`. + * For lambda, you can set path to `2015-03-31/functions/${function-arn}/invocations` + * + * Mutually exclusive with the `action` options. + */ + path?: string; + + /** + * The AWS action to perform in the integration. + * + * Use `actionParams` to specify key-value params for the action. + * + * Mutually exclusive with `path`. + */ + action?: string; + + /** + * Parameters for the action. + * + * `action` must be set, and `path` must be undefined. + * The action params will be URL encoded. + */ + actionParameters?: { [key: string]: string }; + + /** + * Integration options, such as content handling, request/response mapping, etc. + */ + options?: IntegrationOptions +} + +/** + * This type of integration lets an API expose AWS service actions. It is + * intended for calling all AWS service actions, but is not recommended for + * calling a Lambda function, because the Lambda custom integration is a legacy + * technology. + */ +export class AwsIntegration extends Integration { + constructor(props: AwsIntegrationProps) { + const backend = props.subdomain ? `${props.subdomain}.${props.service}` : props.service; + const type = props.proxy ? IntegrationType.AwsProxy : IntegrationType.Aws; + const { apiType, apiValue } = parseAwsApiCall(props.path, props.action, props.actionParameters); + super({ + type, + integrationHttpMethod: 'POST', + uri: cdk.Arn.fromComponents({ + service: 'apigateway', + account: backend, + resource: apiType, + sep: '/', + resourceName: apiValue, + }), + options: props.options, + }); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts new file mode 100644 index 0000000000000..ce1c029174a33 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts @@ -0,0 +1,50 @@ +import { Integration, IntegrationOptions, IntegrationType } from '../integration'; + +export interface HttpIntegrationProps { + /** + * Determines whether to use proxy integration or custom integration. + * + * @default true + */ + proxy?: boolean; + + /** + * HTTP method to use when invoking the backend URL. + * @default GET + */ + httpMethod?: string; + + /** + * Integration options, such as request/resopnse mapping, content handling, + * etc. + * + * @default defaults based on `IntegrationOptions` defaults + */ + options?: IntegrationOptions; +} + +/** + * You can integrate an API method with an HTTP endpoint using the HTTP proxy + * integration or the HTTP custom integration,. + * + * With the proxy integration, the setup is simple. You only need to set the + * HTTP method and the HTTP endpoint URI, according to the backend requirements, + * if you are not concerned with content encoding or caching. + * + * With the custom integration, the setup is more involved. In addition to the + * proxy integration setup steps, you need to specify how the incoming request + * data is mapped to the integration request and how the resulting integration + * response data is mapped to the method response. + */ +export class HttpIntegration extends Integration { + constructor(url: string, props: HttpIntegrationProps) { + const proxy = props.proxy !== undefined ? props.proxy : true; + const method = props.httpMethod || 'GET'; + super({ + type: proxy ? IntegrationType.HttpProxy : IntegrationType.Http, + integrationHttpMethod: method, + uri: url, + options: props.options, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts new file mode 100644 index 0000000000000..6cbe2097ad515 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts @@ -0,0 +1,72 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { IntegrationOptions } from '../integration'; +import { Method } from '../method'; +import { AwsIntegration } from './aws'; + +export interface LambdaIntegrationOptions extends IntegrationOptions { + /** + * Use proxy integration or normal (request/response mapping) integration. + * @default true + */ + proxy?: boolean; + + /** + * Allow invoking method from AWS Console UI (for testing purposes). + * + * This will add another permission to the AWS Lambda resource policy which + * will allow the `test-invoke-stage` stage to invoke this handler. If this + * is set to `false`, the function will only be usable from the deployment + * endpoint. + * + * @default true + */ + enableTest?: boolean; +} + +/** + * Integrates an AWS Lambda function to an API Gateway method. + * + * @example + * + * const handler = new lambda.Function(this, 'MyFunction', ...); + * api.addMethod('GET', new LambdaIntegration(handler)); + * + */ +export class LambdaIntegration extends AwsIntegration { + private readonly handler: lambda.FunctionRef; + private readonly enableTest: boolean; + + constructor(handler: lambda.FunctionRef, options: LambdaIntegrationOptions = { }) { + const proxy = options.proxy === undefined ? true : options.proxy; + + super({ + proxy, + service: 'lambda', + path: `2015-03-31/functions/${handler.functionArn}/invocations`, + options + }); + + this.handler = handler; + this.enableTest = options.enableTest === undefined ? true : false; + } + + public bind(method: Method) { + const principal = new cdk.ServicePrincipal('apigateway.amazonaws.com'); + + const desc = `${method.httpMethod}.${method.resource.resourcePath.replace(/\//g, '.')}`; + + this.handler.addPermission(`ApiPermission.${desc}`, { + principal, + sourceArn: method.methodArn, + }); + + // add permission to invoke from the console + if (this.enableTest) { + this.handler.addPermission(`ApiPermission.Test.${desc}`, { + principal, + sourceArn: method.testMethodArn + }); + } + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/mock.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/mock.ts new file mode 100644 index 0000000000000..5aeb97e755a98 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/mock.ts @@ -0,0 +1,22 @@ +import { Integration, IntegrationOptions, IntegrationType } from '../integration'; + +/** + * This type of integration lets API Gateway return a response without sending + * the request further to the backend. This is useful for API testing because it + * can be used to test the integration set up without incurring charges for + * using the backend and to enable collaborative development of an API. In + * collaborative development, a team can isolate their development effort by + * setting up simulations of API components owned by other teams by using the + * MOCK integrations. It is also used to return CORS-related headers to ensure + * that the API method permits CORS access. In fact, the API Gateway console + * integrates the OPTIONS method to support CORS with a mock integration. + * Gateway responses are other examples of mock integrations. + */ +export class MockIntegration extends Integration { + constructor(options?: IntegrationOptions) { + super({ + type: IntegrationType.Mock, + options + }); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index 5289a480eccc1..e4a3a441014e7 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -1,17 +1,12 @@ import cdk = require('@aws-cdk/cdk'); -import { cloudformation, MethodId } from './apigateway.generated'; -import { MethodIntegration, MockMethodIntegration } from './integrations'; +import { AuthorizerId, cloudformation, MethodId } from './apigateway.generated'; +import { Integration } from './integration'; +import { MockIntegration } from './integrations/mock'; import { IRestApiResource } from './resource'; import { RestApi } from './restapi'; import { validateHttpMethod } from './util'; export interface MethodOptions { - - /** - * The backend system that the method calls when it receives a request. - */ - integration?: MethodIntegration; - /** * A friendly operation name for the method. For example, you can assign the * OperationName of ListPets for the GET /pets method. @@ -20,9 +15,18 @@ export interface MethodOptions { /** * Method authorization. - * @default None + * @default None open access */ - authorization?: MethodAuthorization; + authorizationType?: AuthorizationType; + + /** + * If `authorizationType` is `Custom`, this specifies the ID of the method + * authorizer resource. + * + * NOTE: in the future this will be replaced with an `AuthorizerRef` + * construct. + */ + authorizerId?: AuthorizerId; /** * Indicates whether the method requires clients to submit a valid API key. @@ -31,7 +35,6 @@ export interface MethodOptions { apiKeyRequired?: boolean; // TODO: - // - Authorization (AuthorizationType, AuthorizerId) // - RequestValidatorId // - RequestModels // - RequestParameters @@ -50,6 +53,11 @@ export interface MethodProps { */ httpMethod: string; + /** + * The backend system that the method calls when it receives a request. + */ + integration?: Integration; + /** * Method options. */ @@ -58,10 +66,9 @@ export interface MethodProps { export class Method extends cdk.Construct { public readonly methodId: MethodId; - - private readonly resource: IRestApiResource; - private readonly restApi: RestApi; - private readonly httpMethod: string; + public readonly httpMethod: string; + public readonly resource: IRestApiResource; + public readonly restApi: RestApi; constructor(parent: cdk.Construct, id: string, props: MethodProps) { super(parent, id); @@ -73,18 +80,19 @@ export class Method extends cdk.Construct { validateHttpMethod(this.httpMethod); const options = props.options || { }; - const auth = options.authorization || MethodAuthorization.None; - const resource = new cloudformation.MethodResource(this, 'Resource', { + const methodProps: cloudformation.MethodResourceProps = { resourceId: props.resource.resourceId, restApiId: this.restApi.restApiId, httpMethod: props.httpMethod, operationName: options.operationName, apiKeyRequired: options.apiKeyRequired, - authorizationType: auth.authorizationType, - authorizerId: auth.authorizerId, - integration: this.renderIntegration(options.integration), - }); + authorizationType: options.authorizationType || AuthorizationType.None, + authorizerId: options.authorizerId, + integration: this.renderIntegration(props.integration, this.restApi.defaultIntegration) + }; + + const resource = new cloudformation.MethodResource(this, 'Resource', methodProps); this.methodId = resource.ref; @@ -93,16 +101,7 @@ export class Method extends cdk.Construct { const deployment = props.resource.resourceApi.latestDeployment; if (deployment) { deployment.addDependency(resource); - deployment.addToLogicalId({ - method: { - resourceId: props.resource.resourceId, - httpMethod: props.httpMethod, - operationName: options.operationName, - apiKeyRequired: options.apiKeyRequired, - authorizationType: auth.authorizationType, - authorizerId: auth.authorizerId - } - }); + deployment.addToLogicalId({ method: methodProps }); } } @@ -119,7 +118,8 @@ export class Method extends cdk.Construct { throw new Error('There is no stage associated with this restApi. Either use `autoDeploy` or explicitly assign `deploymentStage`'); } - return this.methodArnForStage(this.restApi.deploymentStage.stageName.toString()); + const stage = this.restApi.deploymentStage.stageName.toString(); + return this.restApi.executeApiArn(this.httpMethod, this.resource.resourcePath, stage); } /** @@ -127,24 +127,21 @@ export class Method extends cdk.Construct { * This stage is used by the AWS Console UI when testing the method. */ public get testMethodArn(): cdk.Arn { - return this.methodArnForStage('test-invoke-stage'); - } - - private methodArnForStage(stage: string) { - return cdk.Arn.fromComponents({ - service: 'execute-api', - resource: this.restApi.restApiId, - sep: '/', - resourceName: `${stage}/${this.httpMethod}${this.resource.resourcePath}` - }); + return this.restApi.executeApiArn(this.httpMethod, this.resource.resourcePath, 'test-invoke-stage'); } - private renderIntegration(integration?: MethodIntegration): cloudformation.MethodResource.IntegrationProperty { + private renderIntegration(integration?: Integration, defaultIntegration?: Integration): cloudformation.MethodResource.IntegrationProperty { if (!integration) { - return this.renderIntegration(new MockMethodIntegration()); + // use defaultIntegration from API if defined + if (defaultIntegration) { + return this.renderIntegration(defaultIntegration); + } + + // fallback to mock + return this.renderIntegration(new MockIntegration()); } - integration.attachToMethod(this); + integration.bind(this); const options = integration.props.options || { }; @@ -170,16 +167,30 @@ export class Method extends cdk.Construct { requestParameters: options.requestParameters, requestTemplates: options.requestTemplates, passthroughBehavior: options.passthroughBehavior, + integrationResponses: options.integrationResponses, credentials, }; } } -export class MethodAuthorization { - public static IAM = new MethodAuthorization('AWS_IAM'); - public static None = new MethodAuthorization('NONE'); +export enum AuthorizationType { + /** + * Open access. + */ + None = 'NONE', - constructor( - public readonly authorizationType: string, - public readonly authorizerId?: string) { } -} + /** + * Use AWS IAM permissions. + */ + IAM = 'AWS_IAM', + + /** + * Use a custom authorizer. + */ + Custom = 'CUSTOM', + + /** + * Use an AWS Cognito user pool. + */ + Cognito = 'COGNITO_USER_POOLS', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 93d0af67bf954..e8ce9dd41ac52 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -1,5 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation, ResourceId } from './apigateway.generated'; +import { Integration } from './integration'; import { Method, MethodOptions } from './method'; import { RestApi } from './restapi'; @@ -35,7 +36,10 @@ export interface IRestApiResource { * Defines a new method for this resource. * @param httpMethod The HTTP method */ - onMethod(httpMethod: string, options?: MethodOptions): Method; + addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method; + + // onHttpGet(resourcePath: string, integration?: Integration): void; + // onHttpPost(resourcePath: string, integration?: Integration): void; } export interface ResourceProps { @@ -61,11 +65,12 @@ export class Resource extends cdk.Construct implements IRestApiResource { validateResourcePathPart(props.pathPart); - const resource = new cloudformation.Resource(this, 'Resource', { + const resourceProps: cloudformation.ResourceProps = { restApiId: props.parent.resourceApi.restApiId, parentId: props.parent.resourceId, pathPart: props.pathPart - }); + }; + const resource = new cloudformation.Resource(this, 'Resource', resourceProps); this.resourceId = resource.ref; this.resourceApi = props.parent.resourceApi; @@ -78,12 +83,7 @@ export class Resource extends cdk.Construct implements IRestApiResource { const deployment = props.parent.resourceApi.latestDeployment; if (deployment) { deployment.addDependency(resource); - deployment.addToLogicalId({ - resource: { - resourceId: props.parent.resourceId, - pathPath: props.pathPart - } - }); + deployment.addToLogicalId({ resource: resourceProps }); } } @@ -91,8 +91,8 @@ export class Resource extends cdk.Construct implements IRestApiResource { return new Resource(this, pathPart, { parent: this, pathPart }); } - public onMethod(httpMethod: string, options?: MethodOptions): Method { - return new Method(this, httpMethod, { resource: this, httpMethod, options }); + public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { + return new Method(this, httpMethod, { resource: this, httpMethod, integration, options }); } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts index 8b1e9ce93390b..a71b11aa1eb19 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts @@ -6,7 +6,7 @@ export interface RestApiRefProps { } export abstract class RestApiRef extends cdk.Construct { - public static import(parent: cdk.Construct, id: string, props: RestApiRefProps) { + public static import(parent: cdk.Construct, id: string, props: RestApiRefProps): RestApiRef { return new ImportedRestApi(parent, id, props); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index c2bf8dcc396fc..651e511b0b9f4 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,9 +1,9 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { cloudformation, ResourceId, RestApiId } from './apigateway.generated'; -import { RestApiBody } from './body'; -import { CommonDeploymentProps, Deployment } from './deployment'; -import { Method } from './method'; +import { Deployment } from './deployment'; +import { Integration } from './integration'; +import { Method, MethodOptions } from './method'; import { IRestApiResource, Resource } from './resource'; import { RestApiRef } from './restapi-ref'; import { Stage, StageOptions } from './stage'; @@ -22,36 +22,40 @@ export interface RestApiProps { * * If this is set, `latestDeployment` will refer to the `Deployment` object * and `deploymentStage` will refer to a `Stage` that points to this - * deployment. To customize the stage options, use the `autoDeployStage` + * deployment. To customize the stage options, use the `deployStageOptions` * property. * + * A CloudFormation Output will also be defined with the root URL endpoint + * of this REST API. + * * @default true */ - autoDeploy?: boolean; + deploy?: boolean; /** - * Options for the latest deployment resource. - * @default CommonDeploymentProps defaults + * Options for the API Gateway stage that will always point to the latest + * deployment when `deploy` is enabled. If `deploy` is disabled, + * this value cannot be set. + * + * @default defaults based on defaults of `StageOptions` */ - autoDeployOptions?: CommonDeploymentProps; + deployOptions?: StageOptions; /** - * Options for the API Gateway stage that will always point to the latest - * deployment when `autoDeploy` is enabled. If `autoDeploy` is disabled, - * this value cannot be set. + * Retains old deployment resources when the API changes. This allows + * manually reverting stages to point to old deployments via the AWS + * Console. * - * @default CommonStageProps defaults + * @default true */ - autoDeployStageOptions?: StageOptions; + retainDeployments?: boolean; /** * A name for the API Gateway RestApi resource. * - * @default If this is not specified, and `body` (Open API definition) - * doesn't include a name, the ID of the RestApi construct will be used. - * Since this name doesn't need to be unique, that should be fine. + * @default construct-id defaults to the id of the RestApi construct */ - name?: string; + restApiName?: string; /** * Custom header parameters for the request. @@ -59,11 +63,6 @@ export interface RestApiProps { */ parameters?: { [key: string]: string }; - /** - * An OpenAPI specification that defines a set of RESTful APIs. - */ - body?: RestApiBody; - /** * A policy document that contains the permissions for this RestApi */ @@ -120,8 +119,29 @@ export interface RestApiProps { * The ID of the API Gateway RestApi resource that you want to clone. */ cloneFrom?: RestApiRef; + + /** + * Automatically configure an AWS CloudWatch role for API Gateway. + * @default true + */ + cloudWatchRole?: boolean; + + /** + * Default integration for all API methods. If this is set, any method + * created within this API that doesn't have an explicit integration will + * use this integration. + */ + defaultIntegration?: Integration; } +/** + * Represents a REST API in Amazon API Gateway. + * + * Use `addResource` and `addMethod` to configure the API model. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + */ export class RestApi extends RestApiRef implements IRestApiResource { /** * The ID of this API Gateway RestApi. @@ -144,17 +164,23 @@ export class RestApi extends RestApiRef implements IRestApiResource { */ public readonly resourcePath = '/'; + /** + * The integration to use as a default for all methods created within this + * API unless an integration is specified. + */ + public defaultIntegration?: Integration; + /** * API Gateway deployment that represents the latest changes of the API. * This resource will be automatically updated every time the REST API model changes. - * This will be undefined if `autoDeploy` is false. + * This will be undefined if `deploy` is false. */ public latestDeployment?: Deployment; /** * API Gateway stage that points to the latest deployment (if defined). * - * If `autoDeploy` is disabled, you will need to explicitly assign this value in order to + * If `deploy` is disabled, you will need to explicitly assign this value in order to * set up integrations. */ public deploymentStage?: Stage; @@ -164,14 +190,8 @@ export class RestApi extends RestApiRef implements IRestApiResource { constructor(parent: cdk.Construct, id: string, props: RestApiProps = { }) { super(parent, id); - // if 'body' (open api definition) is defined, it's okay for name to be undefined - // otherwise, use the construct id as name (there are no restrictions on name, so that should be fine) - const name = props.body ? props.name : id; - - const bodyProps = this.renderBody(props.body); - const resource = new cloudformation.RestApiResource(this, 'Resource', { - restApiName: name, + restApiName: props.restApiName || id, description: props.description, policy: props.policy, failOnWarnings: props.failOnWarnings, @@ -181,34 +201,84 @@ export class RestApi extends RestApiRef implements IRestApiResource { apiKeySourceType: props.apiKeySourceType, cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, parameters: props.parameters, - body: bodyProps && bodyProps.body, - bodyS3Location: bodyProps && bodyProps.bodyS3Location, }); + this.defaultIntegration = props.defaultIntegration; this.restApiId = resource.ref; this.resourceId = new ResourceId(resource.restApiRootResourceId); // they are the same this.resourceApi = this; - this.configureAutoDeploy(props); + this.configureDeployment(props); - // TODO - determine which field of RestApi need to be added to the hash - // of the Deployment resource - which are part of the model? + const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + if (cloudWatchRole) { + this.configureCloudWatchRole(resource); + } + } - this.configureCloudWatchRole(resource); + /** + * The deployed root URL of this REST API. + */ + public get url() { + return this.urlForPath(); } + /** + * Returns the URL for an HTTP path. + * + * Fails if `deploymentStage` is not set either by `deploy` or explicitly. + */ + public urlForPath(path: string = '/'): string { + if (!this.deploymentStage) { + throw new Error('Cannot determine deployment stage for API from "deploymentStage". Use "deploy" or explicitly set "deploymentStage"'); + } + + return this.deploymentStage.urlForPath(path); + } + + /** + * Adds a child resource under the root resource. + * @param pathPart The resource name (path part) + */ public addResource(pathPart: string): Resource { return new Resource(this, pathPart, { parent: this, pathPart }); } - public onMethod(httpMethod: string): Method { - return new Method(this, httpMethod, { resource: this, httpMethod }); + /** + * Adds a method to the root resource (i.e. "GET /") + * + * @param httpMethod The HTTP method (i.e. 'GET', 'POST', etc) + * @param integration Backend integration + * @param options Method options + */ + public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { + return new Method(this, httpMethod, { resource: this, httpMethod, integration, options }); } - public _attachMethod(method: Method) { - this.methods.push(method); + /** + * @returns The "execute-api" ARN. + * @default "*" returns the execute API ARN for all methods/resources in + * this API. + * @param method The method (default `*`) + * @param path The resource path. Must start with '/' (default `*`) + * @param stage The stage (default `*`) + */ + public executeApiArn(method: string = '*', path: string = '/*', stage: string = '*') { + if (!path.startsWith('/')) { + throw new Error(`"path" must begin with a "/": ${path}'`); + } + + return cdk.Arn.fromComponents({ + service: 'execute-api', + resource: this.restApiId, + sep: '/', + resourceName: `${stage}/${method}${path}` + }); } + /** + * Performs validation of the REST API. + */ public validate() { if (this.methods.length === 0) { return [ `The REST API doesn't contain any methods` ]; @@ -217,24 +287,40 @@ export class RestApi extends RestApiRef implements IRestApiResource { return []; } - private configureAutoDeploy(props: RestApiProps) { - const autoDeploy = props.autoDeploy === undefined ? true : props.autoDeploy; - if (autoDeploy) { - this.latestDeployment = new Deployment(this, 'LatestDeployment', { + /** + * Internal API used by `Method` to keep an inventory of methods at the API + * level for validation purposes. + */ + public _attachMethod(method: Method) { + this.methods.push(method); + } + + private configureDeployment(props: RestApiProps) { + const deploy = props.deploy === undefined ? true : props.deploy; + if (deploy) { + + this.latestDeployment = new Deployment(this, 'Deployment', { + description: 'Automatically created by the RestApi construct', api: this, - ...props.autoDeployOptions + retainDeployments: props.retainDeployments }); - this.deploymentStage = new Stage(this, 'DeploymentStage', { + // encode the stage name into the construct id, so if we change the stage name, it will recreate a new stage. + // stage name is part of the endpoint, so that makes sense. + const stageName = (props.deployOptions && props.deployOptions.stageName) || 'prod'; + + this.deploymentStage = new Stage(this, `DeploymentStage.${stageName}`, { deployment: this.latestDeployment, - ...props.autoDeployStageOptions + ...props.deployOptions }); + + new cdk.Output(this, 'Endpoint', { value: this.urlForPath() }); } else { - if (props.autoDeployStageOptions) { - throw new Error(`Cannot set 'autoDeployStageOptions' if 'autoDeploy' is disabled`); + if (props.deployOptions) { + throw new Error(`Cannot set 'deployStageOptions' if 'deploy' is disabled`); } - if (props.autoDeployOptions) { - throw new Error(`Cannot set 'autoDeployOptions' if 'autoDeploy' is disabled`); + if (props.deployOptions) { + throw new Error(`Cannot set 'deployOptions' if 'deploy' is disabled`); } } } @@ -258,17 +344,6 @@ export class RestApi extends RestApiRef implements IRestApiResource { resource.addDependency(apiResource); } - - private renderBody(body?: RestApiBody): { body?: object, bodyS3Location?: cloudformation.RestApiResource.S3LocationProperty } | undefined { - if (!body) { - return undefined; - } - - return { - body: { }, - bodyS3Location : { } - }; - } } export enum ApiKeySourceType { @@ -299,3 +374,5 @@ export enum EndpointType { */ Private = 'PRIVATE' } + +export class RestApiUrl extends cdk.CloudFormationToken { } diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index b968c0f041da6..f5bac63fefb5a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -1,9 +1,10 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation, StageName } from './apigateway.generated'; import { Deployment } from './deployment'; +import { RestApiRef } from './restapi-ref'; import { parseMethodOptionsPath } from './util'; -export interface StageOptions { +export interface StageOptions extends MethodDeploymentOptions { /** * The name of the stage, which API Gateway uses as the first path segment * in the invoked Uniform Resource Identifier (URI). @@ -48,12 +49,6 @@ export interface StageOptions { */ variables?: { [key: string]: string }; - /** - * Default deployment options for all methods. You can indicate deployment - * options for specific resources/methods via `customMethodOptions`. - */ - methodOptions?: MethodDeploymentOptions - /** * Method deployment options for specific resources/methods. These will * override common options defined in `StageOptions#methodOptions`. @@ -63,7 +58,7 @@ export interface StageOptions { * to define options for all methods/resources. */ - customMethodOptions?: { [path: string]: MethodDeploymentOptions }; + methodOptions?: { [path: string]: MethodDeploymentOptions }; } export interface StageProps extends StageOptions { @@ -134,6 +129,8 @@ export interface MethodDeploymentOptions { export class Stage extends cdk.Construct { public readonly stageName: StageName; + private readonly restApi: RestApiRef; + constructor(parent: cdk.Construct, id: string, props: StageProps) { super(parent, id); @@ -160,18 +157,41 @@ export class Stage extends cdk.Construct { }); this.stageName = resource.ref; + this.restApi = props.deployment.api; + } + + /** + * Returns the invoke URL for a certain path. + * @param path The resource path + */ + public urlForPath(path: string = '/') { + return `https://${this.restApi.restApiId}.execute-api.${new cdk.AwsRegion()}.amazonaws.com/${this.stageName}${path}`; } private renderMethodSettings(props: StageProps): cloudformation.StageResource.MethodSettingProperty[] | undefined { const settings = new Array(); - if (props.methodOptions) { - settings.push(renderEntry('/*/*', props.methodOptions)); + // extract common method options from the stage props + const commonMethodOptions: MethodDeploymentOptions = { + metricsEnabled: props.metricsEnabled, + loggingLevel: props.loggingLevel, + dataTraceEnabled: props.dataTraceEnabled, + throttlingBurstLimit: props.throttlingBurstLimit, + throttlingRateLimit: props.throttlingRateLimit, + cachingEnabled: props.cachingEnabled, + cacheTtlSeconds: props.cacheTtlSeconds, + cacheDataEncrypted: props.cacheDataEncrypted + }; + + // if any of them are defined, add an entry for '/*/*'. + const hasCommonOptions = Object.keys(commonMethodOptions).map(v => (commonMethodOptions as any)[v]).filter(x => x).length > 0; + if (hasCommonOptions) { + settings.push(renderEntry('/*/*', commonMethodOptions)); } - if (props.customMethodOptions) { - for (const path of Object.keys(props.customMethodOptions)) { - settings.push(renderEntry(path, props.customMethodOptions[path])); + if (props.methodOptions) { + for (const path of Object.keys(props.methodOptions)) { + settings.push(renderEntry(path, props.methodOptions[path])); } } diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index c1faaf25070ee..2c20405b99904 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -48,6 +48,7 @@ "devDependencies": { "@aws-cdk/assert": "^0.8.2", "cdk-build-tools": "^0.8.2", + "cdk-integ-tools": "^0.8.2", "cfn2ts": "^0.8.2", "pkglint": "^0.8.2" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts new file mode 100644 index 0000000000000..35dea9aff516b --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts @@ -0,0 +1,58 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import apigw = require('../lib'); + +class BookStack extends cdk.Stack { + constructor(parent: cdk.App, name: string) { + super(parent, name); + + const echo = new apigw.LambdaIntegration(new lambda.Function(this, 'Handler', { + runtime: lambda.Runtime.NodeJS610, + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${handlerCode}`) + })); + + const hello = new apigw.LambdaIntegration(new lambda.Function(this, 'Hello', { + runtime: lambda.Runtime.NodeJS610, + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${helloCode}`) + })); + + const api = new apigw.RestApi(this, 'books-api', { defaultIntegration: echo }); + api.addMethod('GET', hello); + + const books = api.addResource('books'); + books.addMethod('GET'); + books.addMethod('POST'); + + const book = books.addResource('{book_id}'); + book.addMethod('GET'); + book.addMethod('DELETE'); + } +} + +class BookApp extends cdk.App { + constructor(argv: string[]) { + super(argv); + + new BookStack(this, 'books'); + } +} + +function handlerCode(event: any, _: any, callback: any) { + return callback(undefined, { + isBase64Encoded: false, + statusCode: 200, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(event) + }); +} + +function helloCode(_event: any, _context: any, callback: any) { + return callback(undefined, { + statusCode: 200, + body: 'hello, world!' + }); +} + +process.stdout.write(new BookApp(process.argv).run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json new file mode 100644 index 0000000000000..8b88a6147b73d --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -0,0 +1,135 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "my-api" + } + }, + "myapiLatestDeployment24E142F763611a77ac8e9dca00e384f72144a2c9": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + } + }, + "DependsOn": [ + "myapiGETF990CE3C" + ], + "DeletionPolicy": "Retain" + }, + "myapiAutoDeployStageprod98CECBB1": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiLatestDeployment24E142F763611a77ac8e9dca00e384f72144a2c9" + }, + "StageName": "prod" + } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + }, + "myapiGETF990CE3C": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Fn::GetAtt": [ + "myapi4C7BF186", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "Type": "MOCK" + } + } + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/", + { + "Ref": "myapiAutoDeployStageprod98CECBB1" + }, + "/" + ] + ] + }, + "Export": { + "Name": "test-apigateway-restapi-defaults:myapiEndpoint3628AFE3" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts index ce9f182480901..9e0eff148e610 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts @@ -8,6 +8,6 @@ const stack = new cdk.Stack(app, 'test-apigateway-restapi-defaults'); const api = new apigateway.RestApi(stack, 'my-api'); // at least one method is required -api.onMethod('GET'); +api.addMethod('GET'); process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json new file mode 100644 index 0000000000000..d2fb767099161 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -0,0 +1,723 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "my-api" + } + }, + "myapiLatestDeployment24E142F7620808ef5f957f2d97015d80d9278fe8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + } + }, + "DependsOn": [ + "myapiv113487378", + "myapiv1toysA55FCBC4", + "myapiv1toysGET7348114D", + "myapiv1toysPOST55128058", + "myapiv1toysPUT59AFBBC2", + "myapiv1appliances507FEFF4", + "myapiv1appliancesGET8FE872EC", + "myapiv1books1D4BE6C1", + "myapiv1booksGETC6B996D0", + "myapiv1booksPOST53E2832E" + ], + "DeletionPolicy": "Retain" + }, + "myapiAutoDeployStagebetaF170BD78": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "CacheClusterEnabled": true, + "CacheClusterSize": "0.5", + "DeploymentId": { + "Ref": "myapiLatestDeployment24E142F7620808ef5f957f2d97015d80d9278fe8" + }, + "Description": "beta stage", + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + }, + { + "CachingEnabled": true, + "HttpMethod": "GET", + "ResourcePath": "/~1api~1appliances" + } + ], + "StageName": "beta" + } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + }, + "myapiv113487378": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "myapi4C7BF186", + "RootResourceId" + ] + }, + "PathPart": "v1", + "RestApiId": { + "Ref": "myapi4C7BF186" + } + } + }, + "myapiv1toysA55FCBC4": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "myapiv113487378" + }, + "PathPart": "toys", + "RestApiId": { + "Ref": "myapi4C7BF186" + } + } + }, + "myapiv1toysGET7348114D": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "myapiv1toysA55FCBC4" + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + "Ref": "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "MyHandler6B74D312", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "myapiv1toysPOST55128058": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "myapiv1toysA55FCBC4" + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "Type": "MOCK" + } + } + }, + "myapiv1toysPUT59AFBBC2": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "PUT", + "ResourceId": { + "Ref": "myapiv1toysA55FCBC4" + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "Type": "MOCK" + } + } + }, + "myapiv1appliances507FEFF4": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "myapiv113487378" + }, + "PathPart": "appliances", + "RestApiId": { + "Ref": "myapi4C7BF186" + } + } + }, + "myapiv1appliancesGET8FE872EC": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "myapiv1appliances507FEFF4" + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "Type": "MOCK" + } + } + }, + "myapiv1books1D4BE6C1": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "myapiv113487378" + }, + "PathPart": "books", + "RestApiId": { + "Ref": "myapi4C7BF186" + } + } + }, + "myapiv1booksGETC6B996D0": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "myapiv1books1D4BE6C1" + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + "Ref": "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "MyHandler6B74D312", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "myapiv1booksPOST53E2832E": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "myapiv1books1D4BE6C1" + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + "Ref": "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "MyHandler6B74D312", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "MyHandlerServiceRoleFFA06653": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyHandler6B74D312": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function handlerCode(event, _, callback) {\n return callback(undefined, {\n isBase64Encoded: false,\n statusCode: 200,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(event)\n });\n }" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyHandlerServiceRoleFFA06653", + "Arn" + ] + }, + "Runtime": "nodejs6.10" + }, + "DependsOn": [ + "MyHandlerServiceRoleFFA06653" + ] + }, + "MyHandlerTokenTOKEN43F46AF4D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "MyHandler6B74D312" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "myapi4C7BF186" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + "Ref": "myapiAutoDeployStagebetaF170BD78" + }, + "/GET/v1/toys" + ] + ] + } + ] + ] + } + } + }, + "MyHandlerTokenTOKEN5853A4C5C": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "MyHandler6B74D312" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "myapi4C7BF186" + }, + "/", + "test-invoke-stage/GET/v1/toys" + ] + ] + } + } + }, + "MyHandlerTokenTOKEN62E0733C0": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "MyHandler6B74D312" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "myapi4C7BF186" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + "Ref": "myapiAutoDeployStagebetaF170BD78" + }, + "/GET/v1/books" + ] + ] + } + ] + ] + } + } + }, + "MyHandlerTokenTOKEN7978BDF6A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "MyHandler6B74D312" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "myapi4C7BF186" + }, + "/", + "test-invoke-stage/GET/v1/books" + ] + ] + } + } + }, + "MyHandlerTokenTOKEN8BF01E10D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "MyHandler6B74D312" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "myapi4C7BF186" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + "Ref": "myapiAutoDeployStagebetaF170BD78" + }, + "/POST/v1/books" + ] + ] + } + ] + ] + } + } + }, + "MyHandlerTokenTOKEN9636E43E2": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "MyHandler6B74D312" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "myapi4C7BF186" + }, + "/", + "test-invoke-stage/POST/v1/books" + ] + ] + } + } + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/", + { + "Ref": "myapiAutoDeployStagebetaF170BD78" + }, + "/" + ] + ] + }, + "Export": { + "Name": "test-apigateway-restapi:myapiEndpoint3628AFE3" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index 55e82530b6e8e..739afd2dd93ff 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -7,16 +7,13 @@ class Test extends cdk.Stack { super(parent, id); const api = new apigateway.RestApi(this, 'my-api', { - minimumCompressionSize: 0, - autoDeployStageOptions: { + deployOptions: { cacheClusterEnabled: true, - stageName: 'test', - description: 'testing stage', + stageName: 'beta', + description: 'beta stage', + loggingLevel: apigateway.MethodLoggingLevel.Info, + dataTraceEnabled: true, methodOptions: { - loggingLevel: apigateway.MethodLoggingLevel.Info, - dataTraceEnabled: true - }, - customMethodOptions: { '/api/appliances/GET': { cachingEnabled: true } @@ -30,33 +27,28 @@ class Test extends cdk.Stack { handler: 'index.handler', }); - const v1 = api.addResource('api'); + const v1 = api.addResource('v1'); - const toys = v1.addResource('toys'); - - toys.onMethod('GET', { - integration: new apigateway.LambdaMethodIntegration(handler) - }); + const integration = new apigateway.LambdaIntegration(handler); - toys.onMethod('POST'); - toys.onMethod('PUT'); + const toys = v1.addResource('toys'); + toys.addMethod('GET', integration); + toys.addMethod('POST'); + toys.addMethod('PUT'); const appliances = v1.addResource('appliances'); - appliances.onMethod('GET'); + appliances.addMethod('GET'); const books = v1.addResource('books'); - books.onMethod('GET'); - books.onMethod('POST'); + books.addMethod('GET', integration); + books.addMethod('POST', integration); function handlerCode(event: any, _: any, callback: any) { - // tslint:disable-next-line:no-console - console.log(JSON.stringify(event, undefined, 2)); - return callback(undefined, { isBase64Encoded: false, statusCode: 200, - headers: { }, - body: 'hi' + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(event) }); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts b/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts index 65362b4156a5e..02bf33f381952 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts @@ -1,4 +1,4 @@ -import { expect } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { App, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; @@ -8,44 +8,7 @@ import apigateway = require('../lib'); // tslint:disable:object-literal-key-quotes export = { - '"name" is defaulted to construct id'(test: Test) { - const stack = new cdk.Stack(); - new apigateway.RestApi(stack, 'my-first-api', { autoDeploy: false }); - expect(stack).toMatch({ - "Resources": { - "myfirstapi5827A5AA": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "my-first-api" - } - } - } - }); - - test.done(); - }, - - '"name" can be undefined if "body" is specified'(test: Test) { - const stack = new cdk.Stack(); - new apigateway.RestApi(stack, 'bla', { - autoDeploy: false, - body: new apigateway.RestApiBody() - }); - expect(stack).toMatch({ - "Resources": { - "blaBE223B94": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": {}, - "BodyS3Location": {} - } - } - } - }); - test.done(); - }, - - 'minimal setup (just a name)'(test: Test) { + 'minimal setup'(test: Test) { const stack = new cdk.Stack(); new apigateway.RestApi(stack, 'my-api'); @@ -58,26 +21,109 @@ export = { "Name": "my-api" } }, - "myapiLatestDeployment24E142F7": { + "myapiDeployment92F2CB49": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "myapi4C7BF186" - } + }, + "Description": "Automatically created by the RestApi construct" }, "DeletionPolicy": "Retain" }, - "myapiDeploymentStage252BF8C8": { + "myapiDeploymentStageprod298F01AF": { "Type": "AWS::ApiGateway::Stage", "Properties": { "RestApiId": { "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiLatestDeployment24E142F7" + "Ref": "myapiDeployment92F2CB49" }, "StageName": "prod" } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + }, + "Export": { + "Name": "myapiEndpoint3628AFE3" + } } } }); @@ -85,7 +131,27 @@ export = { test.done(); }, - 'fails in synthesis if there are no methods'(test: Test) { + '"name" is defaulted to construct id'(test: Test) { + const stack = new cdk.Stack(); + new apigateway.RestApi(stack, 'my-first-api', { + deploy: false, + cloudWatchRole: false, + }); + expect(stack).toMatch({ + "Resources": { + "myfirstapi5827A5AA": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "my-first-api" + } + } + } + }); + + test.done(); + }, + + 'fails in synthesis if there are no methods'(test: Test) { const app = new App(); const stack = new Stack(app, 'my-stack'); @@ -101,8 +167,9 @@ export = { 'newChildResource can be used on IRestApiResource to form a tree'(test: Test) { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'restapi', { - autoDeploy: false, - name: 'my-rest-api' + deploy: false, + cloudWatchRole: false, + restApiName: 'my-rest-api' }); const foo = api.addResource('foo'); @@ -115,7 +182,7 @@ export = { "restapiC5611D27": { "Type": "AWS::ApiGateway::RestApi", "Properties": { - "Name": "restapi" + "Name": "my-rest-api" } }, "restapifooF697E056": { @@ -189,7 +256,7 @@ export = { 'resource path cannot use "/"'(test: Test) { const stack = new cdk.Stack(); - const api = new apigateway.RestApi(stack, 'restapi', { name: 'my-rest-api' }); + const api = new apigateway.RestApi(stack, 'restapi'); test.throws(() => api.addResource('foo/')); test.done(); }, @@ -197,7 +264,7 @@ export = { 'fails if autoDeployStageOptions is set with autoDeploy disabled'(test: Test) { const stack = new cdk.Stack(); test.throws(() => { - new apigateway.RestApi(stack, 'myapi', { autoDeploy: false, autoDeployStageOptions: { stageName: 'foo' }}); + new apigateway.RestApi(stack, 'myapi', { deploy: false, deployOptions: { stageName: 'foo' }}); }, `Cannot set 'autoDeployStageOptions' if 'autoDeploy' is disabled`); test.done(); }, @@ -205,8 +272,16 @@ export = { 'fails if autoDeployOptions is set with autoDeploy disabled'(test: Test) { const stack = new cdk.Stack(); test.throws(() => { - new apigateway.RestApi(stack, 'myapi', { autoDeploy: false, autoDeployOptions: { retainDeployments: false }}); + new apigateway.RestApi(stack, 'myapi', { deploy: false, deployOptions: { cachingEnabled: true }}); }, `Cannot set 'autoDeployOptions' if 'autoDeploy' is disabled`); test.done(); - } + }, + + 'CloudWatch role is created for API Gateway'(test: Test) { + const stack = new cdk.Stack(); + new apigateway.RestApi(stack, 'myapi'); + expect(stack).to(haveResource('AWS::IAM::Role')); + expect(stack).to(haveResource('AWS::ApiGateway::Account')); + test.done(); + } }; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index 2aa6ed95538ff..a1700de793b38 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -198,6 +198,11 @@ export function resolve(obj: any, prefix?: string[]): any { const result: any = { }; for (const key of Object.keys(obj)) { + const resolvedKey = resolve(key); + if (typeof(resolvedKey) !== 'string') { + throw new Error(`The key "${key}" has been resolved to ${JSON.stringify(resolvedKey)} but must be resolvable to a string`); + } + const value = resolve(obj[key], path.concat(key)); // skip undefined @@ -205,7 +210,7 @@ export function resolve(obj: any, prefix?: string[]): any { continue; } - result[key] = value; + result[resolvedKey] = value; } return result; @@ -257,7 +262,7 @@ class TokenStringMap { */ public resolveMarkers(s: string): any { const str = new TokenString(s, BEGIN_TOKEN_MARKER, `[${VALID_KEY_CHARS}]+`, END_TOKEN_MARKER); - const fragments = str.split(this.lookupToken.bind(this)); + const fragments = str.split(key => this.lookupToken(key)); return fragments.join(); } diff --git a/packages/@aws-cdk/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index c0e38503e1ad0..621539538895b 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { CloudFormationToken, isToken, resolve, Token, Arn, FnConcat, Ref } from '../../lib'; +import { CloudFormationToken, isToken, resolve, Token } from '../../lib'; import { evaluateCFN } from '../cloudformation/evaluate-cfn'; export = { @@ -243,24 +243,31 @@ export = { test.done(); }, - 'repro'(test: Test) { + 'tokens can be used in hash keys but must resolve to a string'(test: Test) { // GIVEN - const token = new CloudFormationToken({ Ref: 'Other' }); + const token = new Token(() => 'I am a string'); // WHEN const s = { - foo: `Hello, ${token}` + [token.toString()]: `boom ${token}` }; - // const x = Arn.fromComponents({ - // service: 'apigateway', - // account: 'lambda', - // resource: 'path', - // resourceName: `2015-03-31/functions/${token}/invocations` - // }); - // console.log(x); - console.log(resolve(s)); + // THEN + test.deepEqual(resolve(s), { 'I am a string': 'boom I am a string' }); + test.done(); + }, + + 'fails if token in a hash key resolves to a non-string'(test: Test) { + // GIVEN + const token = new CloudFormationToken({ Ref: 'Other' }); + + // WHEN + const s = { + [token.toString()]: `boom ${token}` + }; + // THEN + test.throws(() => resolve(s), 'The key "${Token[TOKEN.19]}" has been resolved to {"Ref":"Other"} but must be resolvable to a string'); test.done(); } }; From c99e7285e7b24700cfd4d52f6da32cffe12c511c Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 4 Sep 2018 22:32:35 +0300 Subject: [PATCH 04/15] Coverage --- packages/@aws-cdk/assert/lib/expect.ts | 25 +- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 4 +- packages/@aws-cdk/aws-apigateway/lib/index.ts | 1 + .../aws-apigateway/lib/integrations/http.ts | 2 +- .../aws-apigateway/lib/integrations/lambda.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/method.ts | 2 +- .../@aws-cdk/aws-apigateway/lib/resource.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 7 +- packages/@aws-cdk/aws-apigateway/lib/stage.ts | 26 +- .../test/integ.restapi.books.expected.json | 978 ++++++++++++++++++ .../test/integ.restapi.books.ts | 12 +- .../test/integ.restapi.defaults.expected.json | 11 +- .../test/integ.restapi.defaults.ts | 2 +- .../test/integ.restapi.expected.json | 29 +- .../aws-apigateway/test/integ.restapi.ts | 12 +- .../aws-apigateway/test/test.apigateway.ts | 287 ----- .../aws-apigateway/test/test.deployment.ts | 178 ++++ .../@aws-cdk/aws-apigateway/test/test.http.ts | 57 + .../aws-apigateway/test/test.lambda.ts | 148 +++ .../aws-apigateway/test/test.method.ts | 269 +++++ .../aws-apigateway/test/test.restapi.ts | 523 ++++++++++ .../aws-apigateway/test/test.stage.ts | 247 +++++ 22 files changed, 2481 insertions(+), 347 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json delete mode 100644 packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.deployment.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.http.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.lambda.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.method.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.restapi.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.stage.ts diff --git a/packages/@aws-cdk/assert/lib/expect.ts b/packages/@aws-cdk/assert/lib/expect.ts index 7ea023e75f2a0..dc771418ae17a 100644 --- a/packages/@aws-cdk/assert/lib/expect.ts +++ b/packages/@aws-cdk/assert/lib/expect.ts @@ -2,14 +2,27 @@ import cdk = require('@aws-cdk/cdk'); import api = require('@aws-cdk/cx-api'); import { StackInspector } from './inspector'; -export function expect(stack: api.SynthesizedStack | cdk.Stack): StackInspector { +export function expect(stack: api.SynthesizedStack | cdk.Stack, skipValidation = false): StackInspector { // Can't use 'instanceof' here, that breaks if we have multiple copies // of this library. - const sstack: api.SynthesizedStack = isStackClassInstance(stack) ? { - name: 'test', - template: stack.toCloudFormation(), - metadata: {} - } : stack; + let sstack: api.SynthesizedStack; + + if (isStackClassInstance(stack)) { + if (!skipValidation) { + const errors = stack.validateTree(); + if (errors.length > 0) { + throw new Error(`Stack validation failed:\n${errors.map(e => `${e.message} at: ${e.source.parent}`).join('\n')}`); + } + } + + sstack = { + name: 'test', + template: stack.toCloudFormation(), + metadata: {} + }; + } else { + sstack = stack; + } return new StackInspector(sstack); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 0777ae9c28cdf..6888e907c422d 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -141,7 +141,7 @@ class LatestDeploymentResource extends cloudformation.DeploymentResource { * Hooks into synthesis to calculate a logical ID that hashes all the components * add via `addToLogicalId`. */ - public toCloudFormation() { + public validate() { // if hash components were added to the deployment, we use them to calculate // a logical ID for the deployment resource. if (this.hashComponents.length === 0) { @@ -155,6 +155,6 @@ class LatestDeploymentResource extends cloudformation.DeploymentResource { this.customLogicalId = this.originalLogicalId + md5.digest("hex"); } - return super.toCloudFormation(); + return []; } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index be22abcf90558..e48676bb047d8 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -9,6 +9,7 @@ export * from './stage'; export * from './integrations/lambda'; export * from './integrations/aws'; export * from './integrations/mock'; +export * from './integrations/http'; // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts index ce1c029174a33..35573741828f5 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts @@ -37,7 +37,7 @@ export interface HttpIntegrationProps { * response data is mapped to the method response. */ export class HttpIntegration extends Integration { - constructor(url: string, props: HttpIntegrationProps) { + constructor(url: string, props: HttpIntegrationProps = { }) { const proxy = props.proxy !== undefined ? props.proxy : true; const method = props.httpMethod || 'GET'; super({ diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts index 6cbe2097ad515..b8513002c4fde 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts @@ -21,7 +21,7 @@ export interface LambdaIntegrationOptions extends IntegrationOptions { * * @default true */ - enableTest?: boolean; + allowTestInvoke?: boolean; } /** @@ -48,7 +48,7 @@ export class LambdaIntegration extends AwsIntegration { }); this.handler = handler; - this.enableTest = options.enableTest === undefined ? true : false; + this.enableTest = options.allowTestInvoke === undefined ? true : false; } public bind(method: Method) { diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index e4a3a441014e7..da8b78192d8ac 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -146,7 +146,7 @@ export class Method extends cdk.Construct { const options = integration.props.options || { }; let credentials; - if (options.credentialsPassthrough && options.credentialsRole) { + if (options.credentialsPassthrough !== undefined && options.credentialsRole !== undefined) { throw new Error(`'credentialsPassthrough' and 'credentialsRole' are mutually exclusive`); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index e8ce9dd41ac52..6b17bee564ee6 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -36,7 +36,7 @@ export interface IRestApiResource { * Defines a new method for this resource. * @param httpMethod The HTTP method */ - addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method; + onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method; // onHttpGet(resourcePath: string, integration?: Integration): void; // onHttpPost(resourcePath: string, integration?: Integration): void; @@ -91,7 +91,7 @@ export class Resource extends cdk.Construct implements IRestApiResource { return new Resource(this, pathPart, { parent: this, pathPart }); } - public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { + public onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { return new Method(this, httpMethod, { resource: this, httpMethod, integration, options }); } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 651e511b0b9f4..07f8e7c76fd5c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -251,7 +251,7 @@ export class RestApi extends RestApiRef implements IRestApiResource { * @param integration Backend integration * @param options Method options */ - public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { + public onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { return new Method(this, httpMethod, { resource: this, httpMethod, integration, options }); } @@ -265,7 +265,7 @@ export class RestApi extends RestApiRef implements IRestApiResource { */ public executeApiArn(method: string = '*', path: string = '/*', stage: string = '*') { if (!path.startsWith('/')) { - throw new Error(`"path" must begin with a "/": ${path}'`); + throw new Error(`"path" must begin with a "/": '${path}'`); } return cdk.Arn.fromComponents({ @@ -316,9 +316,6 @@ export class RestApi extends RestApiRef implements IRestApiResource { new cdk.Output(this, 'Endpoint', { value: this.urlForPath() }); } else { - if (props.deployOptions) { - throw new Error(`Cannot set 'deployStageOptions' if 'deploy' is disabled`); - } if (props.deployOptions) { throw new Error(`Cannot set 'deployOptions' if 'deploy' is disabled`); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index f5bac63fefb5a..faf1d4f2935a0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -134,15 +134,18 @@ export class Stage extends cdk.Construct { constructor(parent: cdk.Construct, id: string, props: StageProps) { super(parent, id); - let cacheClusterSize; - if (props.cacheClusterEnabled) { - cacheClusterSize = props.cacheClusterSize || '0.5'; - } else { - if (props.cacheClusterSize) { - throw new Error(`Cannot specify cacheClusterSize if cacheCluster is not enabled`); + const methodSettings = this.renderMethodSettings(props); + + // enable cache cluster if cacheClusterSize is set + if (props.cacheClusterSize !== undefined) { + if (props.cacheClusterEnabled === undefined) { + props.cacheClusterEnabled = true; + } else if (props.cacheClusterEnabled === false) { + throw new Error(`Cannot set "cacheClusterSize" to ${props.cacheClusterSize} and "cacheClusterEnabled" to "false"`); } } + const cacheClusterSize = props.cacheClusterEnabled ? (props.cacheClusterSize || '0.5') : undefined; const resource = new cloudformation.StageResource(this, 'Resource', { stageName: props.stageName || 'prod', cacheClusterEnabled: props.cacheClusterEnabled, @@ -153,7 +156,7 @@ export class Stage extends cdk.Construct { description: props.description, documentationVersion: props.documentationVersion, variables: props.variables, - methodSettings: this.renderMethodSettings(props), + methodSettings, }); this.stageName = resource.ref; @@ -165,6 +168,9 @@ export class Stage extends cdk.Construct { * @param path The resource path */ public urlForPath(path: string = '/') { + if (!path.startsWith('/')) { + throw new Error(`Path must begin with "/": ${path}`); + } return `https://${this.restApi.restApiId}.execute-api.${new cdk.AwsRegion()}.amazonaws.com/${this.stageName}${path}`; } @@ -199,8 +205,10 @@ export class Stage extends cdk.Construct { function renderEntry(path: string, options: MethodDeploymentOptions): cloudformation.StageResource.MethodSettingProperty { if (options.cachingEnabled) { - if (!props.cacheClusterEnabled) { - throw new Error(`Cannot enable caching for method ${path} since cache cluster is not enabled on stage`); + if (props.cacheClusterEnabled === undefined) { + props.cacheClusterEnabled = true; + } else if (props.cacheClusterEnabled === false) { + throw new Error(`Cannot enable caching for method ${path} since cache cluster is disabled on stage`); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json new file mode 100644 index 0000000000000..2547e4350fd57 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -0,0 +1,978 @@ +{ + "Resources": { + "HandlerServiceRoleFCDC14AE": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Handler886CB40B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function handlerCode(event, _, callback) {\n return callback(undefined, {\n isBase64Encoded: false,\n statusCode: 200,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(event)\n });\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "HandlerServiceRoleFCDC14AE", + "Arn" + ] + }, + "Runtime": "nodejs6.10" + }, + "DependsOn": [ + "HandlerServiceRoleFCDC14AE" + ] + }, + "HandlerApiPermissionGETbooksF653702A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Handler886CB40B" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + "Ref": "booksapiDeploymentStageprod55D8E03E" + }, + "/GET/books" + ] + ] + } + ] + ] + } + } + }, + "HandlerApiPermissionTestGETbooks30C948ED": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Handler886CB40B" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + "test-invoke-stage/GET/books" + ] + ] + } + } + }, + "HandlerApiPermissionPOSTbooksC036D130": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Handler886CB40B" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + "Ref": "booksapiDeploymentStageprod55D8E03E" + }, + "/POST/books" + ] + ] + } + ] + ] + } + } + }, + "HandlerApiPermissionTestPOSTbooks9A832525": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Handler886CB40B" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + "test-invoke-stage/POST/books" + ] + ] + } + } + }, + "HandlerApiPermissionGETbooksbookid1FAFFB75": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Handler886CB40B" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + "Ref": "booksapiDeploymentStageprod55D8E03E" + }, + "/GET/books/{book_id}" + ] + ] + } + ] + ] + } + } + }, + "HandlerApiPermissionTestGETbooksbookidE99E2659": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Handler886CB40B" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + "test-invoke-stage/GET/books/{book_id}" + ] + ] + } + } + }, + "HandlerApiPermissionDELETEbooksbookidCD656897": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Handler886CB40B" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + "Ref": "booksapiDeploymentStageprod55D8E03E" + }, + "/DELETE/books/{book_id}" + ] + ] + } + ] + ] + } + } + }, + "HandlerApiPermissionTestDELETEbooksbookidB283D137": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Handler886CB40B" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + "test-invoke-stage/DELETE/books/{book_id}" + ] + ] + } + } + }, + "HelloServiceRole1E55EA16": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Hello4A628BD4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function helloCode(_event, _context, callback) {\n return callback(undefined, {\n statusCode: 200,\n body: 'hello, world!'\n });\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "HelloServiceRole1E55EA16", + "Arn" + ] + }, + "Runtime": "nodejs6.10" + }, + "DependsOn": [ + "HelloServiceRole1E55EA16" + ] + }, + "HelloApiPermissionGET36D249D1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Hello4A628BD4" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + "Ref": "booksapiDeploymentStageprod55D8E03E" + }, + "/GET/" + ] + ] + } + ] + ] + } + } + }, + "HelloApiPermissionTestGETBC2F4D5D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Hello4A628BD4" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "execute-api", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "booksapiE1885304" + }, + "/", + "test-invoke-stage/GET/" + ] + ] + } + } + }, + "booksapiE1885304": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "books-api" + } + }, + "booksapiDeployment308B08F11e0205e1dd01621f043d0869c2d9f429": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "booksapiGETF514386E", + "booksapibooks97D84727", + "booksapibooksGETA776447A", + "booksapibooksPOSTF6C6559D", + "booksapibooksbookid5264BCA2", + "booksapibooksbookidGETCCE21986", + "booksapibooksbookidDELETE214F4059" + ], + "DeletionPolicy": "Retain" + }, + "booksapiDeploymentStageprod55D8E03E": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "DeploymentId": { + "Ref": "booksapiDeployment308B08F11e0205e1dd01621f043d0869c2d9f429" + }, + "StageName": "prod" + } + }, + "booksapiCloudWatchRole089CB225": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "booksapiAccountDBA98FB9": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "booksapiCloudWatchRole089CB225", + "Arn" + ] + } + }, + "DependsOn": [ + "booksapiE1885304" + ] + }, + "booksapiGETF514386E": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Fn::GetAtt": [ + "booksapiE1885304", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + "Ref": "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "booksapibooks97D84727": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "booksapiE1885304", + "RootResourceId" + ] + }, + "PathPart": "books", + "RestApiId": { + "Ref": "booksapiE1885304" + } + } + }, + "booksapibooksGETA776447A": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "booksapibooks97D84727" + }, + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + "Ref": "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Handler886CB40B", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "booksapibooksPOSTF6C6559D": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "booksapibooks97D84727" + }, + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + "Ref": "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Handler886CB40B", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "booksapibooksbookid5264BCA2": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Ref": "booksapibooks97D84727" + }, + "PathPart": "{book_id}", + "RestApiId": { + "Ref": "booksapiE1885304" + } + } + }, + "booksapibooksbookidGETCCE21986": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "booksapibooksbookid5264BCA2" + }, + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + "Ref": "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Handler886CB40B", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "booksapibooksbookidDELETE214F4059": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "DELETE", + "ResourceId": { + "Ref": "booksapibooksbookid5264BCA2" + }, + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + "Ref": "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Handler886CB40B", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + } + }, + "Outputs": { + "booksapiEndpointE230E8D5": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "booksapiE1885304" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/", + { + "Ref": "booksapiDeploymentStageprod55D8E03E" + }, + "/" + ] + ] + }, + "Export": { + "Name": "restapi-books-example:booksapiEndpointE230E8D5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts index 35dea9aff516b..4384d20783564 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts @@ -19,15 +19,15 @@ class BookStack extends cdk.Stack { })); const api = new apigw.RestApi(this, 'books-api', { defaultIntegration: echo }); - api.addMethod('GET', hello); + api.onMethod('GET', hello); const books = api.addResource('books'); - books.addMethod('GET'); - books.addMethod('POST'); + books.onMethod('GET'); + books.onMethod('POST'); const book = books.addResource('{book_id}'); - book.addMethod('GET'); - book.addMethod('DELETE'); + book.onMethod('GET'); + book.onMethod('DELETE'); } } @@ -35,7 +35,7 @@ class BookApp extends cdk.App { constructor(argv: string[]) { super(argv); - new BookStack(this, 'books'); + new BookStack(this, 'restapi-books-example'); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json index 8b88a6147b73d..58c603a6cddd7 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -6,26 +6,27 @@ "Name": "my-api" } }, - "myapiLatestDeployment24E142F763611a77ac8e9dca00e384f72144a2c9": { + "myapiDeployment92F2CB49916eaecf87f818f1e175215b8d086029": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "myapi4C7BF186" - } + }, + "Description": "Automatically created by the RestApi construct" }, "DependsOn": [ "myapiGETF990CE3C" ], "DeletionPolicy": "Retain" }, - "myapiAutoDeployStageprod98CECBB1": { + "myapiDeploymentStageprod298F01AF": { "Type": "AWS::ApiGateway::Stage", "Properties": { "RestApiId": { "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiLatestDeployment24E142F763611a77ac8e9dca00e384f72144a2c9" + "Ref": "myapiDeployment92F2CB49916eaecf87f818f1e175215b8d086029" }, "StageName": "prod" } @@ -121,7 +122,7 @@ }, ".amazonaws.com/", { - "Ref": "myapiAutoDeployStageprod98CECBB1" + "Ref": "myapiDeploymentStageprod298F01AF" }, "/" ] diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts index 9e0eff148e610..ce9f182480901 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts @@ -8,6 +8,6 @@ const stack = new cdk.Stack(app, 'test-apigateway-restapi-defaults'); const api = new apigateway.RestApi(stack, 'my-api'); // at least one method is required -api.addMethod('GET'); +api.onMethod('GET'); process.stdout.write(app.run()); \ No newline at end of file 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 d2fb767099161..24b0cedfd60d1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -6,12 +6,13 @@ "Name": "my-api" } }, - "myapiLatestDeployment24E142F7620808ef5f957f2d97015d80d9278fe8": { + "myapiDeployment92F2CB49f9d1ede876fcb76aa1d523f34f91d373": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "myapi4C7BF186" - } + }, + "Description": "Automatically created by the RestApi construct" }, "DependsOn": [ "myapiv113487378", @@ -27,7 +28,7 @@ ], "DeletionPolicy": "Retain" }, - "myapiAutoDeployStagebetaF170BD78": { + "myapiDeploymentStagebeta96434BEB": { "Type": "AWS::ApiGateway::Stage", "Properties": { "RestApiId": { @@ -36,7 +37,7 @@ "CacheClusterEnabled": true, "CacheClusterSize": "0.5", "DeploymentId": { - "Ref": "myapiLatestDeployment24E142F7620808ef5f957f2d97015d80d9278fe8" + "Ref": "myapiDeployment92F2CB49f9d1ede876fcb76aa1d523f34f91d373" }, "Description": "beta stage", "MethodSettings": [ @@ -434,7 +435,7 @@ "MyHandlerServiceRoleFFA06653" ] }, - "MyHandlerTokenTOKEN43F46AF4D": { + "MyHandlerApiPermissionGETv1toys8E10C024": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -471,7 +472,7 @@ "", [ { - "Ref": "myapiAutoDeployStagebetaF170BD78" + "Ref": "myapiDeploymentStagebeta96434BEB" }, "/GET/v1/toys" ] @@ -482,7 +483,7 @@ } } }, - "MyHandlerTokenTOKEN5853A4C5C": { + "MyHandlerApiPermissionTestGETv1toys499738A6": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -520,7 +521,7 @@ } } }, - "MyHandlerTokenTOKEN62E0733C0": { + "MyHandlerApiPermissionGETv1books376A9081": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -557,7 +558,7 @@ "", [ { - "Ref": "myapiAutoDeployStagebetaF170BD78" + "Ref": "myapiDeploymentStagebeta96434BEB" }, "/GET/v1/books" ] @@ -568,7 +569,7 @@ } } }, - "MyHandlerTokenTOKEN7978BDF6A": { + "MyHandlerApiPermissionTestGETv1booksB64C41EB": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -606,7 +607,7 @@ } } }, - "MyHandlerTokenTOKEN8BF01E10D": { + "MyHandlerApiPermissionPOSTv1booksAC487705": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -643,7 +644,7 @@ "", [ { - "Ref": "myapiAutoDeployStagebetaF170BD78" + "Ref": "myapiDeploymentStagebeta96434BEB" }, "/POST/v1/books" ] @@ -654,7 +655,7 @@ } } }, - "MyHandlerTokenTOKEN9636E43E2": { + "MyHandlerApiPermissionTestPOSTv1books6E15773F": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -709,7 +710,7 @@ }, ".amazonaws.com/", { - "Ref": "myapiAutoDeployStagebetaF170BD78" + "Ref": "myapiDeploymentStagebeta96434BEB" }, "/" ] diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index 739afd2dd93ff..6c7268970994e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -32,16 +32,16 @@ class Test extends cdk.Stack { const integration = new apigateway.LambdaIntegration(handler); const toys = v1.addResource('toys'); - toys.addMethod('GET', integration); - toys.addMethod('POST'); - toys.addMethod('PUT'); + toys.onMethod('GET', integration); + toys.onMethod('POST'); + toys.onMethod('PUT'); const appliances = v1.addResource('appliances'); - appliances.addMethod('GET'); + appliances.onMethod('GET'); const books = v1.addResource('books'); - books.addMethod('GET', integration); - books.addMethod('POST', integration); + books.onMethod('GET', integration); + books.onMethod('POST', integration); function handlerCode(event: any, _: any, callback: any) { return callback(undefined, { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts b/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts deleted file mode 100644 index 02bf33f381952..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert'; -import cdk = require('@aws-cdk/cdk'); -import { App, Stack } from '@aws-cdk/cdk'; -import { Test } from 'nodeunit'; -import apigateway = require('../lib'); - -// tslint:disable:max-line-length -// tslint:disable:object-literal-key-quotes - -export = { - 'minimal setup'(test: Test) { - const stack = new cdk.Stack(); - - new apigateway.RestApi(stack, 'my-api'); - - expect(stack).toMatch({ - "Resources": { - "myapi4C7BF186": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "my-api" - } - }, - "myapiDeployment92F2CB49": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "myapi4C7BF186" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DeletionPolicy": "Retain" - }, - "myapiDeploymentStageprod298F01AF": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "myapi4C7BF186" - }, - "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" - }, - "StageName": "prod" - } - }, - "myapiCloudWatchRole095452E5": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn", - ":", - { - "Ref": "AWS::Partition" - }, - ":", - "iam", - ":", - "", - ":", - "aws", - ":", - "policy", - "/", - "service-role/AmazonAPIGatewayPushToCloudWatchLogs" - ] - ] - } - ] - } - }, - "myapiAccountEC421A0A": { - "Type": "AWS::ApiGateway::Account", - "Properties": { - "CloudWatchRoleArn": { - "Fn::GetAtt": [ - "myapiCloudWatchRole095452E5", - "Arn" - ] - } - }, - "DependsOn": [ - "myapi4C7BF186" - ] - } - }, - "Outputs": { - "myapiEndpoint3628AFE3": { - "Value": { - "Fn::Join": [ - "", - [ - "https://", - { - "Ref": "myapi4C7BF186" - }, - ".execute-api.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com/", - { - "Ref": "myapiDeploymentStageprod298F01AF" - }, - "/" - ] - ] - }, - "Export": { - "Name": "myapiEndpoint3628AFE3" - } - } - } - }); - - test.done(); - }, - - '"name" is defaulted to construct id'(test: Test) { - const stack = new cdk.Stack(); - new apigateway.RestApi(stack, 'my-first-api', { - deploy: false, - cloudWatchRole: false, - }); - expect(stack).toMatch({ - "Resources": { - "myfirstapi5827A5AA": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "my-first-api" - } - } - } - }); - - test.done(); - }, - - 'fails in synthesis if there are no methods'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'my-stack'); - - const api = new apigateway.RestApi(stack, 'API'); - - api.addResource('foo'); - api.addResource('bar').addResource('goo'); - - test.throws(() => app.synthesizeStack(stack.name), /The REST API doesn't contain any methods/); - test.done(); - }, - - 'newChildResource can be used on IRestApiResource to form a tree'(test: Test) { - const stack = new cdk.Stack(); - const api = new apigateway.RestApi(stack, 'restapi', { - deploy: false, - cloudWatchRole: false, - restApiName: 'my-rest-api' - }); - - const foo = api.addResource('foo'); - api.addResource('bar'); - - foo.addResource('{hello}'); - - expect(stack).toMatch({ - "Resources": { - "restapiC5611D27": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "my-rest-api" - } - }, - "restapifooF697E056": { - "Type": "AWS::ApiGateway::Resource", - "Properties": { - "ParentId": { - "Fn::GetAtt": [ - "restapiC5611D27", - "RootResourceId" - ] - }, - "PathPart": "foo", - "RestApiId": { - "Ref": "restapiC5611D27" - } - } - }, - "restapifoohello6E7449A9": { - "Type": "AWS::ApiGateway::Resource", - "Properties": { - "ParentId": { - "Ref": "restapifooF697E056" - }, - "PathPart": "{hello}", - "RestApiId": { - "Ref": "restapiC5611D27" - } - } - }, - "restapibar1F6A2522": { - "Type": "AWS::ApiGateway::Resource", - "Properties": { - "ParentId": { - "Fn::GetAtt": [ - "restapiC5611D27", - "RootResourceId" - ] - }, - "PathPart": "bar", - "RestApiId": { - "Ref": "restapiC5611D27" - } - } - } - } - }); - - test.done(); - }, - - 'resourcePath returns the full path of the resource within the API'(test: Test) { - const stack = new cdk.Stack(); - - const api = new apigateway.RestApi(stack, 'restapi'); - - const r1 = api.addResource('r1'); - const r11 = r1.addResource('r1_1'); - const r12 = r1.addResource('r1_2'); - const r121 = r12.addResource('r1_2_1'); - const r2 = api.addResource('r2'); - - test.deepEqual(api.resourcePath, '/'); - test.deepEqual(r1.resourcePath, '/r1'); - test.deepEqual(r11.resourcePath, '/r1/r1_1'); - test.deepEqual(r12.resourcePath, '/r1/r1_2'); - test.deepEqual(r121.resourcePath, '/r1/r1_2/r1_2_1'); - test.deepEqual(r2.resourcePath, '/r2'); - - test.done(); - }, - - 'resource path cannot use "/"'(test: Test) { - const stack = new cdk.Stack(); - const api = new apigateway.RestApi(stack, 'restapi'); - test.throws(() => api.addResource('foo/')); - test.done(); - }, - - 'fails if autoDeployStageOptions is set with autoDeploy disabled'(test: Test) { - const stack = new cdk.Stack(); - test.throws(() => { - new apigateway.RestApi(stack, 'myapi', { deploy: false, deployOptions: { stageName: 'foo' }}); - }, `Cannot set 'autoDeployStageOptions' if 'autoDeploy' is disabled`); - test.done(); - }, - - 'fails if autoDeployOptions is set with autoDeploy disabled'(test: Test) { - const stack = new cdk.Stack(); - test.throws(() => { - new apigateway.RestApi(stack, 'myapi', { deploy: false, deployOptions: { cachingEnabled: true }}); - }, `Cannot set 'autoDeployOptions' if 'autoDeploy' is disabled`); - test.done(); - }, - - 'CloudWatch role is created for API Gateway'(test: Test) { - const stack = new cdk.Stack(); - new apigateway.RestApi(stack, 'myapi'); - expect(stack).to(haveResource('AWS::IAM::Role')); - expect(stack).to(haveResource('AWS::ApiGateway::Account')); - test.done(); - } -}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts new file mode 100644 index 0000000000000..02a4917d3f8dd --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -0,0 +1,178 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'minimal setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + api.onMethod('GET'); + + // WHEN + new apigateway.Deployment(stack, 'deployment', { api }); + + // THEN + expect(stack).toMatch({ + Resources: { + apiGETECF0BD67: { + Type: "AWS::ApiGateway::Method", + Properties: { + HttpMethod: "GET", + ResourceId: { + "Fn::GetAtt": [ + "apiC8550315", + "RootResourceId" + ] + }, + RestApiId: { + Ref: "apiC8550315" + }, + AuthorizationType: "NONE", + Integration: { + Type: "MOCK" + } + } + }, + apiC8550315: { + Type: "AWS::ApiGateway::RestApi", + Properties: { + Name: "api" + } + }, + deployment33381975: { + Type: "AWS::ApiGateway::Deployment", + Properties: { + RestApiId: { + Ref: "apiC8550315" + } + }, + DeletionPolicy: "Retain" + } + } + }); + + test.done(); + }, + + '"retainDeployments" can be used to control the deletion policy of the resource (default is "Retain")'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + api.onMethod('GET'); + + // WHEN + new apigateway.Deployment(stack, 'deployment', { api, retainDeployments: false }); + + // THEN + expect(stack).toMatch({ + Resources: { + apiGETECF0BD67: { + Type: "AWS::ApiGateway::Method", + Properties: { + HttpMethod: "GET", + ResourceId: { + "Fn::GetAtt": [ + "apiC8550315", + "RootResourceId" + ] + }, + RestApiId: { + Ref: "apiC8550315" + }, + AuthorizationType: "NONE", + Integration: { + Type: "MOCK" + } + } + }, + apiC8550315: { + Type: "AWS::ApiGateway::RestApi", + Properties: { + Name: "api" + } + }, + deployment33381975: { + Type: "AWS::ApiGateway::Deployment", + Properties: { + RestApiId: { + Ref: "apiC8550315" + } + } + } + } + }); + + test.done(); + }, + + '"description" can be set on the deployment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + api.onMethod('GET'); + + // WHEN + new apigateway.Deployment(stack, 'deployment', { api, description: 'this is my deployment' }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Deployment', { + Description: 'this is my deployment' + })); + + test.done(); + }, + + '"addToLogicalId" will "salt" the logical ID of the deployment resource'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + const deployment = new apigateway.Deployment(stack, 'deployment', { api }); + api.onMethod('GET'); + + // default logical ID (with no "salt") + test.ok(synthesize().Resources.deployment33381975); + + // adding some salt + deployment.addToLogicalId({ foo: 123 }); // add some data to the logical ID + + // the logical ID changed + const template = synthesize(); + test.ok(!template.Resources.deployment33381975, 'old resource id deleted'); + test.ok(template.Resources.deployment33381975427670fa9e4148dc851927485bdf36a5, 'new resource is created'); + + // tokens supported, and are resolved upon synthesis + const value = 'hello hello'; + deployment.addToLogicalId({ foo: new cdk.Token(() => value) }); + + const template2 = synthesize(); + test.ok(template2.Resources.deployment33381975a12dfe81474913364dc31c06e37f9449); + + test.done(); + + function synthesize() { + stack.validateTree(); + return stack.toCloudFormation(); + } + }, + + '"addDependency" can be used to add a resource as a dependency'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); + const deployment = new apigateway.Deployment(stack, 'deployment', { api }); + api.onMethod('GET'); + + const dep = new cdk.Resource(stack, 'MyResource', { type: 'foo' }); + + // WHEN + deployment.addDependency(dep); + + expect(stack).to(haveResource('AWS::ApiGateway::Deployment', { + DependsOn: [ "MyResource" ], + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.http.ts b/packages/@aws-cdk/aws-apigateway/test/test.http.ts new file mode 100644 index 0000000000000..ac8601cead848 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.http.ts @@ -0,0 +1,57 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'minimal setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api'); + + // WHEN + const integ = new apigateway.HttpIntegration('http://foo/bar'); + + api.onMethod('GET', integ); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: "GET", + Type: "HTTP_PROXY", + Uri: "http://foo/bar" + } + })); + + test.done(); + }, + + 'options can be passed via props'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api'); + + // WHEN + const integ = new apigateway.HttpIntegration('http://foo/bar', { + httpMethod: 'POST', + proxy: false, + options: { + cacheNamespace: 'hey' + } + }); + + api.onMethod('GET', integ); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Integration: { + CacheNamespace: "hey", + IntegrationHttpMethod: "POST", + Type: "HTTP", + Uri: "http://foo/bar" + } + })); + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts new file mode 100644 index 0000000000000..7f1a7aef5744e --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts @@ -0,0 +1,148 @@ +import { expect, haveResource, not } from '@aws-cdk/assert'; +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'minimal setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api'); + const handler = new lambda.Function(stack, 'Handler', { + runtime: lambda.Runtime.Python27, + handler: 'boom', + code: lambda.Code.inline('foo') + }); + + // WHEN + const integ = new apigateway.LambdaIntegration(handler); + api.onMethod('GET', integ); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: "POST", + Type: "AWS_PROXY", + Uri: { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + Ref: "AWS::Partition" + }, + ":", + "apigateway", + ":", + { + Ref: "AWS::Region" + }, + ":", + "lambda", + ":", + "path", + "/", + { + "Fn::Join": [ + "", + [ + "2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Handler886CB40B", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + })); + test.done(); + }, + + '"allowTestInvoke" can be used to disallow calling the API from the test UI'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Handler', { + runtime: lambda.Runtime.NodeJS610, + code: lambda.Code.inline('foo'), + handler: 'index.handler' + }); + + const api = new apigateway.RestApi(stack, 'api'); + + // WHEN + const integ = new apigateway.LambdaIntegration(fn, { allowTestInvoke: false }); + api.onMethod('GET', integ); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + SourceArn: { + "Fn::Join": [ + "", + [ + "arn", ":", { Ref: "AWS::Partition" }, ":", "execute-api", ":", { Ref: "AWS::Region" }, ":", { Ref: "AWS::AccountId" }, ":", + { Ref: "apiC8550315" }, "/", { "Fn::Join": [ "", [ { Ref: "apiDeploymentStageprod896C8101" }, "/GET/" ] ] } + ] + ] + } + })); + + expect(stack).to(not(haveResource('AWS::Lambda::Permission', { + SourceArn: { + "Fn::Join": [ + "", + [ + "arn", + ":", + { Ref: "AWS::Partition" }, + ":", + "execute-api", + ":", + { Ref: "AWS::Region" }, + ":", + { Ref: "AWS::AccountId" }, + ":", + { Ref: "apiC8550315" }, + "/", + "test-invoke-stage/GET/" + ] + ] + } + }))); + + test.done(); + }, + + '"proxy" can be used to disable proxy mode'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Handler', { + runtime: lambda.Runtime.NodeJS610, + code: lambda.Code.inline('foo'), + handler: 'index.handler' + }); + + const api = new apigateway.RestApi(stack, 'api'); + + // WHEN + const integ = new apigateway.LambdaIntegration(fn, { proxy: false }); + api.onMethod('GET', integ); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Integration: { + Type: 'AWS' + } + })); + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts new file mode 100644 index 0000000000000..c75283b5c1a45 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -0,0 +1,269 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import iam = require('@aws-cdk/aws-iam'); +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(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + + // WHEN + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api, + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: "POST", + AuthorizationType: "NONE", + Integration: { + Type: "MOCK" + } + })); + + test.done(); + }, + + 'method options can be specified'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + + // WHEN + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api, + options: { + apiKeyRequired: true, + operationName: 'MyOperation', + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Type: "AWS::ApiGateway::Method", + Properties: { + ApiKeyRequired: true, + OperationName: "MyOperation" + } + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + 'integration can be set via a property'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + + // WHEN + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api, + integration: new apigateway.AwsIntegration({ service: 's3', path: 'bucket/key' }) + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Type: "AWS::ApiGateway::Method", + Properties: { + Integration: { + IntegrationHttpMethod: "POST", + Type: "AWS", + Uri: { + "Fn::Join": [ + "", + [ + "arn", ":", { Ref: "AWS::Partition" }, ":", "apigateway", ":", + { Ref: "AWS::Region" }, ":", "s3", ":", "path", "/", "bucket/key" + ] + ] + } + } + } + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + 'use default integration from api'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const defaultIntegration = new apigateway.Integration({ type: apigateway.IntegrationType.HttpProxy, uri: 'https://amazon.com' }); + const api = new apigateway.RestApi(stack, 'test-api', { + cloudWatchRole: false, + deploy: false, + defaultIntegration + }); + + // WHEN + new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api, + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Integration: { + Type: "HTTP_PROXY", + Uri: 'https://amazon.com' + } + })); + + test.done(); + }, + + '"methodArn" returns the ARN execute-api ARN for this method in the current stage'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api'); + + // WHEN + const method = new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api, + }); + + // THEN + test.deepEqual(cdk.resolve(method.methodArn), { + "Fn::Join": [ + "", + [ + "arn", + ":", + { Ref: "AWS::Partition" }, + ":", + "execute-api", + ":", + { Ref: "AWS::Region" }, + ":", + { Ref: "AWS::AccountId" }, + ":", + { Ref: "testapiD6451F70" }, + "/", + { "Fn::Join": [ "", [ { Ref: "testapiDeploymentStageprod5C9E92A4" }, "/POST/" ] ] } + ] + ] + }); + + test.done(); + }, + + '"testMethodArn" returns the ARN of the "test-invoke-stage" stage (console UI)'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api'); + + // WHEN + const method = new apigateway.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api, + }); + + // THEN + test.deepEqual(cdk.resolve(method.testMethodArn), { + "Fn::Join": [ + "", + [ + "arn", + ":", + { Ref: "AWS::Partition" }, + ":", + "execute-api", + ":", + { Ref: "AWS::Region" }, + ":", + { Ref: "AWS::AccountId" }, + ":", + { Ref: "testapiD6451F70" }, + "/", + "test-invoke-stage/POST/" + ] + ] + }); + + test.done(); + }, + + '"methodArn" fails if the API does not have a deployment stage'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const method = new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', resource: api }); + + // WHEN + THEN + test.throws(() => method.methodArn, + /There is no stage associated with this restApi. Either use `autoDeploy` or explicitly assign `deploymentStage`/); + + test.done(); + }, + + 'integration "credentialsRole" can be used to assume a role when calling backend'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const role = new iam.Role(stack, 'MyRole', { assumedBy: new cdk.ServicePrincipal('foo') }); + + // WHEN + api.onMethod('GET', new apigateway.Integration({ + type: apigateway.IntegrationType.AwsProxy, + options: { + credentialsRole: role + } + })); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Integration: { + Credentials: { "Fn::GetAtt": [ "MyRoleF48FFE04", "Arn" ] } + } + })); + test.done(); + }, + + 'integration "credentialsPassthrough" can be used to passthrough user credentials to backend'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + + // WHEN + api.onMethod('GET', new apigateway.Integration({ + type: apigateway.IntegrationType.AwsProxy, + options: { + credentialsPassthrough: true + } + })); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Integration: { + Credentials: { "Fn::Join": [ "", [ "arn", ":", { Ref: "AWS::Partition" }, ":", "iam", ":", "", ":", "*", ":", "user", "/", "*" ] ] } + } + })); + test.done(); + }, + + 'integration "credentialsRole" and "credentialsPassthrough" are mutually exclusive'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); + const role = new iam.Role(stack, 'MyRole', { assumedBy: new cdk.ServicePrincipal('foo') }); + + // WHEN + const integration = new apigateway.Integration({ + type: apigateway.IntegrationType.AwsProxy, + options: { + credentialsPassthrough: true, + credentialsRole: role + } + }); + + // THEN + test.throws(() => api.onMethod('GET', integration), /'credentialsPassthrough' and 'credentialsRole' are mutually exclusive/); + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts new file mode 100644 index 0000000000000..220c3cd2b8d07 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -0,0 +1,523 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { App, Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +// tslint:disable:max-line-length + +export = { + 'minimal setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = new apigateway.RestApi(stack, 'my-api'); + api.onMethod('GET'); // must have at least one method + + // THEN + expect(stack).toMatch({ + Resources: { + myapi4C7BF186: { + Type: "AWS::ApiGateway::RestApi", + Properties: { + Name: "my-api" + } + }, + myapiGETF990CE3C: { + Type: "AWS::ApiGateway::Method", + Properties: { + HttpMethod: "GET", + ResourceId: { + "Fn::GetAtt": [ + "myapi4C7BF186", + "RootResourceId" + ] + }, + RestApiId: { + Ref: "myapi4C7BF186" + }, + AuthorizationType: "NONE", + Integration: { + Type: "MOCK" + } + } + }, + myapiDeployment92F2CB49916eaecf87f818f1e175215b8d086029: { + Type: "AWS::ApiGateway::Deployment", + Properties: { + RestApiId: { + Ref: "myapi4C7BF186" + }, + Description: "Automatically created by the RestApi construct" + }, + DependsOn: [ "myapiGETF990CE3C" ], + DeletionPolicy: "Retain" + }, + myapiDeploymentStageprod298F01AF: { + Type: "AWS::ApiGateway::Stage", + Properties: { + RestApiId: { + Ref: "myapi4C7BF186" + }, + DeploymentId: { + Ref: "myapiDeployment92F2CB49916eaecf87f818f1e175215b8d086029" + }, + StageName: "prod" + } + }, + myapiCloudWatchRole095452E5: { + Type: "AWS::IAM::Role", + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "apigateway.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + ManagedPolicyArns: [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + Ref: "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + myapiAccountEC421A0A: { + Type: "AWS::ApiGateway::Account", + Properties: { + CloudWatchRoleArn: { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + DependsOn: [ + "myapi4C7BF186" + ] + } + }, + Outputs: { + myapiEndpoint3628AFE3: { + Value: { + "Fn::Join": [ + "", + [ + "https://", + { + Ref: "myapi4C7BF186" + }, + ".execute-api.", + { + Ref: "AWS::Region" + }, + ".amazonaws.com/", + { + Ref: "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + }, + Export: { + Name: "myapiEndpoint3628AFE3" + } + } + } + }); + + test.done(); + }, + + '"name" is defaulted to construct id'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = new apigateway.RestApi(stack, 'my-first-api', { + deploy: false, + cloudWatchRole: false, + }); + + api.onMethod('GET'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { + Name: "my-first-api" + })); + + test.done(); + }, + + 'fails in synthesis if there are no methods'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'my-stack'); + const api = new apigateway.RestApi(stack, 'API'); + + // WHEN + api.addResource('foo'); + api.addResource('bar').addResource('goo'); + + // THEN + test.throws(() => app.synthesizeStack(stack.name), /The REST API doesn't contain any methods/); + test.done(); + }, + + '"addResource" can be used on "IRestApiResource" to form a tree'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'restapi', { + deploy: false, + cloudWatchRole: false, + restApiName: 'my-rest-api' + }); + + api.onMethod('GET'); + + // WHEN + const foo = api.addResource('foo'); + api.addResource('bar'); + foo.addResource('{hello}'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + PathPart: "foo", + ParentId: { "Fn::GetAtt": [ "restapiC5611D27", "RootResourceId"] } + })); + + expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + PathPart: "bar", + ParentId: { "Fn::GetAtt": [ "restapiC5611D27", "RootResourceId"] } + })); + + expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + PathPart: "{hello}", + ParentId: { Ref: "restapifooF697E056" } + })); + + test.done(); + }, + + '"addMethod" can be used to add methods to resources'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const api = new apigateway.RestApi(stack, 'restapi', { deploy: false, cloudWatchRole: false }); + const r1 = api.addResource('r1'); + + // WHEN + api.onMethod('GET'); + r1.onMethod('POST'); + + // THEN + expect(stack).toMatch({ + Resources: { + restapiC5611D27: { + Type: "AWS::ApiGateway::RestApi", + Properties: { + Name: "restapi" + } + }, + restapir1CF2997EA: { + Type: "AWS::ApiGateway::Resource", + Properties: { + ParentId: { + "Fn::GetAtt": [ + "restapiC5611D27", + "RootResourceId" + ] + }, + PathPart: "r1", + RestApiId: { + Ref: "restapiC5611D27" + } + } + }, + restapir1POST766920C4: { + Type: "AWS::ApiGateway::Method", + Properties: { + HttpMethod: "POST", + ResourceId: { + Ref: "restapir1CF2997EA" + }, + RestApiId: { + Ref: "restapiC5611D27" + }, + AuthorizationType: "NONE", + Integration: { + Type: "MOCK" + } + } + }, + restapiGET6FC1785A: { + Type: "AWS::ApiGateway::Method", + Properties: { + HttpMethod: "GET", + ResourceId: { + "Fn::GetAtt": [ + "restapiC5611D27", + "RootResourceId" + ] + }, + RestApiId: { + Ref: "restapiC5611D27" + }, + AuthorizationType: "NONE", + Integration: { + Type: "MOCK" + } + } + } + } + }); + + test.done(); + }, + + 'resourcePath returns the full path of the resource within the API'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'restapi'); + + // WHEN + const r1 = api.addResource('r1'); + const r11 = r1.addResource('r1_1'); + const r12 = r1.addResource('r1_2'); + const r121 = r12.addResource('r1_2_1'); + const r2 = api.addResource('r2'); + + // THEN + test.deepEqual(api.resourcePath, '/'); + test.deepEqual(r1.resourcePath, '/r1'); + test.deepEqual(r11.resourcePath, '/r1/r1_1'); + test.deepEqual(r12.resourcePath, '/r1/r1_2'); + test.deepEqual(r121.resourcePath, '/r1/r1_2/r1_2_1'); + test.deepEqual(r2.resourcePath, '/r2'); + test.done(); + }, + + 'resource path part validation'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'restapi'); + + // THEN + test.throws(() => api.addResource('foo/')); + api.addResource('boom-bam'); + test.throws(() => api.addResource('illegal()')); + api.addResource('{foo}'); + test.throws(() => api.addResource('foo{bar}')); + test.done(); + }, + + 'fails if "deployOptions" is set with "deploy" disabled'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + test.throws(() => new apigateway.RestApi(stack, 'myapi', { + deploy: false, + deployOptions: { cachingEnabled: true } + }), /Cannot set 'deployOptions' if 'deploy' is disabled/); + + test.done(); + }, + + 'CloudWatch role is created for API Gateway'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'myapi'); + api.onMethod('GET'); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Role')); + expect(stack).to(haveResource('AWS::ApiGateway::Account')); + test.done(); + }, + + 'import/export'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const imported = apigateway.RestApi.import(stack, 'imported-api', { + restApiId: new apigateway.RestApiId('api-rxt4498f') + }); + const exported = imported.export(); + + // THEN + expect(stack).toMatch({ + Outputs: { + importedapiRestApiIdC00F155A: { + Value: "api-rxt4498f", + Export: { + Name: "importedapiRestApiIdC00F155A" + } + } + } + }); + test.deepEqual(cdk.resolve(exported), { restApiId: { 'Fn::ImportValue': 'importedapiRestApiIdC00F155A' } }); + test.done(); + }, + + '"url" and "urlForPath" return the URL endpoints of the deployed API'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api'); + api.onMethod('GET'); + + // THEN + test.deepEqual(cdk.resolve(api.url), { 'Fn::Join': + [ '', + [ 'https://', + { Ref: 'apiC8550315' }, + '.execute-api.', + { Ref: 'AWS::Region' }, + '.amazonaws.com/', + { Ref: 'apiDeploymentStageprod896C8101' }, + '/' ] ] }); + test.deepEqual(cdk.resolve(api.urlForPath('/foo/bar')), { 'Fn::Join': + [ '', + [ 'https://', + { Ref: 'apiC8550315' }, + '.execute-api.', + { Ref: 'AWS::Region' }, + '.amazonaws.com/', + { Ref: 'apiDeploymentStageprod896C8101' }, + '/foo/bar' ] ] }); + test.done(); + }, + + '"urlForPath" would not work if there is no deployment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api', { deploy: false }); + api.onMethod('GET'); + + // THEN + test.throws(() => api.url, /Cannot determine deployment stage for API from "deploymentStage". Use "deploy" or explicitly set "deploymentStage"/); + test.throws(() => api.urlForPath('/foo'), /Cannot determine deployment stage for API from "deploymentStage". Use "deploy" or explicitly set "deploymentStage"/); + test.done(); + }, + + '"urlForPath" requires that path will begin with "/"'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api'); + api.onMethod('GET'); + + // THEN + test.throws(() => api.urlForPath('foo'), /Path must begin with \"\/\": foo/); + test.done(); + }, + + '"executeApiArn" returns the execute-api ARN for a resource/method'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api'); + api.onMethod('GET'); + + // WHEN + const arn = api.executeApiArn('method', '/path', 'stage'); + + // THEN + test.deepEqual(cdk.resolve(arn), { 'Fn::Join': + [ '', + [ 'arn', + ':', + { Ref: 'AWS::Partition' }, + ':', + 'execute-api', + ':', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + { Ref: 'apiC8550315' }, + '/', + 'stage/method/path' ] ] }); + test.done(); + }, + + '"executeApiArn" path must begin with "/"'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'api'); + api.onMethod('GET'); + + // THEN + test.throws(() => api.executeApiArn('method', 'hey-path', 'stage'), /"path" must begin with a "\/": 'hey-path'/); + test.done(); + }, + + '"endpointTypes" can be used to specify endpoint configuration for the api'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = new apigateway.RestApi(stack, 'api', { + endpointTypes: [ apigateway.EndpointType.Edge, apigateway.EndpointType.Private ] + }); + + api.onMethod('GET'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { + EndpointConfiguration: { + Types: [ + "EDGE", + "PRIVATE" + ] + } + })); + test.done(); + }, + + '"cloneFrom" can be used to clone an existing API'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const cloneFrom = apigateway.RestApi.import(stack, 'RestApi', { + restApiId: new apigateway.RestApiId('foobar') + }); + + // WHEN + const api = new apigateway.RestApi(stack, 'api', { + cloneFrom + }); + + api.onMethod('GET'); + + expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { + CloneFrom: "foobar", + Name: "api" + })); + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts new file mode 100644 index 0000000000000..510f042fddce5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts @@ -0,0 +1,247 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import apigateway = require('../lib'); + +export = { + 'minimal setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); + api.onMethod('GET'); + + // WHEN + new apigateway.Stage(stack, 'my-stage', { deployment }); + + // THEN + expect(stack).toMatch({ + Resources: { + testapiD6451F70: { + Type: "AWS::ApiGateway::RestApi", + Properties: { + Name: "test-api" + } + }, + testapiGETD8DE4ED1: { + Type: "AWS::ApiGateway::Method", + Properties: { + HttpMethod: "GET", + ResourceId: { + "Fn::GetAtt": [ + "testapiD6451F70", + "RootResourceId" + ] + }, + RestApiId: { + Ref: "testapiD6451F70" + }, + AuthorizationType: "NONE", + Integration: { + Type: "MOCK" + } + } + }, + mydeployment71ED3B4B: { + Type: "AWS::ApiGateway::Deployment", + Properties: { + RestApiId: { + Ref: "testapiD6451F70" + } + }, + DeletionPolicy: "Retain" + }, + mystage7483BE9A: { + Type: "AWS::ApiGateway::Stage", + Properties: { + RestApiId: { + Ref: "testapiD6451F70" + }, + DeploymentId: { + Ref: "mydeployment71ED3B4B" + }, + StageName: "prod" + } + } + } + }); + + test.done(); + }, + + 'common method settings can be set at the stage level'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); + api.onMethod('GET'); + + // WHEN + new apigateway.Stage(stack, 'my-stage', { + deployment, + loggingLevel: apigateway.MethodLoggingLevel.Info, + throttlingRateLimit: 12 + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Stage', { + MethodSettings: [ + { + HttpMethod: "*", + LoggingLevel: "INFO", + ResourcePath: "/*", + ThrottlingRateLimit: 12, + } + ] + })); + + test.done(); + }, + + 'custom method settings can be set by their path'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); + api.onMethod('GET'); + + // WHEN + new apigateway.Stage(stack, 'my-stage', { + deployment, + loggingLevel: apigateway.MethodLoggingLevel.Info, + throttlingRateLimit: 12, + methodOptions: { + '/goo/bar/GET': { + loggingLevel: apigateway.MethodLoggingLevel.Error, + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Stage', { + MethodSettings: [ + { + HttpMethod: "*", + LoggingLevel: "INFO", + ResourcePath: "/*", + ThrottlingRateLimit: 12 + }, + { + HttpMethod: "GET", + LoggingLevel: "ERROR", + ResourcePath: "/~1goo~1bar" + } + ] + })); + + test.done(); + }, + + 'default "cacheClusterSize" is 0.5 (if cache cluster is enabled)'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); + api.onMethod('GET'); + + // WHEN + new apigateway.Stage(stack, 'my-stage', { + deployment, + cacheClusterEnabled: true + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Stage', { + CacheClusterEnabled: true, + CacheClusterSize: "0.5" + })); + + test.done(); + }, + + 'setting "cacheClusterSize" implies "cacheClusterEnabled"'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); + api.onMethod('GET'); + + // WHEN + new apigateway.Stage(stack, 'my-stage', { + deployment, + cacheClusterSize: '0.5' + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Stage', { + CacheClusterEnabled: true, + CacheClusterSize: "0.5" + })); + + test.done(); + }, + + 'fails when "cacheClusterEnabled" is "false" and "cacheClusterSize" is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); + api.onMethod('GET'); + + // THEN + test.throws(() => new apigateway.Stage(stack, 'my-stage', { + deployment, + cacheClusterSize: '0.5', + cacheClusterEnabled: false + }), /Cannot set "cacheClusterSize" to 0.5 and "cacheClusterEnabled" to "false"/); + + test.done(); + }, + + 'if "cachingEnabled" in method settings, implicitly enable cache cluster'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); + api.onMethod('GET'); + + // WHEN + new apigateway.Stage(stack, 'my-stage', { + deployment, + cachingEnabled: true + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Stage', { + CacheClusterEnabled: true, + CacheClusterSize: "0.5", + MethodSettings: [ + { + CachingEnabled: true, + HttpMethod: "*", + ResourcePath: "/*" + } + ], + StageName: "prod" + })); + + test.done(); + }, + + 'if caching cluster is explicitly disabled, do not auto-enable cache cluster when "cachingEnabled" is set in method options'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); + api.onMethod('GET'); + + // THEN + test.throws(() => new apigateway.Stage(stack, 'my-stage', { + cacheClusterEnabled: false, + deployment, + cachingEnabled: true + }), /Cannot enable caching for method \/\*\/\* since cache cluster is disabled on stage/); + + test.done(); + }, +}; \ No newline at end of file From d5418a00e3d04ad567aca6151f2390297788ac0b Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 4 Sep 2018 22:52:52 +0300 Subject: [PATCH 05/15] Fix event rule test --- packages/@aws-cdk/aws-events/test/test.rule.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 7ef0d9a0fd19d..2846fb6ee4802 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -211,7 +211,7 @@ export = { const t3: IEventRuleTarget = { asEventRuleTarget: () => ({ id: 'T3', arn: new cdk.Arn('ARN3') }) }; const t4: IEventRuleTarget = { asEventRuleTarget: () => ({ id: 'T4', arn: new cdk.Arn('ARN4') }) }; - const rule = new EventRule(stack, 'EventRule'); + const rule = new EventRule(stack, 'EventRule', { scheduleExpression: 'rate(1 minute)' }); // a plain string should just be stringified (i.e. double quotes added and escaped) rule.addTarget(t2, { @@ -244,6 +244,7 @@ export = { "Type": "AWS::Events::Rule", "Properties": { "State": "ENABLED", + "ScheduleExpression": "rate(1 minute)", "Targets": [ { "Arn": "ARN2", From cbdf007836518b14f0142a3d3a44006ced93e85d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 5 Sep 2018 11:11:43 +0300 Subject: [PATCH 06/15] Fix lambda test --- packages/@aws-cdk/aws-lambda/test/test.lambda.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 117049e09960a..5be65215b9575 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -255,8 +255,8 @@ export = { // GIVEN const stack = new cdk.Stack(); const fn = newTestLambda(stack); - const rule1 = new events.EventRule(stack, 'Rule'); - const rule2 = new events.EventRule(stack, 'Rule2'); + const rule1 = new events.EventRule(stack, 'Rule', { scheduleExpression: 'rate(1 minute)' }); + const rule2 = new events.EventRule(stack, 'Rule2', { scheduleExpression: 'rate(5 minutes)' }); // WHEN rule1.addTarget(fn); From 459e6d90c8997384d2f3b94788c6e8a24f519cf4 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 5 Sep 2018 11:27:34 +0300 Subject: [PATCH 07/15] fix pipeline tests --- packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 5bcff6d224cb2..ac39af06d2fef 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -262,6 +262,8 @@ export = { }); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + + // first stage must contain a Source action so we can't use it to test Lambda const stage = new codepipeline.Stage(stack, 'Stage', { pipeline }); new lambda.PipelineInvokeAction(stack, 'InvokeAction', { stage, @@ -269,7 +271,7 @@ export = { userParameters: 'foo-bar/42' }); - expect(stack).to(haveResource('AWS::CodePipeline::Pipeline', { + expect(stack, /* skip validation */ true).to(haveResource('AWS::CodePipeline::Pipeline', { "ArtifactStore": { "Location": { "Ref": "PipelineArtifactsBucket22248F97" @@ -309,7 +311,7 @@ export = { ] })); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack, /* skip validation */ true).to(haveResource('AWS::IAM::Policy', { "PolicyDocument": { "Statement": [ { From a85fbe0147db98f1450a0a299c4fe09b2f61858c Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 16 Sep 2018 14:03:30 +0300 Subject: [PATCH 08/15] Initial set of CR fixes - Default retainDeployments to "false" - Make RestApi, Stage and Deployment IDependable - Fix README --- packages/@aws-cdk/aws-apigateway/README.md | 5 ++-- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 14 ++++++--- .../@aws-cdk/aws-apigateway/lib/resource.ts | 3 -- .../aws-apigateway/lib/restapi-ref.ts | 16 +++++++++- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 17 +++++++++-- packages/@aws-cdk/aws-apigateway/lib/stage.ts | 4 ++- .../test/integ.restapi.books.expected.json | 3 +- .../test/integ.restapi.defaults.expected.json | 3 +- .../aws-apigateway/test/integ.restapi.ts | 1 + .../aws-apigateway/test/test.deployment.ts | 10 +++---- .../aws-apigateway/test/test.restapi.ts | 29 ++++++++++++++++--- .../aws-apigateway/test/test.stage.ts | 3 +- 12 files changed, 79 insertions(+), 29 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 654d82ac86b94..bbe20ee3f2d82 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -146,9 +146,8 @@ changes (i.e. a resource or method are added, configuration is changed), a new logical ID will be assigned to the deployment resource. This will cause CloudFormation to create a new deployment resource. -By default, old deployments are _retained_, which means users can use the AWS -API Gateway Web Console to point the stage to an old deployment. To disable -this behavior, set the `retainDeployments` property to `false`. +By default, old deployments are _deleted_. You can set `retainDeployments: true` +to allow users revert the stage to an old deployment. [Deployment]: https://docs.aws.amazon.com/apigateway/api-reference/resource/deployment/ [Stage]: https://docs.aws.amazon.com/apigateway/api-reference/resource/stage/ diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 6888e907c422d..9afd8723ece90 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -19,7 +19,7 @@ export interface DeploymentProps { * If this is true (default), the old API Gateway Deployment resource will not be deleted. * This will allow manually reverting back to a previous deployment in case for example * - * @default true + * @default false */ retainDeployments?: boolean; } @@ -54,10 +54,15 @@ export interface DeploymentProps { * model. Use the `addDependency(dep)` method to circumvent that. This is done * automatically for the `restApi.latestDeployment` deployment. */ -export class Deployment extends cdk.Construct { +export class Deployment extends cdk.Construct implements cdk.IDependable { public readonly deploymentId: DeploymentId; public readonly api: RestApiRef; + /** + * Allows taking a dependency on this construct. + */ + public readonly dependencyElements = new Array(); + private readonly resource: LatestDeploymentResource; constructor(parent: cdk.Construct, id: string, props: DeploymentProps) { @@ -68,12 +73,13 @@ export class Deployment extends cdk.Construct { restApiId: props.api.restApiId, }); - if (props.retainDeployments === undefined || props.retainDeployments) { + if (props.retainDeployments) { this.resource.options.deletionPolicy = cdk.DeletionPolicy.Retain; } this.api = props.api; this.deploymentId = new DeploymentId(() => this.resource.ref); + this.dependencyElements.push(this.resource); } /** @@ -117,7 +123,7 @@ class LatestDeploymentResource extends cloudformation.DeploymentResource { } }, ref: { - get: () => new cdk.Token(() => ({ Ref: this.customLogicalId })) + get: () => new cdk.CloudFormationToken(() => ({ Ref: this.customLogicalId })) }, }); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 6b17bee564ee6..37bf3cb22da4c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -37,9 +37,6 @@ export interface IRestApiResource { * @param httpMethod The HTTP method */ onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method; - - // onHttpGet(resourcePath: string, integration?: Integration): void; - // onHttpPost(resourcePath: string, integration?: Integration): void; } export interface ResourceProps { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts index a71b11aa1eb19..e5212518f1852 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts @@ -2,10 +2,20 @@ import cdk = require('@aws-cdk/cdk'); import { RestApiId } from './apigateway.generated'; export interface RestApiRefProps { + /** + * The REST API ID of an existing REST API resource. + */ restApiId: RestApiId; } export abstract class RestApiRef extends cdk.Construct { + + /** + * Imports an existing REST API resource. + * @param parent Parent construct + * @param id Construct ID + * @param props Imported rest API properties + */ public static import(parent: cdk.Construct, id: string, props: RestApiRefProps): RestApiRef { return new ImportedRestApi(parent, id, props); } @@ -13,8 +23,12 @@ export abstract class RestApiRef extends cdk.Construct { /** * The ID of this API Gateway RestApi. */ - public abstract restApiId: RestApiId; + public readonly abstract restApiId: RestApiId; + /** + * Exports a REST API resource from this stack. + * @returns REST API props that can be imported to another stack. + */ public export(): RestApiRefProps { return { restApiId: new RestApiId(new cdk.Output(this, 'RestApiId', { value: this.restApiId }).makeImportValue()), diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 07f8e7c76fd5c..9453b0122945e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -46,7 +46,7 @@ export interface RestApiProps { * manually reverting stages to point to old deployments via the AWS * Console. * - * @default true + * @default false */ retainDeployments?: boolean; @@ -142,7 +142,7 @@ export interface RestApiProps { * By default, the API will automatically be deployed and accessible from a * public endpoint. */ -export class RestApi extends RestApiRef implements IRestApiResource { +export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDependable { /** * The ID of this API Gateway RestApi. */ @@ -177,6 +177,11 @@ export class RestApi extends RestApiRef implements IRestApiResource { */ public latestDeployment?: Deployment; + /** + * Allows taking a dependency on this construct. + */ + public readonly dependencyElements = new Array(); + /** * API Gateway stage that points to the latest deployment (if defined). * @@ -214,6 +219,14 @@ export class RestApi extends RestApiRef implements IRestApiResource { if (cloudWatchRole) { this.configureCloudWatchRole(resource); } + + this.dependencyElements.push(resource); + if (this.latestDeployment) { + this.dependencyElements.push(this.latestDeployment); + } + if (this.deploymentStage) { + this.dependencyElements.push(this.deploymentStage); + } } /** diff --git a/packages/@aws-cdk/aws-apigateway/lib/stage.ts b/packages/@aws-cdk/aws-apigateway/lib/stage.ts index faf1d4f2935a0..fd0026c661461 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/stage.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -126,8 +126,9 @@ export interface MethodDeploymentOptions { cacheDataEncrypted?: boolean; } -export class Stage extends cdk.Construct { +export class Stage extends cdk.Construct implements cdk.IDependable { public readonly stageName: StageName; + public readonly dependencyElements = new Array(); private readonly restApi: RestApiRef; @@ -161,6 +162,7 @@ export class Stage extends cdk.Construct { this.stageName = resource.ref; this.restApi = props.deployment.api; + this.dependencyElements.push(resource); } /** diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 2547e4350fd57..f83050e7a6b30 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -572,8 +572,7 @@ "booksapibooksbookid5264BCA2", "booksapibooksbookidGETCCE21986", "booksapibooksbookidDELETE214F4059" - ], - "DeletionPolicy": "Retain" + ] }, "booksapiDeploymentStageprod55D8E03E": { "Type": "AWS::ApiGateway::Stage", diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json index 58c603a6cddd7..4138766758c77 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -16,8 +16,7 @@ }, "DependsOn": [ "myapiGETF990CE3C" - ], - "DeletionPolicy": "Retain" + ] }, "myapiDeploymentStageprod298F01AF": { "Type": "AWS::ApiGateway::Stage", diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index 6c7268970994e..b6636cde11e43 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -7,6 +7,7 @@ class Test extends cdk.Stack { super(parent, id); const api = new apigateway.RestApi(this, 'my-api', { + retainDeployments: true, deployOptions: { cacheClusterEnabled: true, stageName: 'beta', diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index 02a4917d3f8dd..5f48b7df60c1a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -47,8 +47,7 @@ export = { RestApiId: { Ref: "apiC8550315" } - }, - DeletionPolicy: "Retain" + } } } }); @@ -56,14 +55,14 @@ export = { test.done(); }, - '"retainDeployments" can be used to control the deletion policy of the resource (default is "Retain")'(test: Test) { + '"retainDeployments" can be used to control the deletion policy of the resource'(test: Test) { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); api.onMethod('GET'); // WHEN - new apigateway.Deployment(stack, 'deployment', { api, retainDeployments: false }); + new apigateway.Deployment(stack, 'deployment', { api, retainDeployments: true }); // THEN expect(stack).toMatch({ @@ -99,7 +98,8 @@ export = { RestApiId: { Ref: "apiC8550315" } - } + }, + DeletionPolicy: "Retain" } } }); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 220c3cd2b8d07..0421d0ae5469c 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { App, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; @@ -51,8 +51,7 @@ export = { }, Description: "Automatically created by the RestApi construct" }, - DependsOn: [ "myapiGETF990CE3C" ], - DeletionPolicy: "Retain" + DependsOn: [ "myapiGETF990CE3C" ] }, myapiDeploymentStageprod298F01AF: { Type: "AWS::ApiGateway::Stage", @@ -519,5 +518,27 @@ export = { })); test.done(); - } + }, + + 'allow taking a dependency on the rest api (includes deployment and stage)'(test: Test) { + const stack = new cdk.Stack(); + + const api = new apigateway.RestApi(stack, 'myapi'); + + api.onMethod('GET'); + + const resource = new cdk.Resource(stack, 'DependsOnRestApi', { type: 'My::Resource' }); + + resource.addDependency(api); + + expect(stack).to(haveResource('My::Resource', { + DependsOn: [ + 'myapi162F20B8', // api + 'myapiDeploymentB7EF8EB75c091a668064a3f3a1f6d68a3fb22cf9', // deployment + 'myapiDeploymentStageprod329F21FF' // stage + ] + }, ResourcePart.CompleteDefinition)); + + test.done(); + } }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts index 510f042fddce5..f9e660d9f96f1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts @@ -48,8 +48,7 @@ export = { RestApiId: { Ref: "testapiD6451F70" } - }, - DeletionPolicy: "Retain" + } }, mystage7483BE9A: { Type: "AWS::ApiGateway::Stage", From 255ea423e058822bf67122eb13afd925b57b6b9d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 16 Sep 2018 14:53:46 +0300 Subject: [PATCH 09/15] Allow specifying default integration and options at all levels --- packages/@aws-cdk/aws-apigateway/README.md | 11 +- .../@aws-cdk/aws-apigateway/lib/method.ts | 18 +-- .../@aws-cdk/aws-apigateway/lib/resource.ts | 44 ++++++- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 20 ++- .../test/integ.restapi.books.expected.json | 122 +++++++++++++----- .../test/integ.restapi.books.ts | 26 +++- .../aws-apigateway/test/test.restapi.ts | 108 ++++++++++++++-- 7 files changed, 276 insertions(+), 73 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index bbe20ee3f2d82..21c62f88c2ee9 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -67,12 +67,15 @@ book.addMethod('GET', getBookIntegration, { }); ``` -#### Default Integration +#### Default Integration and Method Options -The `defaultIntegration` can be used to configure a default integration at the -API level. This integration will be used if an integration was not specified. +The `defaultIntegration` and `defaultMethodOptions` properties can be used to +configure a default integration at any resource level. These options will be +used when defining method under this resource (recursively) with undefined +integration or options. -> If not defined, the default integration is `MockIntegration` +> If not defined, the default integration is `MockIntegration`. See reference +documentation for default method options. The following example defines the `booksBackend` integration as a default integration. This means that all API methods that do not explicitly define an diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index da8b78192d8ac..a1453fa771ae0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -81,15 +81,17 @@ export class Method extends cdk.Construct { const options = props.options || { }; + const defaultMethodOptions = props.resource.defaultMethodOptions || {}; + const methodProps: cloudformation.MethodResourceProps = { resourceId: props.resource.resourceId, restApiId: this.restApi.restApiId, httpMethod: props.httpMethod, - operationName: options.operationName, - apiKeyRequired: options.apiKeyRequired, - authorizationType: options.authorizationType || AuthorizationType.None, - authorizerId: options.authorizerId, - integration: this.renderIntegration(props.integration, this.restApi.defaultIntegration) + operationName: options.operationName || defaultMethodOptions.operationName, + apiKeyRequired: options.apiKeyRequired || defaultMethodOptions.apiKeyRequired, + authorizationType: options.authorizationType || defaultMethodOptions.authorizationType || AuthorizationType.None, + authorizerId: options.authorizerId || defaultMethodOptions.authorizerId, + integration: this.renderIntegration(props.integration) }; const resource = new cloudformation.MethodResource(this, 'Resource', methodProps); @@ -130,11 +132,11 @@ export class Method extends cdk.Construct { return this.restApi.executeApiArn(this.httpMethod, this.resource.resourcePath, 'test-invoke-stage'); } - private renderIntegration(integration?: Integration, defaultIntegration?: Integration): cloudformation.MethodResource.IntegrationProperty { + private renderIntegration(integration?: Integration): cloudformation.MethodResource.IntegrationProperty { if (!integration) { // use defaultIntegration from API if defined - if (defaultIntegration) { - return this.renderIntegration(defaultIntegration); + if (this.resource.defaultIntegration) { + return this.renderIntegration(this.resource.defaultIntegration); } // fallback to mock diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 37bf3cb22da4c..bb501ad413c9d 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -25,12 +25,24 @@ export interface IRestApiResource { */ readonly resourcePath: string; + /** + * An integration to use as a default for all methods created within this + * API unless an integration is specified. + */ + readonly defaultIntegration?: Integration; + + /** + * Method options to use as a default for all methods created within this + * API unless custom options are specified. + */ + readonly defaultMethodOptions?: MethodOptions; + /** * Defines a new child resource where this resource is the parent. * @param pathPart The path part for the child resource * @returns A Resource object */ - addResource(pathPart: string): Resource; + addResource(pathPart: string, options?: ResourceOptions): Resource; /** * Defines a new method for this resource. @@ -39,7 +51,21 @@ export interface IRestApiResource { onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method; } -export interface ResourceProps { +export interface ResourceOptions { + /** + * An integration to use as a default for all methods created within this + * API unless an integration is specified. + */ + readonly defaultIntegration?: Integration; + + /** + * Method options to use as a default for all methods created within this + * API unless custom options are specified. + */ + readonly defaultMethodOptions?: MethodOptions; +} + +export interface ResourceProps extends ResourceOptions { /** * The parent resource of this resource. You can either pass another * `Resource` object or a `RestApi` object here. @@ -56,6 +82,8 @@ export class Resource extends cdk.Construct implements IRestApiResource { public readonly resourceApi: RestApi; public readonly resourceId: ResourceId; public readonly resourcePath: string; + public readonly defaultIntegration?: Integration; + public readonly defaultMethodOptions?: MethodOptions; constructor(parent: cdk.Construct, id: string, props: ResourceProps) { super(parent, id); @@ -82,10 +110,18 @@ export class Resource extends cdk.Construct implements IRestApiResource { deployment.addDependency(resource); deployment.addToLogicalId({ resource: resourceProps }); } + + // setup defaults based on properties and inherit from parent. method defaults + // are inherited per property, so children can override piecemeal. + this.defaultIntegration = props.defaultIntegration || props.parent.defaultIntegration; + this.defaultMethodOptions = { + ...props.parent.defaultMethodOptions, + ...props.defaultMethodOptions + }; } - public addResource(pathPart: string): Resource { - return new Resource(this, pathPart, { parent: this, pathPart }); + public addResource(pathPart: string, options?: ResourceOptions): Resource { + return new Resource(this, pathPart, { parent: this, pathPart, ...options }); } public onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 9453b0122945e..de77725077dbe 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -4,11 +4,11 @@ import { cloudformation, ResourceId, RestApiId } from './apigateway.generated'; import { Deployment } from './deployment'; import { Integration } from './integration'; import { Method, MethodOptions } from './method'; -import { IRestApiResource, Resource } from './resource'; +import { IRestApiResource, Resource, ResourceOptions } from './resource'; import { RestApiRef } from './restapi-ref'; import { Stage, StageOptions } from './stage'; -export interface RestApiProps { +export interface RestApiProps extends ResourceOptions { /** * Indicates if a Deployment should be automatically created for this API, * and recreated when the API model (resources, methods) changes. @@ -165,10 +165,16 @@ export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDepend public readonly resourcePath = '/'; /** - * The integration to use as a default for all methods created within this + * An integration to use as a default for all methods created within this * API unless an integration is specified. */ - public defaultIntegration?: Integration; + public readonly defaultIntegration?: Integration; + + /** + * Method options to use as a default for all methods created within this + * API unless custom options are specified. + */ + public readonly defaultMethodOptions?: MethodOptions; /** * API Gateway deployment that represents the latest changes of the API. @@ -209,6 +215,8 @@ export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDepend }); this.defaultIntegration = props.defaultIntegration; + this.defaultMethodOptions = props.defaultMethodOptions; + this.restApiId = resource.ref; this.resourceId = new ResourceId(resource.restApiRootResourceId); // they are the same this.resourceApi = this; @@ -253,8 +261,8 @@ export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDepend * Adds a child resource under the root resource. * @param pathPart The resource name (path part) */ - public addResource(pathPart: string): Resource { - return new Resource(this, pathPart, { parent: this, pathPart }); + public addResource(pathPart: string, options?: ResourceOptions): Resource { + return new Resource(this, pathPart, { parent: this, pathPart, ...options }); } /** diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index f83050e7a6b30..156bf05fc7927 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -1,6 +1,6 @@ { "Resources": { - "HandlerServiceRoleFCDC14AE": { + "BooksHandlerServiceRole5B6A8847": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -41,31 +41,31 @@ ] } }, - "Handler886CB40B": { + "BooksHandler3EB83358": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = function handlerCode(event, _, callback) {\n return callback(undefined, {\n isBase64Encoded: false,\n statusCode: 200,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(event)\n });\n}" + "ZipFile": "exports.handler = function echoHandlerCode(event, _, callback) {\n return callback(undefined, {\n isBase64Encoded: false,\n statusCode: 200,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(event)\n });\n}" }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ - "HandlerServiceRoleFCDC14AE", + "BooksHandlerServiceRole5B6A8847", "Arn" ] }, "Runtime": "nodejs6.10" }, "DependsOn": [ - "HandlerServiceRoleFCDC14AE" + "BooksHandlerServiceRole5B6A8847" ] }, - "HandlerApiPermissionGETbooksF653702A": { + "BooksHandlerApiPermissionGETbooksAB573150": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { - "Ref": "Handler886CB40B" + "Ref": "BooksHandler3EB83358" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { @@ -108,12 +108,12 @@ } } }, - "HandlerApiPermissionTestGETbooks30C948ED": { + "BooksHandlerApiPermissionTestGETbooksE0682829": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { - "Ref": "Handler886CB40B" + "Ref": "BooksHandler3EB83358" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { @@ -146,12 +146,12 @@ } } }, - "HandlerApiPermissionPOSTbooksC036D130": { + "BooksHandlerApiPermissionPOSTbooksC38F97D8": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { - "Ref": "Handler886CB40B" + "Ref": "BooksHandler3EB83358" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { @@ -194,12 +194,12 @@ } } }, - "HandlerApiPermissionTestPOSTbooks9A832525": { + "BooksHandlerApiPermissionTestPOSTbooksCEEC4EF7": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { - "Ref": "Handler886CB40B" + "Ref": "BooksHandler3EB83358" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { @@ -232,12 +232,72 @@ } } }, - "HandlerApiPermissionGETbooksbookid1FAFFB75": { + "BookHandlerServiceRole894768AD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "BookHandlerF9638A7A": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function echoHandlerCode(event, _, callback) {\n return callback(undefined, {\n isBase64Encoded: false,\n statusCode: 200,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(event)\n });\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BookHandlerServiceRole894768AD", + "Arn" + ] + }, + "Runtime": "nodejs6.10" + }, + "DependsOn": [ + "BookHandlerServiceRole894768AD" + ] + }, + "BookHandlerApiPermissionGETbooksbookid8BEBC7A6": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { - "Ref": "Handler886CB40B" + "Ref": "BookHandlerF9638A7A" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { @@ -280,12 +340,12 @@ } } }, - "HandlerApiPermissionTestGETbooksbookidE99E2659": { + "BookHandlerApiPermissionTestGETbooksbookid7E089259": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { - "Ref": "Handler886CB40B" + "Ref": "BookHandlerF9638A7A" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { @@ -318,12 +378,12 @@ } } }, - "HandlerApiPermissionDELETEbooksbookidCD656897": { + "BookHandlerApiPermissionDELETEbooksbookid56D0DC9D": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { - "Ref": "Handler886CB40B" + "Ref": "BookHandlerF9638A7A" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { @@ -366,12 +426,12 @@ } } }, - "HandlerApiPermissionTestDELETEbooksbookidB283D137": { + "BookHandlerApiPermissionTestDELETEbooksbookid3E3975F4": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { - "Ref": "Handler886CB40B" + "Ref": "BookHandlerF9638A7A" }, "Principal": "apigateway.amazonaws.com", "SourceArn": { @@ -556,7 +616,7 @@ "Name": "books-api" } }, - "booksapiDeployment308B08F11e0205e1dd01621f043d0869c2d9f429": { + "booksapiDeployment308B08F1063edabedab805e320c3e8cc6da5e586": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -581,7 +641,7 @@ "Ref": "booksapiE1885304" }, "DeploymentId": { - "Ref": "booksapiDeployment308B08F11e0205e1dd01621f043d0869c2d9f429" + "Ref": "booksapiDeployment308B08F1063edabedab805e320c3e8cc6da5e586" }, "StageName": "prod" } @@ -724,7 +784,7 @@ "RestApiId": { "Ref": "booksapiE1885304" }, - "AuthorizationType": "NONE", + "AuthorizationType": "AWS_IAM", "Integration": { "IntegrationHttpMethod": "POST", "Type": "AWS_PROXY", @@ -755,7 +815,7 @@ "2015-03-31/functions/", { "Fn::GetAtt": [ - "Handler886CB40B", + "BooksHandler3EB83358", "Arn" ] }, @@ -779,7 +839,7 @@ "RestApiId": { "Ref": "booksapiE1885304" }, - "AuthorizationType": "NONE", + "AuthorizationType": "AWS_IAM", "Integration": { "IntegrationHttpMethod": "POST", "Type": "AWS_PROXY", @@ -810,7 +870,7 @@ "2015-03-31/functions/", { "Fn::GetAtt": [ - "Handler886CB40B", + "BooksHandler3EB83358", "Arn" ] }, @@ -846,7 +906,7 @@ "RestApiId": { "Ref": "booksapiE1885304" }, - "AuthorizationType": "NONE", + "AuthorizationType": "AWS_IAM", "Integration": { "IntegrationHttpMethod": "POST", "Type": "AWS_PROXY", @@ -877,7 +937,7 @@ "2015-03-31/functions/", { "Fn::GetAtt": [ - "Handler886CB40B", + "BookHandlerF9638A7A", "Arn" ] }, @@ -901,7 +961,7 @@ "RestApiId": { "Ref": "booksapiE1885304" }, - "AuthorizationType": "NONE", + "AuthorizationType": "AWS_IAM", "Integration": { "IntegrationHttpMethod": "POST", "Type": "AWS_PROXY", @@ -932,7 +992,7 @@ "2015-03-31/functions/", { "Fn::GetAtt": [ - "Handler886CB40B", + "BookHandlerF9638A7A", "Arn" ] }, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts index 4384d20783564..b491eaed741b5 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts @@ -6,10 +6,16 @@ class BookStack extends cdk.Stack { constructor(parent: cdk.App, name: string) { super(parent, name); - const echo = new apigw.LambdaIntegration(new lambda.Function(this, 'Handler', { + const booksHandler = new apigw.LambdaIntegration(new lambda.Function(this, 'BooksHandler', { runtime: lambda.Runtime.NodeJS610, handler: 'index.handler', - code: lambda.Code.inline(`exports.handler = ${handlerCode}`) + code: lambda.Code.inline(`exports.handler = ${echoHandlerCode}`) + })); + + const bookHandler = new apigw.LambdaIntegration(new lambda.Function(this, 'BookHandler', { + runtime: lambda.Runtime.NodeJS610, + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${echoHandlerCode}`) })); const hello = new apigw.LambdaIntegration(new lambda.Function(this, 'Hello', { @@ -18,14 +24,22 @@ class BookStack extends cdk.Stack { code: lambda.Code.inline(`exports.handler = ${helloCode}`) })); - const api = new apigw.RestApi(this, 'books-api', { defaultIntegration: echo }); + const api = new apigw.RestApi(this, 'books-api'); api.onMethod('GET', hello); - const books = api.addResource('books'); + const books = api.addResource('books', { + defaultIntegration: booksHandler, + defaultMethodOptions: { authorizationType: apigw.AuthorizationType.IAM } + }); + books.onMethod('GET'); books.onMethod('POST'); - const book = books.addResource('{book_id}'); + const book = books.addResource('{book_id}', { + defaultIntegration: bookHandler + // note that authorization type is inherited from /books + }); + book.onMethod('GET'); book.onMethod('DELETE'); } @@ -39,7 +53,7 @@ class BookApp extends cdk.App { } } -function handlerCode(event: any, _: any, callback: any) { +function echoHandlerCode(event: any, _: any, callback: any) { return callback(undefined, { isBase64Encoded: false, statusCode: 200, diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 0421d0ae5469c..1f190058ed995 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -521,24 +521,104 @@ export = { }, 'allow taking a dependency on the rest api (includes deployment and stage)'(test: Test) { - const stack = new cdk.Stack(); + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'myapi'); + api.onMethod('GET'); + const resource = new cdk.Resource(stack, 'DependsOnRestApi', { type: 'My::Resource' }); - const api = new apigateway.RestApi(stack, 'myapi'); + // WHEN + resource.addDependency(api); - api.onMethod('GET'); + // THEN + expect(stack).to(haveResource('My::Resource', { + DependsOn: [ + 'myapi162F20B8', // api + 'myapiDeploymentB7EF8EB75c091a668064a3f3a1f6d68a3fb22cf9', // deployment + 'myapiDeploymentStageprod329F21FF' // stage + ] + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, - const resource = new cdk.Resource(stack, 'DependsOnRestApi', { type: 'My::Resource' }); + 'defaultIntegration and defaultMethodOptions can be used at any level'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const rootInteg = new apigateway.AwsIntegration({ + service: 's3', + action: 'GetObject' + }); - resource.addDependency(api); + // WHEN + const api = new apigateway.RestApi(stack, 'myapi', { + defaultIntegration: rootInteg, + defaultMethodOptions: { + authorizerId: new apigateway.AuthorizerId('AUTHID'), + authorizationType: apigateway.AuthorizationType.IAM, + } + }); - expect(stack).to(haveResource('My::Resource', { - DependsOn: [ - 'myapi162F20B8', // api - 'myapiDeploymentB7EF8EB75c091a668064a3f3a1f6d68a3fb22cf9', // deployment - 'myapiDeploymentStageprod329F21FF' // stage - ] - }, ResourcePart.CompleteDefinition)); + // CASE #1: should inherit integration and options from root resource + api.onMethod('GET'); - test.done(); - } + const child = api.addResource('child'); + + // CASE #2: should inherit integration from root and method options, but + // "authorizationType" will be overridden to "None" instead of "IAM" + child.onMethod('POST', undefined, { + authorizationType: apigateway.AuthorizationType.Cognito + }); + + const child2 = api.addResource('child2', { + defaultIntegration: new apigateway.MockIntegration(), + defaultMethodOptions: { + authorizerId: new apigateway.AuthorizerId('AUTHID2'), + } + }); + + // CASE #3: integartion and authorizer ID are inherited from child2 + child2.onMethod('DELETE'); + + // CASE #4: same as case #3, but integration is customized + child2.onMethod('PUT', new apigateway.AwsIntegration({ action: 'foo', service: 'bar' })); + + // THEN + + // CASE #1 + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + ResourceId: { "Fn::GetAtt": [ "myapi162F20B8", "RootResourceId" ] }, + Integration: { Type: 'AWS' }, + AuthorizerId: 'AUTHID', + AuthorizationType: 'AWS_IAM', + })); + + // CASE #2 + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'POST', + ResourceId: { Ref: "myapichildA0A65412" }, + Integration: { Type: 'AWS' }, + AuthorizerId: 'AUTHID', + AuthorizationType: 'COGNITO_USER_POOLS', + })); + + // CASE #3 + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'DELETE', + Integration: { Type: 'MOCK' }, + AuthorizerId: 'AUTHID2', + AuthorizationType: 'AWS_IAM' + })); + + // CASE #4 + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'PUT', + Integration: { Type: 'AWS' }, + AuthorizerId: 'AUTHID2', + AuthorizationType: 'AWS_IAM' + })); + + test.done(); + } }; \ No newline at end of file From a4b13e3c92df77d7f7be17fa018de935058af2c7 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 16 Sep 2018 15:02:03 +0300 Subject: [PATCH 10/15] Fix build break --- packages/@aws-cdk/aws-apigateway/lib/restapi.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index de77725077dbe..2b414e8f130da 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -125,13 +125,6 @@ export interface RestApiProps extends ResourceOptions { * @default true */ cloudWatchRole?: boolean; - - /** - * Default integration for all API methods. If this is set, any method - * created within this API that doesn't have an explicit integration will - * use this integration. - */ - defaultIntegration?: Integration; } /** From f3433debdf1ab7e93702b3875bc403d80ba0c380 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 16 Sep 2018 15:50:45 +0300 Subject: [PATCH 11/15] Misc * "ANY" represented as "*" method in Lambda permissions * Use `api.root` to represent the root resource ("/") instead of the API itself Update package-lock.json files --- packages/@aws-cdk/applet-js/package-lock.json | 2 +- packages/@aws-cdk/assert/package-lock.json | 2 +- packages/@aws-cdk/aws-apigateway/README.md | 18 ++-- .../@aws-cdk/aws-apigateway/lib/resource.ts | 7 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 82 +++++++----------- .../test/integ.restapi.books.ts | 4 +- .../test/integ.restapi.defaults.ts | 2 +- .../aws-apigateway/test/integ.restapi.ts | 2 +- .../aws-apigateway/test/test.deployment.ts | 10 +-- .../@aws-cdk/aws-apigateway/test/test.http.ts | 4 +- .../aws-apigateway/test/test.lambda.ts | 67 ++++++++++++++- .../aws-apigateway/test/test.method.ts | 22 ++--- .../aws-apigateway/test/test.restapi.ts | 86 ++++++++++++------- .../aws-apigateway/test/test.stage.ts | 16 ++-- .../@aws-cdk/aws-cloudfront/package-lock.json | 2 +- .../@aws-cdk/aws-cloudtrail/package-lock.json | 2 +- .../@aws-cdk/aws-codebuild/package-lock.json | 2 +- .../@aws-cdk/aws-codecommit/package-lock.json | 2 +- .../@aws-cdk/aws-route53/package-lock.json | 2 +- packages/@aws-cdk/aws-sqs/package-lock.json | 2 +- packages/@aws-cdk/cdk/package-lock.json | 68 +++++++++++++++ packages/@aws-cdk/cfnspec/package-lock.json | 2 +- .../cloudformation-diff/package-lock.json | 2 +- packages/aws-cdk/package-lock.json | 2 +- .../simple-resource-bundler/package-lock.json | 2 +- tools/cdk-build-tools/package-lock.json | 46 +++++----- tools/cdk-integ-tools/package-lock.json | 2 +- tools/cfn2ts/package-lock.json | 2 +- tools/merkle-build/package-lock.json | 2 +- tools/pkglint/package-lock.json | 2 +- tools/pkgtools/package-lock.json | 2 +- tools/y-npm/package-lock.json | 2 +- 32 files changed, 303 insertions(+), 167 deletions(-) create mode 100644 packages/@aws-cdk/cdk/package-lock.json diff --git a/packages/@aws-cdk/applet-js/package-lock.json b/packages/@aws-cdk/applet-js/package-lock.json index 7e7b4e53bf7ed..d863db9f6da85 100644 --- a/packages/@aws-cdk/applet-js/package-lock.json +++ b/packages/@aws-cdk/applet-js/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/applet-js", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/assert/package-lock.json b/packages/@aws-cdk/assert/package-lock.json index b1436714ceace..5c7111324c7ee 100644 --- a/packages/@aws-cdk/assert/package-lock.json +++ b/packages/@aws-cdk/assert/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/assert", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 21c62f88c2ee9..93e139bbef868 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -9,18 +9,18 @@ running on AWS Lambda, or any web application. ### Defining APIs APIs are defined as a hierarchy of resources and methods. `addResource` and -`addMethod` can be used to build this hierarchy. The root of this hierarchy is -the `RestApi` object (which can also be treated as a resource). +`addMethod` can be used to build this hierarchy. The root resource is +`api.root`. For example, the following code defines an API that includes the following HTTP -endpoints: `GET /books`, `POST /books`, `GET /books/{book_id}`, `DELETE /books/{book_id}`. +endpoints: `ANY /, GET /books`, `POST /books`, `GET /books/{book_id}`, `DELETE /books/{book_id}`. ```ts const api = new apigateway.RestApi(this, 'books-api'); -api.addMethod('GET'); +api.root.addMethod('ANY'); -const books = api.addResource('books'); +const books = api.root.addResource('books'); books.addMethod('GET'); books.addMethod('POST'); @@ -29,13 +29,13 @@ book.addMethod('GET'); book.addMethod('DELETE'); ``` -### Backend Integrations +### Integration Targets Methods are associated with backend integrations, which are invoked when this method is called. API Gateway supports the following integrations: - * `MockIntegration` - can be used to test APIs. This is the default integration - if one is not specified. + * `MockIntegration` - can be used to test APIs. This is the default + integration if one is not specified. * `LambdaIntegration` - can be used to invoke an AWS Lambda function. * `AwsIntegration` - can be used to invoke arbitrary AWS service APIs. * `HttpIntegration` - can be used to invoke HTTP endpoints. @@ -87,7 +87,7 @@ const api = new apigateway.RestApi(this, 'books', { defaultIntegration: booksBackend }); -const books = new api.addResource('books'); +const books = new api.root.addResource('books'); books.addMethod('GET'); // integrated with `booksBackend` books.addMethod('POST'); // integrated with `booksBackend` diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index bb501ad413c9d..e0a994a47787a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -40,6 +40,7 @@ export interface IRestApiResource { /** * Defines a new child resource where this resource is the parent. * @param pathPart The path part for the child resource + * @param options Resource options * @returns A Resource object */ addResource(pathPart: string, options?: ResourceOptions): Resource; @@ -47,8 +48,12 @@ export interface IRestApiResource { /** * Defines a new method for this resource. * @param httpMethod The HTTP method + * @param target The target backend integration for this method + * @param options Method options, such as authentication. + * + * @returns The newly created `Method` object. */ - onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method; + onMethod(httpMethod: string, target?: Integration, options?: MethodOptions): Method; } export interface ResourceOptions { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 2b414e8f130da..d03f4ac49c278 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -135,40 +135,12 @@ export interface RestApiProps extends ResourceOptions { * By default, the API will automatically be deployed and accessible from a * public endpoint. */ -export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDependable { +export class RestApi extends RestApiRef implements cdk.IDependable { /** * The ID of this API Gateway RestApi. */ public readonly restApiId: RestApiId; - /** - * The ID of the root resource of this RestApi. To be used as a parent for - * all top-level resources. - */ - public readonly resourceId: ResourceId; - - /** - * Points to /this/ RestApi. - */ - public readonly resourceApi: RestApi; - - /** - * The full path of this resource. - */ - public readonly resourcePath = '/'; - - /** - * An integration to use as a default for all methods created within this - * API unless an integration is specified. - */ - public readonly defaultIntegration?: Integration; - - /** - * Method options to use as a default for all methods created within this - * API unless custom options are specified. - */ - public readonly defaultMethodOptions?: MethodOptions; - /** * API Gateway deployment that represents the latest changes of the API. * This resource will be automatically updated every time the REST API model changes. @@ -189,6 +161,15 @@ export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDepend */ public deploymentStage?: Stage; + /** + * Represents the root resource ("/") of this API. Use it to define the API model: + * + * api.root.onMethod('ANY', redirectToHomePage); // "ANY /" + * api.root.addResource('friends').onMethod('GET', getFriendsHandler); // "GET /friends" + * + */ + public readonly root: IRestApiResource; + private readonly methods = new Array(); constructor(parent: cdk.Construct, id: string, props: RestApiProps = { }) { @@ -207,12 +188,7 @@ export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDepend parameters: props.parameters, }); - this.defaultIntegration = props.defaultIntegration; - this.defaultMethodOptions = props.defaultMethodOptions; - this.restApiId = resource.ref; - this.resourceId = new ResourceId(resource.restApiRootResourceId); // they are the same - this.resourceApi = this; this.configureDeployment(props); @@ -228,6 +204,21 @@ export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDepend if (this.deploymentStage) { this.dependencyElements.push(this.deploymentStage); } + + // configure the "root" resource + this.root = { + addResource: (pathPart: string, options?: ResourceOptions) => { + return new Resource(this, pathPart, { parent: this.root, pathPart, ...options }); + }, + onMethod: (httpMethod: string, integration?: Integration, options?: MethodOptions) => { + return new Method(this, httpMethod, { resource: this.root, httpMethod, integration, options }); + }, + defaultIntegration: props.defaultIntegration, + defaultMethodOptions: props.defaultMethodOptions, + resourceApi: this, + resourceId: new ResourceId(resource.restApiRootResourceId), + resourcePath: '/' + }; } /** @@ -250,25 +241,6 @@ export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDepend return this.deploymentStage.urlForPath(path); } - /** - * Adds a child resource under the root resource. - * @param pathPart The resource name (path part) - */ - public addResource(pathPart: string, options?: ResourceOptions): Resource { - return new Resource(this, pathPart, { parent: this, pathPart, ...options }); - } - - /** - * Adds a method to the root resource (i.e. "GET /") - * - * @param httpMethod The HTTP method (i.e. 'GET', 'POST', etc) - * @param integration Backend integration - * @param options Method options - */ - public onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { - return new Method(this, httpMethod, { resource: this, httpMethod, integration, options }); - } - /** * @returns The "execute-api" ARN. * @default "*" returns the execute API ARN for all methods/resources in @@ -282,6 +254,10 @@ export class RestApi extends RestApiRef implements IRestApiResource, cdk.IDepend throw new Error(`"path" must begin with a "/": '${path}'`); } + if (method.toUpperCase() === 'ANY') { + method = '*'; + } + return cdk.Arn.fromComponents({ service: 'execute-api', resource: this.restApiId, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts index b491eaed741b5..65d4ab1f31d20 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts @@ -25,9 +25,9 @@ class BookStack extends cdk.Stack { })); const api = new apigw.RestApi(this, 'books-api'); - api.onMethod('GET', hello); + api.root.onMethod('ANY', hello); - const books = api.addResource('books', { + const books = api.root.addResource('books', { defaultIntegration: booksHandler, defaultMethodOptions: { authorizationType: apigw.AuthorizationType.IAM } }); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts index ce9f182480901..a9a4c09265481 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts @@ -8,6 +8,6 @@ const stack = new cdk.Stack(app, 'test-apigateway-restapi-defaults'); const api = new apigateway.RestApi(stack, 'my-api'); // at least one method is required -api.onMethod('GET'); +api.root.onMethod('GET'); process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index b6636cde11e43..3377d82787238 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -28,7 +28,7 @@ class Test extends cdk.Stack { handler: 'index.handler', }); - const v1 = api.addResource('v1'); + const v1 = api.root.addResource('v1'); const integration = new apigateway.LambdaIntegration(handler); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index 5f48b7df60c1a..f242bc83757b3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -8,7 +8,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Deployment(stack, 'deployment', { api }); @@ -59,7 +59,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Deployment(stack, 'deployment', { api, retainDeployments: true }); @@ -111,7 +111,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Deployment(stack, 'deployment', { api, description: 'this is my deployment' }); @@ -129,7 +129,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); const deployment = new apigateway.Deployment(stack, 'deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // default logical ID (with no "salt") test.ok(synthesize().Resources.deployment33381975); @@ -162,7 +162,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); const deployment = new apigateway.Deployment(stack, 'deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); const dep = new cdk.Resource(stack, 'MyResource', { type: 'foo' }); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.http.ts b/packages/@aws-cdk/aws-apigateway/test/test.http.ts index ac8601cead848..de586504c8ae3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.http.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.http.ts @@ -12,7 +12,7 @@ export = { // WHEN const integ = new apigateway.HttpIntegration('http://foo/bar'); - api.onMethod('GET', integ); + api.root.onMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { @@ -40,7 +40,7 @@ export = { } }); - api.onMethod('GET', integ); + api.root.onMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts index 7f1a7aef5744e..bbee6ec652fde 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts @@ -17,7 +17,7 @@ export = { // WHEN const integ = new apigateway.LambdaIntegration(handler); - api.onMethod('GET', integ); + api.root.onMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { @@ -80,7 +80,7 @@ export = { // WHEN const integ = new apigateway.LambdaIntegration(fn, { allowTestInvoke: false }); - api.onMethod('GET', integ); + api.root.onMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::Lambda::Permission', { @@ -134,7 +134,7 @@ export = { // WHEN const integ = new apigateway.LambdaIntegration(fn, { proxy: false }); - api.onMethod('GET', integ); + api.root.onMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { @@ -143,6 +143,67 @@ export = { } })); + test.done(); + }, + + 'when "ANY" is used, lambda permission will include "*" for method'(test: Test) { + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api'); + + const handler = new lambda.Function(stack, 'MyFunc', { + runtime: lambda.Runtime.NodeJS610, + handler: 'index.handler', + code: lambda.Code.inline(``) + }); + + const target = new apigateway.LambdaIntegration(handler); + + api.root.onMethod('ANY', target); + + expect(stack).to(haveResource('AWS::Lambda::Permission', { + SourceArn: { + "Fn::Join": [ + "", + [ + "arn", ":", + { Ref: "AWS::Partition" }, + ":", + "execute-api", + ":", + { Ref: "AWS::Region" }, + ":", + { Ref: "AWS::AccountId" }, + ":", + { Ref: "testapiD6451F70" }, + "/", + "test-invoke-stage/*/" + ] + ] + } + })); + + expect(stack).to(haveResource('AWS::Lambda::Permission', { + SourceArn: { + "Fn::Join": [ + "", + [ + "arn", ":", + { Ref: "AWS::Partition" }, + ":", + "execute-api", + ":", + { Ref: "AWS::Region" }, + ":", + { Ref: "AWS::AccountId" }, + ":", + { Ref: "testapiD6451F70" }, + "/", + "prod/*/" + ] + ] + } + })); + test.done(); } }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index c75283b5c1a45..c19c520c04ff6 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -13,7 +13,7 @@ export = { // WHEN new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', - resource: api, + resource: api.root, }); // THEN @@ -36,7 +36,7 @@ export = { // WHEN new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', - resource: api, + resource: api.root, options: { apiKeyRequired: true, operationName: 'MyOperation', @@ -63,7 +63,7 @@ export = { // WHEN new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', - resource: api, + resource: api.root, integration: new apigateway.AwsIntegration({ service: 's3', path: 'bucket/key' }) }); @@ -103,7 +103,7 @@ export = { // WHEN new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', - resource: api, + resource: api.root, }); // THEN @@ -125,7 +125,7 @@ export = { // WHEN const method = new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', - resource: api, + resource: api.root, }); // THEN @@ -161,7 +161,7 @@ export = { // WHEN const method = new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', - resource: api, + resource: api.root, }); // THEN @@ -193,7 +193,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); - const method = new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', resource: api }); + const method = new apigateway.Method(stack, 'my-method', { httpMethod: 'POST', resource: api.root }); // WHEN + THEN test.throws(() => method.methodArn, @@ -209,7 +209,7 @@ export = { const role = new iam.Role(stack, 'MyRole', { assumedBy: new cdk.ServicePrincipal('foo') }); // WHEN - api.onMethod('GET', new apigateway.Integration({ + api.root.onMethod('GET', new apigateway.Integration({ type: apigateway.IntegrationType.AwsProxy, options: { credentialsRole: role @@ -231,7 +231,7 @@ export = { const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); // WHEN - api.onMethod('GET', new apigateway.Integration({ + api.root.onMethod('GET', new apigateway.Integration({ type: apigateway.IntegrationType.AwsProxy, options: { credentialsPassthrough: true @@ -263,7 +263,7 @@ export = { }); // THEN - test.throws(() => api.onMethod('GET', integration), /'credentialsPassthrough' and 'credentialsRole' are mutually exclusive/); + test.throws(() => api.root.onMethod('GET', integration), /'credentialsPassthrough' and 'credentialsRole' are mutually exclusive/); test.done(); - } + }, }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 1f190058ed995..7a7b96778442f 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -13,7 +13,7 @@ export = { // WHEN const api = new apigateway.RestApi(stack, 'my-api'); - api.onMethod('GET'); // must have at least one method + api.root.onMethod('GET'); // must have at least one method // THEN expect(stack).toMatch({ @@ -163,7 +163,7 @@ export = { cloudWatchRole: false, }); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { @@ -180,8 +180,8 @@ export = { const api = new apigateway.RestApi(stack, 'API'); // WHEN - api.addResource('foo'); - api.addResource('bar').addResource('goo'); + api.root.addResource('foo'); + api.root.addResource('bar').addResource('goo'); // THEN test.throws(() => app.synthesizeStack(stack.name), /The REST API doesn't contain any methods/); @@ -197,11 +197,11 @@ export = { restApiName: 'my-rest-api' }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN - const foo = api.addResource('foo'); - api.addResource('bar'); + const foo = api.root.addResource('foo'); + api.root.addResource('bar'); foo.addResource('{hello}'); // THEN @@ -228,10 +228,10 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'restapi', { deploy: false, cloudWatchRole: false }); - const r1 = api.addResource('r1'); + const r1 = api.root.addResource('r1'); // WHEN - api.onMethod('GET'); + api.root.onMethod('GET'); r1.onMethod('POST'); // THEN @@ -305,14 +305,14 @@ export = { const api = new apigateway.RestApi(stack, 'restapi'); // WHEN - const r1 = api.addResource('r1'); + const r1 = api.root.addResource('r1'); const r11 = r1.addResource('r1_1'); const r12 = r1.addResource('r1_2'); const r121 = r12.addResource('r1_2_1'); - const r2 = api.addResource('r2'); + const r2 = api.root.addResource('r2'); // THEN - test.deepEqual(api.resourcePath, '/'); + test.deepEqual(api.root.resourcePath, '/'); test.deepEqual(r1.resourcePath, '/r1'); test.deepEqual(r11.resourcePath, '/r1/r1_1'); test.deepEqual(r12.resourcePath, '/r1/r1_2'); @@ -327,11 +327,11 @@ export = { const api = new apigateway.RestApi(stack, 'restapi'); // THEN - test.throws(() => api.addResource('foo/')); - api.addResource('boom-bam'); - test.throws(() => api.addResource('illegal()')); - api.addResource('{foo}'); - test.throws(() => api.addResource('foo{bar}')); + test.throws(() => api.root.addResource('foo/')); + api.root.addResource('boom-bam'); + test.throws(() => api.root.addResource('illegal()')); + api.root.addResource('{foo}'); + test.throws(() => api.root.addResource('foo{bar}')); test.done(); }, @@ -352,7 +352,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'myapi'); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN expect(stack).to(haveResource('AWS::IAM::Role')); @@ -389,7 +389,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN test.deepEqual(cdk.resolve(api.url), { 'Fn::Join': @@ -417,7 +417,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false }); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN test.throws(() => api.url, /Cannot determine deployment stage for API from "deploymentStage". Use "deploy" or explicitly set "deploymentStage"/); @@ -429,7 +429,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN test.throws(() => api.urlForPath('foo'), /Path must begin with \"\/\": foo/); @@ -440,7 +440,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN const arn = api.executeApiArn('method', '/path', 'stage'); @@ -468,13 +468,39 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN test.throws(() => api.executeApiArn('method', 'hey-path', 'stage'), /"path" must begin with a "\/": 'hey-path'/); test.done(); }, + '"executeApiArn" will convert ANY to "*"'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const api = new apigateway.RestApi(stack, 'api'); + const method = api.root.onMethod('ANY'); + + // THEN + test.deepEqual(cdk.resolve(method.methodArn), { 'Fn::Join': + [ '', + [ 'arn', + ':', + { Ref: 'AWS::Partition' }, + ':', + 'execute-api', + ':', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + { Ref: 'apiC8550315' }, + '/', + { 'Fn::Join': [ '', [ { Ref: 'apiDeploymentStageprod896C8101' }, '/*/' ] ] } ] ] }); + test.done(); + }, + '"endpointTypes" can be used to specify endpoint configuration for the api'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -484,7 +510,7 @@ export = { endpointTypes: [ apigateway.EndpointType.Edge, apigateway.EndpointType.Private ] }); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { @@ -510,7 +536,7 @@ export = { cloneFrom }); - api.onMethod('GET'); + api.root.onMethod('GET'); expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { CloneFrom: "foobar", @@ -524,7 +550,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'myapi'); - api.onMethod('GET'); + api.root.onMethod('GET'); const resource = new cdk.Resource(stack, 'DependsOnRestApi', { type: 'My::Resource' }); // WHEN @@ -560,9 +586,9 @@ export = { }); // CASE #1: should inherit integration and options from root resource - api.onMethod('GET'); + api.root.onMethod('GET'); - const child = api.addResource('child'); + const child = api.root.addResource('child'); // CASE #2: should inherit integration from root and method options, but // "authorizationType" will be overridden to "None" instead of "IAM" @@ -570,7 +596,7 @@ export = { authorizationType: apigateway.AuthorizationType.Cognito }); - const child2 = api.addResource('child2', { + const child2 = api.root.addResource('child2', { defaultIntegration: new apigateway.MockIntegration(), defaultMethodOptions: { authorizerId: new apigateway.AuthorizerId('AUTHID2'), @@ -620,5 +646,5 @@ export = { })); test.done(); - } + }, }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts index f9e660d9f96f1..3acf88f81c479 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts @@ -9,7 +9,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { deployment }); @@ -73,7 +73,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -102,7 +102,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -141,7 +141,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -163,7 +163,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -185,7 +185,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN test.throws(() => new apigateway.Stage(stack, 'my-stage', { @@ -202,7 +202,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -232,7 +232,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.onMethod('GET'); + api.root.onMethod('GET'); // THEN test.throws(() => new apigateway.Stage(stack, 'my-stage', { diff --git a/packages/@aws-cdk/aws-cloudfront/package-lock.json b/packages/@aws-cdk/aws-cloudfront/package-lock.json index 1823106b705a5..79f6f1f43bdbd 100644 --- a/packages/@aws-cdk/aws-cloudfront/package-lock.json +++ b/packages/@aws-cdk/aws-cloudfront/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/aws-cloudfront", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/aws-cloudtrail/package-lock.json b/packages/@aws-cdk/aws-cloudtrail/package-lock.json index d54d3e67c66af..ea9586383b8fa 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package-lock.json +++ b/packages/@aws-cdk/aws-cloudtrail/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/aws-cloudtrail", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/aws-codebuild/package-lock.json b/packages/@aws-cdk/aws-codebuild/package-lock.json index 23e81ede2c862..57217cf3012f6 100644 --- a/packages/@aws-cdk/aws-codebuild/package-lock.json +++ b/packages/@aws-cdk/aws-codebuild/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/aws-codebuild", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/aws-codecommit/package-lock.json b/packages/@aws-cdk/aws-codecommit/package-lock.json index 45514f6130fba..0f6b1bbd3507f 100644 --- a/packages/@aws-cdk/aws-codecommit/package-lock.json +++ b/packages/@aws-cdk/aws-codecommit/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/aws-codecommit", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/aws-route53/package-lock.json b/packages/@aws-cdk/aws-route53/package-lock.json index 902be0a8f3cf1..2c23464d646dc 100644 --- a/packages/@aws-cdk/aws-route53/package-lock.json +++ b/packages/@aws-cdk/aws-route53/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/aws-route53", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/aws-sqs/package-lock.json b/packages/@aws-cdk/aws-sqs/package-lock.json index 11a17e4648fce..39b34d8de968e 100644 --- a/packages/@aws-cdk/aws-sqs/package-lock.json +++ b/packages/@aws-cdk/aws-sqs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/aws-sqs", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/cdk/package-lock.json b/packages/@aws-cdk/cdk/package-lock.json new file mode 100644 index 0000000000000..c68b089e0c913 --- /dev/null +++ b/packages/@aws-cdk/cdk/package-lock.json @@ -0,0 +1,68 @@ +{ + "name": "@aws-cdk/cdk", + "version": "0.9.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/js-base64": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/js-base64/-/js-base64-2.3.1.tgz", + "integrity": "sha512-4RKbhIDGC87s4EBy2Cp2/5S2O6kmCRcZnD5KRCq1q9z2GhBte1+BdsfVKCpG8yKpDGNyEE2G6IqFIh6W2YwWPA==", + "dev": true + }, + "cli-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-0.1.7.tgz", + "integrity": "sha1-rcMgD6RxzCEbDaf1ZrcemLnWc0c=", + "requires": { + "es5-ext": "0.8.x" + } + }, + "difflib": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", + "integrity": "sha1-teMDYabbAjF21WKJLbhZQKcY9H4=", + "requires": { + "heap": ">= 0.2.0" + } + }, + "dreamopt": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.6.0.tgz", + "integrity": "sha1-2BPM2sjTnYrVJndVFKE92mZNa0s=", + "requires": { + "wordwrap": ">=0.0.2" + } + }, + "es5-ext": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.8.2.tgz", + "integrity": "sha1-q6jZ4ZQ6iVrJaDemKjmz9V7NlKs=" + }, + "heap": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz", + "integrity": "sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw=" + }, + "js-base64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz", + "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==" + }, + "json-diff": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-diff/-/json-diff-0.3.1.tgz", + "integrity": "sha1-bbw64tJeB1p/1xvNmHRFhmb7aBs=", + "requires": { + "cli-color": "~0.1.6", + "difflib": "~0.2.1", + "dreamopt": "~0.6.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + } + } +} diff --git a/packages/@aws-cdk/cfnspec/package-lock.json b/packages/@aws-cdk/cfnspec/package-lock.json index eacbcd7a28251..37ba43ec00284 100644 --- a/packages/@aws-cdk/cfnspec/package-lock.json +++ b/packages/@aws-cdk/cfnspec/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/cfnspec", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/cloudformation-diff/package-lock.json b/packages/@aws-cdk/cloudformation-diff/package-lock.json index 03a0c516ea094..70d96430245fa 100644 --- a/packages/@aws-cdk/cloudformation-diff/package-lock.json +++ b/packages/@aws-cdk/cloudformation-diff/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/cloudformation-diff", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/aws-cdk/package-lock.json b/packages/aws-cdk/package-lock.json index eb45ba3ca901e..7ed088f693c7f 100644 --- a/packages/aws-cdk/package-lock.json +++ b/packages/aws-cdk/package-lock.json @@ -1,6 +1,6 @@ { "name": "aws-cdk", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/simple-resource-bundler/package-lock.json b/packages/simple-resource-bundler/package-lock.json index e13159d4f80f3..0bdb78ff27d49 100644 --- a/packages/simple-resource-bundler/package-lock.json +++ b/packages/simple-resource-bundler/package-lock.json @@ -1,6 +1,6 @@ { "name": "simple-resource-bundler", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tools/cdk-build-tools/package-lock.json b/tools/cdk-build-tools/package-lock.json index 849d4fa970e51..0eeede008c2c1 100644 --- a/tools/cdk-build-tools/package-lock.json +++ b/tools/cdk-build-tools/package-lock.json @@ -1,6 +1,6 @@ { "name": "cdk-build-tools", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -71,9 +71,9 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codemaker": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/codemaker/-/codemaker-0.7.4.tgz", - "integrity": "sha512-lipkzQdXPamyuvF73HBWvnykDupzV+Kp50GmCfBN3FuVEkAl6DnFpP7uIq/9LFAMWRNIziCmIFBuFi3d7pOXjA==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/codemaker/-/codemaker-0.7.5.tgz", + "integrity": "sha512-bGwk/bcC8poiWL/XZsWrvKYYtdr7ll9Og0c9wLw1ZxeuV2nNLXa5BdPyyjamYy29NIYvmHOnwamcs2BLcv/Q9Q==", "requires": { "camelcase": "^4.1.0", "decamelize": "^2.0.0", @@ -120,11 +120,11 @@ "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=" }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "decamelize": { @@ -254,15 +254,15 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "jsii": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/jsii/-/jsii-0.7.4.tgz", - "integrity": "sha512-xbNh0DTrUyNsJUN6TJ3fbp4NcwqeeaIow+A70HRHm0h19nwMTldOZSDOdt5RbnshV1cTy111Kk3KeuCtkm3FsQ==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/jsii/-/jsii-0.7.5.tgz", + "integrity": "sha512-9SnXWJlwDexFcWxEl3GgPb1aJNQfY3lME3+MQ5xrotsNHxM3gk4E+es1eNdj3O0/Hc0GUhoNgVV54Why6mI3Uw==", "requires": { "case": "^1.5.5", "colors": "^1.3.1", "deep-equal": "^1.0.1", "fs-extra": "^7.0.0", - "jsii-spec": "^0.7.4", + "jsii-spec": "^0.7.5", "log4js": "^3.0.4", "semver": "^5.5.0", "sort-json": "^2.0.0", @@ -293,14 +293,14 @@ } }, "jsii-pacmak": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/jsii-pacmak/-/jsii-pacmak-0.7.4.tgz", - "integrity": "sha512-QV4vEbdUrytOnjy8a0URVbd1D1VCpO/3iZlmrXi+L4nakrSPP4XV16esXYU+SWZWUq0WCPf/0h7Vs1aYxup5zQ==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/jsii-pacmak/-/jsii-pacmak-0.7.5.tgz", + "integrity": "sha512-ZhT7ytJpRrOGqODUwJWt+FIb1dp9mL3cgcWZM0t1uCIFcktDShFGHjAVt9D4jSmhUh8SPvnmil1JrZ0aps/uJw==", "requires": { "clone": "^2.1.1", - "codemaker": "^0.7.4", + "codemaker": "^0.7.5", "fs-extra": "^4.0.3", - "jsii-spec": "^0.7.4", + "jsii-spec": "^0.7.5", "spdx-license-list": "^4.1.0", "xmlbuilder": "^10.0.0", "yargs": "^12.0.0" @@ -338,9 +338,9 @@ } }, "jsii-spec": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/jsii-spec/-/jsii-spec-0.7.4.tgz", - "integrity": "sha512-tQGICxSm+zFEzwrOXtdZh//kNfU8jpMjZQch/yeCGN2kBashz6DvygoZ2Dwejn+KA7Rlk8cASnKOw5LjldQTFA==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/jsii-spec/-/jsii-spec-0.7.5.tgz", + "integrity": "sha512-cEC5qgkl/gJs2/HuRfz4RQkQf+oRdyKtxzn7AZhiJHQxa3RYWi/vI8kSNv6K/hjzzQMNXx+ZkOJf9koduxl+uw==", "requires": { "jsonschema": "^1.2.4" } @@ -444,9 +444,9 @@ } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, "nice-try": { "version": "1.0.5", diff --git a/tools/cdk-integ-tools/package-lock.json b/tools/cdk-integ-tools/package-lock.json index fdd6635b65072..4e81c1d24dae1 100644 --- a/tools/cdk-integ-tools/package-lock.json +++ b/tools/cdk-integ-tools/package-lock.json @@ -1,6 +1,6 @@ { "name": "cdk-integ-tools", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tools/cfn2ts/package-lock.json b/tools/cfn2ts/package-lock.json index e75ebee3592e0..2cb374a5784be 100644 --- a/tools/cfn2ts/package-lock.json +++ b/tools/cfn2ts/package-lock.json @@ -1,6 +1,6 @@ { "name": "cfn2ts", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tools/merkle-build/package-lock.json b/tools/merkle-build/package-lock.json index 2482fe1435fef..caae9d5b8fda8 100644 --- a/tools/merkle-build/package-lock.json +++ b/tools/merkle-build/package-lock.json @@ -1,6 +1,6 @@ { "name": "merkle-build", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tools/pkglint/package-lock.json b/tools/pkglint/package-lock.json index c2b2812659008..dba30215ec890 100644 --- a/tools/pkglint/package-lock.json +++ b/tools/pkglint/package-lock.json @@ -1,6 +1,6 @@ { "name": "pkglint", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tools/pkgtools/package-lock.json b/tools/pkgtools/package-lock.json index 24716b3e68447..91726180a4b28 100644 --- a/tools/pkgtools/package-lock.json +++ b/tools/pkgtools/package-lock.json @@ -1,6 +1,6 @@ { "name": "pkgtools", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tools/y-npm/package-lock.json b/tools/y-npm/package-lock.json index 9906c0c45d33e..0eab73215fb33 100644 --- a/tools/y-npm/package-lock.json +++ b/tools/y-npm/package-lock.json @@ -1,6 +1,6 @@ { "name": "y-npm", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { From 5baedcfa617e2487db3d367c921d52b14f7e18d1 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 16 Sep 2018 15:57:01 +0300 Subject: [PATCH 12/15] Fix lambda test --- .../aws-apigateway/test/test.lambda.ts | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts index bbee6ec652fde..b9e8a53de822d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts @@ -187,21 +187,40 @@ export = { "Fn::Join": [ "", [ - "arn", ":", - { Ref: "AWS::Partition" }, + "arn", + ":", + { + Ref: "AWS::Partition" + }, ":", "execute-api", ":", - { Ref: "AWS::Region" }, + { + Ref: "AWS::Region" + }, ":", - { Ref: "AWS::AccountId" }, + { + Ref: "AWS::AccountId" + }, ":", - { Ref: "testapiD6451F70" }, + { + Ref: "testapiD6451F70" + }, "/", - "prod/*/" + { + "Fn::Join": [ + "", + [ + { + Ref: "testapiDeploymentStageprod5C9E92A4" + }, + "/*/" + ] + ] + } ] ] - } + } })); test.done(); From 22682ac425c32f9038b6f8a143bdafb15a87bd2d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Sun, 16 Sep 2018 15:59:53 +0300 Subject: [PATCH 13/15] Update expectation --- .../test/integ.restapi.books.expected.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 156bf05fc7927..93459c98efb7d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -524,7 +524,7 @@ "HelloServiceRole1E55EA16" ] }, - "HelloApiPermissionGET36D249D1": { + "HelloApiPermissionANY16B11477": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -563,7 +563,7 @@ { "Ref": "booksapiDeploymentStageprod55D8E03E" }, - "/GET/" + "/*/" ] ] } @@ -572,7 +572,7 @@ } } }, - "HelloApiPermissionTestGETBC2F4D5D": { + "HelloApiPermissionTestANY9A757A77": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -604,7 +604,7 @@ "Ref": "booksapiE1885304" }, "/", - "test-invoke-stage/GET/" + "test-invoke-stage/*/" ] ] } @@ -616,7 +616,7 @@ "Name": "books-api" } }, - "booksapiDeployment308B08F1063edabedab805e320c3e8cc6da5e586": { + "booksapiDeployment308B08F19d5655c7356bb9d23943b328416b2f5e": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -625,7 +625,7 @@ "Description": "Automatically created by the RestApi construct" }, "DependsOn": [ - "booksapiGETF514386E", + "booksapiANYF4F0CDEB", "booksapibooks97D84727", "booksapibooksGETA776447A", "booksapibooksPOSTF6C6559D", @@ -641,7 +641,7 @@ "Ref": "booksapiE1885304" }, "DeploymentId": { - "Ref": "booksapiDeployment308B08F1063edabedab805e320c3e8cc6da5e586" + "Ref": "booksapiDeployment308B08F19d5655c7356bb9d23943b328416b2f5e" }, "StageName": "prod" } @@ -701,10 +701,10 @@ "booksapiE1885304" ] }, - "booksapiGETF514386E": { + "booksapiANYF4F0CDEB": { "Type": "AWS::ApiGateway::Method", "Properties": { - "HttpMethod": "GET", + "HttpMethod": "ANY", "ResourceId": { "Fn::GetAtt": [ "booksapiE1885304", From fd0facaccb93661002da679e56d5a3af6d72aa20 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 17 Sep 2018 12:51:06 +0300 Subject: [PATCH 14/15] Make lazy logical IDs a bit less hacky --- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 9afd8723ece90..f8161123633eb 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -104,28 +104,52 @@ export class Deployment extends cdk.Construct implements cdk.IDependable { } class LatestDeploymentResource extends cloudformation.DeploymentResource { - private originalLogicalId: string; - private customLogicalId?: string; + private originalLogicalId?: string; + private lazyLogicalIdRequired: boolean; + private lazyLogicalId?: string; private hashComponents = new Array(); constructor(parent: cdk.Construct, id: string, props: cloudformation.DeploymentResourceProps) { super(parent, id, props); - this.originalLogicalId = this.logicalId; - - Object.defineProperties(this, { - logicalId: { - get: () => { - if (!this.customLogicalId) { - throw new Error('The logical ID of this resource cannot be evaluated eagerly. Use: new cdk.Token(() => foo.logicalId)'); - } - return this.customLogicalId; - } - }, - ref: { - get: () => new cdk.CloudFormationToken(() => ({ Ref: this.customLogicalId })) - }, - }); + // from this point, don't allow accessing logical ID before synthesis + this.lazyLogicalIdRequired = true; + } + + /** + * Returns either the original or the custom logical ID of this resource. + */ + public get logicalId() { + if (!this.lazyLogicalIdRequired) { + return this.originalLogicalId!; + } + + if (!this.lazyLogicalId) { + throw new Error('This resource has a lazy logical ID which is calculated just before synthesis. Use a cdk.Token to evaluate'); + } + + return this.lazyLogicalId; + } + + /** + * Sets the logical ID of this resource. + */ + public set logicalId(v: string) { + this.originalLogicalId = v; + } + + /** + * Returns a lazy reference to this resource (evaluated only upon synthesis). + */ + public get ref() { + return new DeploymentId(() => ({ Ref: this.lazyLogicalId })); + } + + /** + * Does nothing. + */ + public set ref(_v: DeploymentId) { + return; } /** @@ -133,7 +157,6 @@ class LatestDeploymentResource extends cloudformation.DeploymentResource { * This can be used to couple the deployment to the API Gateway model. */ public addToLogicalId(data: unknown) { - // if the construct is locked, it means we are already synthesizing and then // we can't modify the hash because we might have already calculated it. if (this.locked) { @@ -151,14 +174,14 @@ class LatestDeploymentResource extends cloudformation.DeploymentResource { // if hash components were added to the deployment, we use them to calculate // a logical ID for the deployment resource. if (this.hashComponents.length === 0) { - this.customLogicalId = this.originalLogicalId; + this.lazyLogicalId = this.originalLogicalId; } else { const md5 = crypto.createHash('md5'); this.hashComponents .map(c => cdk.resolve(c)) .forEach(c => md5.update(JSON.stringify(c))); - this.customLogicalId = this.originalLogicalId + md5.digest("hex"); + this.lazyLogicalId = this.originalLogicalId + md5.digest("hex"); } return []; From f3f2091eb56637918805e3e416520f99d89c5368 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 17 Sep 2018 13:13:15 +0300 Subject: [PATCH 15/15] CR fixes * Rename `onMethod` to `addMethod`. * Fixed some docs --- packages/@aws-cdk/aws-apigateway/README.md | 28 ++++---------- packages/@aws-cdk/aws-apigateway/lib/index.ts | 6 +-- .../aws-apigateway/lib/integration.ts | 4 +- .../aws-apigateway/lib/integrations/index.ts | 4 ++ .../@aws-cdk/aws-apigateway/lib/resource.ts | 4 +- .../@aws-cdk/aws-apigateway/lib/restapi.ts | 6 +-- .../test/integ.restapi.books.ts | 10 ++--- .../test/integ.restapi.defaults.ts | 2 +- .../aws-apigateway/test/integ.restapi.ts | 12 +++--- .../aws-apigateway/test/test.deployment.ts | 10 ++--- .../@aws-cdk/aws-apigateway/test/test.http.ts | 4 +- .../aws-apigateway/test/test.lambda.ts | 8 ++-- .../aws-apigateway/test/test.method.ts | 6 +-- .../aws-apigateway/test/test.restapi.ts | 38 +++++++++---------- .../aws-apigateway/test/test.stage.ts | 16 ++++---- 15 files changed, 73 insertions(+), 85 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 93e139bbef868..a44ad5b5bee21 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -98,9 +98,9 @@ book.addMethod('GET'); // integrated with `booksBackend` ### Deployments By default, the `RestApi` construct will automatically create an API Gateway -[Deployment] and a "prod" [Stage] which represent the API configuration you defined in -your CDK app. This means that when you deploy your app, your API can be accessed -from the public internet via the stage URL. +[Deployment] and a "prod" [Stage] which represent the API configuration you +defined in your CDK app. This means that when you deploy your app, your API will +be have open access from the internet via the stage URL. The URL of your API can be obtained from the attribute `restApi.url`, and is also exported as an `Output` from your stack, so it's printed when you `cdk @@ -150,27 +150,15 @@ logical ID will be assigned to the deployment resource. This will cause CloudFormation to create a new deployment resource. By default, old deployments are _deleted_. You can set `retainDeployments: true` -to allow users revert the stage to an old deployment. +to allow users revert the stage to an old deployment manually. [Deployment]: https://docs.aws.amazon.com/apigateway/api-reference/resource/deployment/ [Stage]: https://docs.aws.amazon.com/apigateway/api-reference/resource/stage/ -### TODO - -The following features are not supported yet by this library: - -- [ ] Swagger/Open API models -- [ ] [Authorizers](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-authorizer.html) -- [ ] Method options: [`RequestValidatorId`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestvalidatorid), [`RequestModels`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestmodels), [`RequestParameters`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-requestparameters), [`MethodResponses`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-methodresponses) -- [ ] [Custom domains](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-domainname.html) -- [ ] [API keys](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html) -- [ ] [Base path mapping](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-basepathmapping.html) -- [ ] [Client certificates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-clientcertificate.html) -- [ ] Documentation ([part](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-documentationpart.html) and [version](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-documentationversion.html)) -- [ ] [Model](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-model.html) -- [ ] [Request validators](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-requestvalidator.html) -- [ ] [Usage plans](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-usageplan.html) -- [ ] [VPC Links](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-vpclink.html) +### Missing Features + +See [awslabs/aws-cdk#723](https://github.com/awslabs/aws-cdk/issues/723) for a +list of missing features. ---- diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index e48676bb047d8..a3b1ae3e2e5ec 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -5,11 +5,7 @@ export * from './method'; export * from './integration'; export * from './deployment'; export * from './stage'; - -export * from './integrations/lambda'; -export * from './integrations/aws'; -export * from './integrations/mock'; -export * from './integrations/http'; +export * from './integrations'; // AWS::ApiGateway CloudFormation Resources: export * from './apigateway.generated'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integration.ts b/packages/@aws-cdk/aws-apigateway/lib/integration.ts index 3ce05347116ab..e1cd7e983da67 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integration.ts @@ -3,7 +3,8 @@ import { Method } from './method'; export interface IntegrationOptions { /** - * A list of request parameters whose values API Gateway caches. + * A list of request parameters whose values are to be cached. It determines + * request parameters that will make it into the cache key. */ cacheKeyParameters?: string[]; @@ -103,7 +104,6 @@ export interface IntegrationProps { /** * The Uniform Resource Identifier (URI) for the integration. * - * - If you specify MOCK for the `type` property, this is not required. * - If you specify HTTP for the `type` property, specify the API endpoint URL. * - If you specify MOCK for the `type` property, don't specify this property. * - If you specify AWS for the `type` property, specify an AWS service that diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts new file mode 100644 index 0000000000000..1369c366d655f --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts @@ -0,0 +1,4 @@ +export * from './aws'; +export * from './lambda'; +export * from './http'; +export * from './mock'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index e0a994a47787a..447cffbdc1aee 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -53,7 +53,7 @@ export interface IRestApiResource { * * @returns The newly created `Method` object. */ - onMethod(httpMethod: string, target?: Integration, options?: MethodOptions): Method; + addMethod(httpMethod: string, target?: Integration, options?: MethodOptions): Method; } export interface ResourceOptions { @@ -129,7 +129,7 @@ export class Resource extends cdk.Construct implements IRestApiResource { return new Resource(this, pathPart, { parent: this, pathPart, ...options }); } - public onMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { + public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { return new Method(this, httpMethod, { resource: this, httpMethod, integration, options }); } } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index d03f4ac49c278..3a87dbb5e9bec 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -164,8 +164,8 @@ export class RestApi extends RestApiRef implements cdk.IDependable { /** * Represents the root resource ("/") of this API. Use it to define the API model: * - * api.root.onMethod('ANY', redirectToHomePage); // "ANY /" - * api.root.addResource('friends').onMethod('GET', getFriendsHandler); // "GET /friends" + * api.root.addMethod('ANY', redirectToHomePage); // "ANY /" + * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" * */ public readonly root: IRestApiResource; @@ -210,7 +210,7 @@ export class RestApi extends RestApiRef implements cdk.IDependable { addResource: (pathPart: string, options?: ResourceOptions) => { return new Resource(this, pathPart, { parent: this.root, pathPart, ...options }); }, - onMethod: (httpMethod: string, integration?: Integration, options?: MethodOptions) => { + addMethod: (httpMethod: string, integration?: Integration, options?: MethodOptions) => { return new Method(this, httpMethod, { resource: this.root, httpMethod, integration, options }); }, defaultIntegration: props.defaultIntegration, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts index 65d4ab1f31d20..2882add039a0a 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts @@ -25,23 +25,23 @@ class BookStack extends cdk.Stack { })); const api = new apigw.RestApi(this, 'books-api'); - api.root.onMethod('ANY', hello); + api.root.addMethod('ANY', hello); const books = api.root.addResource('books', { defaultIntegration: booksHandler, defaultMethodOptions: { authorizationType: apigw.AuthorizationType.IAM } }); - books.onMethod('GET'); - books.onMethod('POST'); + books.addMethod('GET'); + books.addMethod('POST'); const book = books.addResource('{book_id}', { defaultIntegration: bookHandler // note that authorization type is inherited from /books }); - book.onMethod('GET'); - book.onMethod('DELETE'); + book.addMethod('GET'); + book.addMethod('DELETE'); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts index a9a4c09265481..77dab235bf976 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.ts @@ -8,6 +8,6 @@ const stack = new cdk.Stack(app, 'test-apigateway-restapi-defaults'); const api = new apigateway.RestApi(stack, 'my-api'); // at least one method is required -api.root.onMethod('GET'); +api.root.addMethod('GET'); process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index 3377d82787238..d91f32b99c4ab 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -33,16 +33,16 @@ class Test extends cdk.Stack { const integration = new apigateway.LambdaIntegration(handler); const toys = v1.addResource('toys'); - toys.onMethod('GET', integration); - toys.onMethod('POST'); - toys.onMethod('PUT'); + toys.addMethod('GET', integration); + toys.addMethod('POST'); + toys.addMethod('PUT'); const appliances = v1.addResource('appliances'); - appliances.onMethod('GET'); + appliances.addMethod('GET'); const books = v1.addResource('books'); - books.onMethod('GET', integration); - books.onMethod('POST', integration); + books.addMethod('GET', integration); + books.addMethod('POST', integration); function handlerCode(event: any, _: any, callback: any) { return callback(undefined, { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index f242bc83757b3..ca8d40cf07035 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -8,7 +8,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Deployment(stack, 'deployment', { api }); @@ -59,7 +59,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Deployment(stack, 'deployment', { api, retainDeployments: true }); @@ -111,7 +111,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Deployment(stack, 'deployment', { api, description: 'this is my deployment' }); @@ -129,7 +129,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); const deployment = new apigateway.Deployment(stack, 'deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // default logical ID (with no "salt") test.ok(synthesize().Resources.deployment33381975); @@ -162,7 +162,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); const deployment = new apigateway.Deployment(stack, 'deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); const dep = new cdk.Resource(stack, 'MyResource', { type: 'foo' }); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.http.ts b/packages/@aws-cdk/aws-apigateway/test/test.http.ts index de586504c8ae3..413d17fc00a9d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.http.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.http.ts @@ -12,7 +12,7 @@ export = { // WHEN const integ = new apigateway.HttpIntegration('http://foo/bar'); - api.root.onMethod('GET', integ); + api.root.addMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { @@ -40,7 +40,7 @@ export = { } }); - api.root.onMethod('GET', integ); + api.root.addMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts index b9e8a53de822d..ebe24b459fcc9 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts @@ -17,7 +17,7 @@ export = { // WHEN const integ = new apigateway.LambdaIntegration(handler); - api.root.onMethod('GET', integ); + api.root.addMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { @@ -80,7 +80,7 @@ export = { // WHEN const integ = new apigateway.LambdaIntegration(fn, { allowTestInvoke: false }); - api.root.onMethod('GET', integ); + api.root.addMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::Lambda::Permission', { @@ -134,7 +134,7 @@ export = { // WHEN const integ = new apigateway.LambdaIntegration(fn, { proxy: false }); - api.root.onMethod('GET', integ); + api.root.addMethod('GET', integ); // THEN expect(stack).to(haveResource('AWS::ApiGateway::Method', { @@ -158,7 +158,7 @@ export = { const target = new apigateway.LambdaIntegration(handler); - api.root.onMethod('ANY', target); + api.root.addMethod('ANY', target); expect(stack).to(haveResource('AWS::Lambda::Permission', { SourceArn: { diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index c19c520c04ff6..b71eea10acfd7 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -209,7 +209,7 @@ export = { const role = new iam.Role(stack, 'MyRole', { assumedBy: new cdk.ServicePrincipal('foo') }); // WHEN - api.root.onMethod('GET', new apigateway.Integration({ + api.root.addMethod('GET', new apigateway.Integration({ type: apigateway.IntegrationType.AwsProxy, options: { credentialsRole: role @@ -231,7 +231,7 @@ export = { const api = new apigateway.RestApi(stack, 'test-api', { deploy: false }); // WHEN - api.root.onMethod('GET', new apigateway.Integration({ + api.root.addMethod('GET', new apigateway.Integration({ type: apigateway.IntegrationType.AwsProxy, options: { credentialsPassthrough: true @@ -263,7 +263,7 @@ export = { }); // THEN - test.throws(() => api.root.onMethod('GET', integration), /'credentialsPassthrough' and 'credentialsRole' are mutually exclusive/); + test.throws(() => api.root.addMethod('GET', integration), /'credentialsPassthrough' and 'credentialsRole' are mutually exclusive/); test.done(); }, }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 7a7b96778442f..ddaf17380bf9c 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -13,7 +13,7 @@ export = { // WHEN const api = new apigateway.RestApi(stack, 'my-api'); - api.root.onMethod('GET'); // must have at least one method + api.root.addMethod('GET'); // must have at least one method // THEN expect(stack).toMatch({ @@ -163,7 +163,7 @@ export = { cloudWatchRole: false, }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { @@ -197,7 +197,7 @@ export = { restApiName: 'my-rest-api' }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN const foo = api.root.addResource('foo'); @@ -231,8 +231,8 @@ export = { const r1 = api.root.addResource('r1'); // WHEN - api.root.onMethod('GET'); - r1.onMethod('POST'); + api.root.addMethod('GET'); + r1.addMethod('POST'); // THEN expect(stack).toMatch({ @@ -352,7 +352,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'myapi'); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN expect(stack).to(haveResource('AWS::IAM::Role')); @@ -389,7 +389,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN test.deepEqual(cdk.resolve(api.url), { 'Fn::Join': @@ -417,7 +417,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN test.throws(() => api.url, /Cannot determine deployment stage for API from "deploymentStage". Use "deploy" or explicitly set "deploymentStage"/); @@ -429,7 +429,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN test.throws(() => api.urlForPath('foo'), /Path must begin with \"\/\": foo/); @@ -440,7 +440,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN const arn = api.executeApiArn('method', '/path', 'stage'); @@ -468,7 +468,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN test.throws(() => api.executeApiArn('method', 'hey-path', 'stage'), /"path" must begin with a "\/": 'hey-path'/); @@ -480,7 +480,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'api'); - const method = api.root.onMethod('ANY'); + const method = api.root.addMethod('ANY'); // THEN test.deepEqual(cdk.resolve(method.methodArn), { 'Fn::Join': @@ -510,7 +510,7 @@ export = { endpointTypes: [ apigateway.EndpointType.Edge, apigateway.EndpointType.Private ] }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { @@ -536,7 +536,7 @@ export = { cloneFrom }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { CloneFrom: "foobar", @@ -550,7 +550,7 @@ export = { // GIVEN const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'myapi'); - api.root.onMethod('GET'); + api.root.addMethod('GET'); const resource = new cdk.Resource(stack, 'DependsOnRestApi', { type: 'My::Resource' }); // WHEN @@ -586,13 +586,13 @@ export = { }); // CASE #1: should inherit integration and options from root resource - api.root.onMethod('GET'); + api.root.addMethod('GET'); const child = api.root.addResource('child'); // CASE #2: should inherit integration from root and method options, but // "authorizationType" will be overridden to "None" instead of "IAM" - child.onMethod('POST', undefined, { + child.addMethod('POST', undefined, { authorizationType: apigateway.AuthorizationType.Cognito }); @@ -604,10 +604,10 @@ export = { }); // CASE #3: integartion and authorizer ID are inherited from child2 - child2.onMethod('DELETE'); + child2.addMethod('DELETE'); // CASE #4: same as case #3, but integration is customized - child2.onMethod('PUT', new apigateway.AwsIntegration({ action: 'foo', service: 'bar' })); + child2.addMethod('PUT', new apigateway.AwsIntegration({ action: 'foo', service: 'bar' })); // THEN diff --git a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts index 3acf88f81c479..18bc26913126e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.stage.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts @@ -9,7 +9,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { deployment }); @@ -73,7 +73,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -102,7 +102,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -141,7 +141,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -163,7 +163,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -185,7 +185,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN test.throws(() => new apigateway.Stage(stack, 'my-stage', { @@ -202,7 +202,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // WHEN new apigateway.Stage(stack, 'my-stage', { @@ -232,7 +232,7 @@ export = { const stack = new cdk.Stack(); const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); const deployment = new apigateway.Deployment(stack, 'my-deployment', { api }); - api.root.onMethod('GET'); + api.root.addMethod('GET'); // THEN test.throws(() => new apigateway.Stage(stack, 'my-stage', {