From b0f3857b5f4869c322acfa0108d8b667bfe9cc5a Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 17 Sep 2018 13:44:56 +0300 Subject: [PATCH] feat(aws-apigateway): API Gateway Construct Library (#665) Introduce an initial construct library for API Gateway. By all means it does not cover the entire surface area of API Gateway, but provides basic support for defining API gateway configurations (resources/methods hierarchy). Integration support includes `LambdaIntegration` which accepts a `lambda.Function`, sets up the appropriate permissions and binds it to an API method. Includes many "smart defaults" at every level such as: - Automatically define a `Deployment` and a `Stage` for the API, which will be recreated every time the API configuration changes (by generating the logical ID of the AWS::ApiGateway::Deployment resource from a hash of the API configuration, similarly to SAM). - Automatically configure a role for API Gateway to allow writing CloudWatch logs and alarms. - Specify `defaultIntegration` at the API level which will apply to all methods without integration. Supports defining APIs like this: ```ts const integ = new apigw.LambdaIntegration(toysLambdaHandler); const api = new apigw.RestApi(this, 'my-awesome-api'); api.root.onMethod('GET', new apigw.HttpIntegration('https://amazon.com/toys')); const toys = api.root.addResource('toys', { defaultIntegration: integ }); toys.addMethod('ANY'); const toy = toys.addResource('{toy}'); toy.addMethod('GET'); // get a toy toy.addMethod('DELETE'); // remove a toy ``` See [README] for more details. [README]: https://github.com/awslabs/aws-cdk/blob/c99e7285e7b24700cfd4d52f6da32cffe12c511c/packages/%40aws-cdk/aws-apigateway/README.md ### Framework Changes * __resolve__: allow object keys to include tokens, as long as they resolvable to strings. * __assert__: invoke `stack.validateTree()` by default for `expect(stack)` to simulate synthesis. ---- * Features not supported yet are listed here: #727, #723 * Fixes #602, copy: @hassankhan --- packages/@aws-cdk/applet-js/package-lock.json | 2 +- packages/@aws-cdk/assert/lib/expect.ts | 25 +- packages/@aws-cdk/assert/package-lock.json | 2 +- packages/@aws-cdk/aws-apigateway/README.md | 167 ++- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 189 +++ packages/@aws-cdk/aws-apigateway/lib/index.ts | 9 + .../aws-apigateway/lib/integration.ts | 260 +++++ .../aws-apigateway/lib/integrations/aws.ts | 81 ++ .../aws-apigateway/lib/integrations/http.ts | 50 + .../aws-apigateway/lib/integrations/index.ts | 4 + .../aws-apigateway/lib/integrations/lambda.ts | 72 ++ .../aws-apigateway/lib/integrations/mock.ts | 22 + .../@aws-cdk/aws-apigateway/lib/method.ts | 198 ++++ .../@aws-cdk/aws-apigateway/lib/resource.ts | 146 +++ .../aws-apigateway/lib/restapi-ref.ts | 47 + .../@aws-cdk/aws-apigateway/lib/restapi.ts | 365 ++++++ packages/@aws-cdk/aws-apigateway/lib/stage.ts | 232 ++++ packages/@aws-cdk/aws-apigateway/lib/util.ts | 69 ++ packages/@aws-cdk/aws-apigateway/package.json | 5 +- .../test/integ.restapi.books.expected.json | 1037 +++++++++++++++++ .../test/integ.restapi.books.ts | 72 ++ .../test/integ.restapi.defaults.expected.json | 135 +++ .../test/integ.restapi.defaults.ts | 13 + .../test/integ.restapi.expected.json | 724 ++++++++++++ .../aws-apigateway/test/integ.restapi.ts | 62 + .../aws-apigateway/test/test.apigateway.ts | 8 - .../aws-apigateway/test/test.deployment.ts | 178 +++ .../@aws-cdk/aws-apigateway/test/test.http.ts | 57 + .../aws-apigateway/test/test.lambda.ts | 228 ++++ .../aws-apigateway/test/test.method.ts | 269 +++++ .../aws-apigateway/test/test.restapi.ts | 650 +++++++++++ .../aws-apigateway/test/test.stage.ts | 246 ++++ .../@aws-cdk/aws-apigateway/test/test.util.ts | 68 ++ .../@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-codepipeline/test/test.pipeline.ts | 6 +- .../@aws-cdk/aws-events/test/test.rule.ts | 3 +- .../@aws-cdk/aws-lambda/test/test.lambda.ts | 4 +- .../@aws-cdk/aws-route53/package-lock.json | 2 +- packages/@aws-cdk/aws-sqs/package-lock.json | 2 +- .../@aws-cdk/cdk/lib/cloudformation/arn.ts | 17 +- packages/@aws-cdk/cdk/lib/core/construct.ts | 2 +- packages/@aws-cdk/cdk/lib/core/tokens.ts | 9 +- packages/@aws-cdk/cdk/package-lock.json | 132 +-- .../@aws-cdk/cdk/test/core/test.tokens.ts | 28 + 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 +- 58 files changed, 5835 insertions(+), 136 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/deployment.ts create mode 100644 packages/@aws-cdk/aws-apigateway/lib/integration.ts 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/index.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/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/lib/util.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json 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.defaults.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts 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 create mode 100644 packages/@aws-cdk/aws-apigateway/test/test.util.ts 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/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/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 488e67b16c110..a44ad5b5bee21 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -1,2 +1,165 @@ -## 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 resource is +`api.root`. + +For example, the following code defines an API that includes the following HTTP +endpoints: `ANY /, GET /books`, `POST /books`, `GET /books/{book_id}`, `DELETE /books/{book_id}`. + +```ts +const api = new apigateway.RestApi(this, 'books-api'); + +api.root.addMethod('ANY'); + +const books = api.root.addResource('books'); +books.addMethod('GET'); +books.addMethod('POST'); + +const book = books.addResource('{book_id}'); +book.addMethod('GET'); +book.addMethod('DELETE'); +``` + +### 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. + * `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 and Method Options + +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`. 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 +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.root.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 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 +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 _deleted_. You can set `retainDeployments: true` +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/ + +### Missing Features + +See [awslabs/aws-cdk#723](https://github.com/awslabs/aws-cdk/issues/723) for a +list of missing features. + +---- + +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/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts new file mode 100644 index 0000000000000..f8161123633eb --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -0,0 +1,189 @@ +import cdk = require('@aws-cdk/cdk'); +import crypto = require('crypto'); +import { cloudformation, DeploymentId } from './apigateway.generated'; +import { RestApiRef } from './restapi-ref'; + +export interface DeploymentProps { + /** + * The Rest API to deploy. + */ + api: RestApiRef; + + /** + * 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 false + */ + retainDeployments?: boolean; +} + +/** + * 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 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) { + super(parent, id); + + this.resource = new LatestDeploymentResource(this, 'Resource', { + description: props.description, + restApiId: props.api.restApiId, + }); + + 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); + } + + /** + * 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 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 lazyLogicalIdRequired: boolean; + private lazyLogicalId?: string; + private hashComponents = new Array(); + + constructor(parent: cdk.Construct, id: string, props: cloudformation.DeploymentResourceProps) { + super(parent, id, props); + + // 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; + } + + /** + * 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 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) { + 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.lazyLogicalId = this.originalLogicalId + md5.digest("hex"); + } + + return []; + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index 7dbb5e9fe70c8..a3b1ae3e2e5ec 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 './restapi-ref'; +export * from './resource'; +export * from './method'; +export * from './integration'; +export * from './deployment'; +export * from './stage'; +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 new file mode 100644 index 0000000000000..e1cd7e983da67 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/integration.ts @@ -0,0 +1,260 @@ +import iam = require('@aws-cdk/aws-iam'); +import { Method } from './method'; + +export interface IntegrationOptions { + /** + * 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[]; + + /** + * An API-specific tag group of related cached parameters. + */ + cacheNamespace?: string; + + /** + * Specifies how to handle request payload content type conversions. + * + * @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. + */ + 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 }; + + /** + * 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 { + /** + * Specifies an API method integration type. + */ + type: IntegrationType; + + /** + * The Uniform Resource Identifier (URI) for the integration. + * + * - 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; +} + +/** + * 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) { } + + /** + * Can be overridden by subclasses to allow the integration to interact with the method + * being integrated, access the REST API object, method ARNs, etc. + */ + public bind(_method: Method) { + return; + } +} + +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' +} + +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..35573741828f5 --- /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/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/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts new file mode 100644 index 0000000000000..b8513002c4fde --- /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 + */ + allowTestInvoke?: 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.allowTestInvoke === 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 new file mode 100644 index 0000000000000..a1453fa771ae0 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -0,0 +1,198 @@ +import cdk = require('@aws-cdk/cdk'); +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 { + /** + * 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. + * @default None open access + */ + 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. + * @default false + */ + apiKeyRequired?: boolean; + + // TODO: + // - RequestValidatorId + // - RequestModels + // - RequestParameters + // - 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; + + /** + * The backend system that the method calls when it receives a request. + */ + integration?: Integration; + + /** + * Method options. + */ + options?: MethodOptions; +} + +export class Method extends cdk.Construct { + public readonly methodId: MethodId; + public readonly httpMethod: string; + public readonly resource: IRestApiResource; + public readonly restApi: RestApi; + + constructor(parent: cdk.Construct, id: string, props: MethodProps) { + super(parent, id); + + this.resource = props.resource; + this.restApi = props.resource.resourceApi; + this.httpMethod = props.httpMethod; + + validateHttpMethod(this.httpMethod); + + 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 || 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); + + this.methodId = resource.ref; + + props.resource.resourceApi._attachMethod(this); + + const deployment = props.resource.resourceApi.latestDeployment; + if (deployment) { + deployment.addDependency(resource); + deployment.addToLogicalId({ method: methodProps }); + } + } + + /** + * 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`'); + } + + const stage = this.restApi.deploymentStage.stageName.toString(); + return this.restApi.executeApiArn(this.httpMethod, this.resource.resourcePath, stage); + } + + /** + * 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.restApi.executeApiArn(this.httpMethod, this.resource.resourcePath, 'test-invoke-stage'); + } + + private renderIntegration(integration?: Integration): cloudformation.MethodResource.IntegrationProperty { + if (!integration) { + // use defaultIntegration from API if defined + if (this.resource.defaultIntegration) { + return this.renderIntegration(this.resource.defaultIntegration); + } + + // fallback to mock + return this.renderIntegration(new MockIntegration()); + } + + integration.bind(this); + + const options = integration.props.options || { }; + + let credentials; + if (options.credentialsPassthrough !== undefined && options.credentialsRole !== undefined) { + 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, + integrationResponses: options.integrationResponses, + credentials, + }; + } +} + +export enum AuthorizationType { + /** + * Open access. + */ + None = 'NONE', + + /** + * 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 new file mode 100644 index 0000000000000..447cffbdc1aee --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -0,0 +1,146 @@ +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'; + +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; + + /** + * The full path of this resuorce. + */ + 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 + * @param options Resource options + * @returns A Resource object + */ + addResource(pathPart: string, options?: ResourceOptions): Resource; + + /** + * 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. + */ + addMethod(httpMethod: string, target?: Integration, options?: MethodOptions): Method; +} + +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. + */ + 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; + public readonly resourcePath: string; + public readonly defaultIntegration?: Integration; + public readonly defaultMethodOptions?: MethodOptions; + + constructor(parent: cdk.Construct, id: string, props: ResourceProps) { + super(parent, id); + + validateResourcePathPart(props.pathPart); + + 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; + + // 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); + 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, options?: ResourceOptions): Resource { + return new Resource(this, pathPart, { parent: this, pathPart, ...options }); + } + + public addMethod(httpMethod: string, integration?: Integration, options?: MethodOptions): Method { + return new Method(this, httpMethod, { resource: this, httpMethod, integration, options }); + } +} + +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..e5212518f1852 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi-ref.ts @@ -0,0 +1,47 @@ +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); + } + + /** + * The ID of this API Gateway RestApi. + */ + 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()), + }; + } +} + +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..3a87dbb5e9bec --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -0,0 +1,365 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { cloudformation, ResourceId, RestApiId } from './apigateway.generated'; +import { Deployment } from './deployment'; +import { Integration } from './integration'; +import { Method, MethodOptions } from './method'; +import { IRestApiResource, Resource, ResourceOptions } from './resource'; +import { RestApiRef } from './restapi-ref'; +import { Stage, StageOptions } from './stage'; + +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. + * + * 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 `deployStageOptions` + * property. + * + * A CloudFormation Output will also be defined with the root URL endpoint + * of this REST API. + * + * @default true + */ + deploy?: boolean; + + /** + * 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` + */ + deployOptions?: StageOptions; + + /** + * Retains old deployment resources when the API changes. This allows + * manually reverting stages to point to old deployments via the AWS + * Console. + * + * @default false + */ + retainDeployments?: boolean; + + /** + * A name for the API Gateway RestApi resource. + * + * @default construct-id defaults to the id of the RestApi construct + */ + restApiName?: 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 }; + + /** + * 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; + + /** + * Automatically configure an AWS CloudWatch role for API Gateway. + * @default true + */ + cloudWatchRole?: boolean; +} + +/** + * 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 cdk.IDependable { + /** + * The ID of this API Gateway RestApi. + */ + public readonly restApiId: RestApiId; + + /** + * 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 `deploy` is false. + */ + 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). + * + * If `deploy` is disabled, you will need to explicitly assign this value in order to + * set up integrations. + */ + public deploymentStage?: Stage; + + /** + * Represents the root resource ("/") of this API. Use it to define the API model: + * + * api.root.addMethod('ANY', redirectToHomePage); // "ANY /" + * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" + * + */ + public readonly root: IRestApiResource; + + private readonly methods = new Array(); + + constructor(parent: cdk.Construct, id: string, props: RestApiProps = { }) { + super(parent, id); + + const resource = new cloudformation.RestApiResource(this, 'Resource', { + restApiName: props.restApiName || id, + 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, + }); + + this.restApiId = resource.ref; + + this.configureDeployment(props); + + const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + 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); + } + + // configure the "root" resource + this.root = { + addResource: (pathPart: string, options?: ResourceOptions) => { + return new Resource(this, pathPart, { parent: this.root, pathPart, ...options }); + }, + addMethod: (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: '/' + }; + } + + /** + * 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); + } + + /** + * @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}'`); + } + + if (method.toUpperCase() === 'ANY') { + method = '*'; + } + + 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` ]; + } + + return []; + } + + /** + * 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, + retainDeployments: props.retainDeployments + }); + + // 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.deployOptions + }); + + new cdk.Output(this, 'Endpoint', { value: this.urlForPath() }); + } else { + if (props.deployOptions) { + throw new Error(`Cannot set 'deployOptions' if 'deploy' is disabled`); + } + } + } + + 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); + } +} + +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' +} + +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 new file mode 100644 index 0000000000000..fd0026c661461 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/stage.ts @@ -0,0 +1,232 @@ +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 extends MethodDeploymentOptions { + /** + * 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. + * @default 0.5 + */ + 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 }; + + /** + * 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. + */ + + methodOptions?: { [path: string]: MethodDeploymentOptions }; +} + +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 implements cdk.IDependable { + public readonly stageName: StageName; + public readonly dependencyElements = new Array(); + + private readonly restApi: RestApiRef; + + constructor(parent: cdk.Construct, id: string, props: StageProps) { + super(parent, id); + + 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, + cacheClusterSize, + clientCertificateId: props.clientCertificateId, + deploymentId: props.deployment.deploymentId, + restApiId: props.deployment.api.restApiId, + description: props.description, + documentationVersion: props.documentationVersion, + variables: props.variables, + methodSettings, + }); + + this.stageName = resource.ref; + this.restApi = props.deployment.api; + this.dependencyElements.push(resource); + } + + /** + * Returns the invoke URL for a certain path. + * @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}`; + } + + private renderMethodSettings(props: StageProps): cloudformation.StageResource.MethodSettingProperty[] | undefined { + const settings = new Array(); + + // 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.methodOptions) { + for (const path of Object.keys(props.methodOptions)) { + settings.push(renderEntry(path, props.methodOptions[path])); + } + } + + return settings.length === 0 ? undefined : settings; + + function renderEntry(path: string, options: MethodDeploymentOptions): cloudformation.StageResource.MethodSettingProperty { + if (options.cachingEnabled) { + 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`); + } + } + + 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 662e5096229de..26e3bd29d49cd 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -54,11 +54,14 @@ "devDependencies": { "@aws-cdk/assert": "^0.9.1", "cdk-build-tools": "^0.9.1", + "cdk-integ-tools": "^0.9.1", "cfn2ts": "^0.9.1", "pkglint": "^0.9.1" }, "dependencies": { - "@aws-cdk/cdk": "^0.9.1" + "@aws-cdk/cdk": "^0.9.1", + "@aws-cdk/aws-iam": "^0.9.1", + "@aws-cdk/aws-lambda": "^0.9.1" }, "homepage": "https://github.com/awslabs/aws-cdk" } 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..93459c98efb7d --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -0,0 +1,1037 @@ +{ + "Resources": { + "BooksHandlerServiceRole5B6A8847": { + "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" + ] + ] + } + ] + } + }, + "BooksHandler3EB83358": { + "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": [ + "BooksHandlerServiceRole5B6A8847", + "Arn" + ] + }, + "Runtime": "nodejs6.10" + }, + "DependsOn": [ + "BooksHandlerServiceRole5B6A8847" + ] + }, + "BooksHandlerApiPermissionGETbooksAB573150": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "BooksHandler3EB83358" + }, + "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" + ] + ] + } + ] + ] + } + } + }, + "BooksHandlerApiPermissionTestGETbooksE0682829": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "BooksHandler3EB83358" + }, + "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" + ] + ] + } + } + }, + "BooksHandlerApiPermissionPOSTbooksC38F97D8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "BooksHandler3EB83358" + }, + "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" + ] + ] + } + ] + ] + } + } + }, + "BooksHandlerApiPermissionTestPOSTbooksCEEC4EF7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "BooksHandler3EB83358" + }, + "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" + ] + ] + } + } + }, + "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": "BookHandlerF9638A7A" + }, + "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}" + ] + ] + } + ] + ] + } + } + }, + "BookHandlerApiPermissionTestGETbooksbookid7E089259": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "BookHandlerF9638A7A" + }, + "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}" + ] + ] + } + } + }, + "BookHandlerApiPermissionDELETEbooksbookid56D0DC9D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "BookHandlerF9638A7A" + }, + "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}" + ] + ] + } + ] + ] + } + } + }, + "BookHandlerApiPermissionTestDELETEbooksbookid3E3975F4": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "BookHandlerF9638A7A" + }, + "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" + ] + }, + "HelloApiPermissionANY16B11477": { + "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" + }, + "/*/" + ] + ] + } + ] + ] + } + } + }, + "HelloApiPermissionTestANY9A757A77": { + "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/*/" + ] + ] + } + } + }, + "booksapiE1885304": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "books-api" + } + }, + "booksapiDeployment308B08F19d5655c7356bb9d23943b328416b2f5e": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "booksapiANYF4F0CDEB", + "booksapibooks97D84727", + "booksapibooksGETA776447A", + "booksapibooksPOSTF6C6559D", + "booksapibooksbookid5264BCA2", + "booksapibooksbookidGETCCE21986", + "booksapibooksbookidDELETE214F4059" + ] + }, + "booksapiDeploymentStageprod55D8E03E": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "DeploymentId": { + "Ref": "booksapiDeployment308B08F19d5655c7356bb9d23943b328416b2f5e" + }, + "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" + ] + }, + "booksapiANYF4F0CDEB": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "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": "AWS_IAM", + "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": [ + "BooksHandler3EB83358", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "booksapibooksPOSTF6C6559D": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Ref": "booksapibooks97D84727" + }, + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "AuthorizationType": "AWS_IAM", + "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": [ + "BooksHandler3EB83358", + "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": "AWS_IAM", + "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": [ + "BookHandlerF9638A7A", + "Arn" + ] + }, + "/invocations" + ] + ] + } + ] + ] + } + } + } + }, + "booksapibooksbookidDELETE214F4059": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "DELETE", + "ResourceId": { + "Ref": "booksapibooksbookid5264BCA2" + }, + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "AuthorizationType": "AWS_IAM", + "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": [ + "BookHandlerF9638A7A", + "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 new file mode 100644 index 0000000000000..2882add039a0a --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.ts @@ -0,0 +1,72 @@ +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 booksHandler = new apigw.LambdaIntegration(new lambda.Function(this, 'BooksHandler', { + runtime: lambda.Runtime.NodeJS610, + handler: 'index.handler', + 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', { + runtime: lambda.Runtime.NodeJS610, + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${helloCode}`) + })); + + const api = new apigw.RestApi(this, 'books-api'); + api.root.addMethod('ANY', hello); + + const books = api.root.addResource('books', { + defaultIntegration: booksHandler, + defaultMethodOptions: { authorizationType: apigw.AuthorizationType.IAM } + }); + + books.addMethod('GET'); + books.addMethod('POST'); + + const book = books.addResource('{book_id}', { + defaultIntegration: bookHandler + // note that authorization type is inherited from /books + }); + + book.addMethod('GET'); + book.addMethod('DELETE'); + } +} + +class BookApp extends cdk.App { + constructor(argv: string[]) { + super(argv); + + new BookStack(this, 'restapi-books-example'); + } +} + +function echoHandlerCode(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..4138766758c77 --- /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" + } + }, + "myapiDeployment92F2CB49916eaecf87f818f1e175215b8d086029": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "myapiGETF990CE3C" + ] + }, + "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" + ] + }, + "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": "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + }, + "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 new file mode 100644 index 0000000000000..77dab235bf976 --- /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.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.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json new file mode 100644 index 0000000000000..24b0cedfd60d1 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -0,0 +1,724 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "my-api" + } + }, + "myapiDeployment92F2CB49f9d1ede876fcb76aa1d523f34f91d373": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "myapiv113487378", + "myapiv1toysA55FCBC4", + "myapiv1toysGET7348114D", + "myapiv1toysPOST55128058", + "myapiv1toysPUT59AFBBC2", + "myapiv1appliances507FEFF4", + "myapiv1appliancesGET8FE872EC", + "myapiv1books1D4BE6C1", + "myapiv1booksGETC6B996D0", + "myapiv1booksPOST53E2832E" + ], + "DeletionPolicy": "Retain" + }, + "myapiDeploymentStagebeta96434BEB": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "CacheClusterEnabled": true, + "CacheClusterSize": "0.5", + "DeploymentId": { + "Ref": "myapiDeployment92F2CB49f9d1ede876fcb76aa1d523f34f91d373" + }, + "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" + ] + }, + "MyHandlerApiPermissionGETv1toys8E10C024": { + "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": "myapiDeploymentStagebeta96434BEB" + }, + "/GET/v1/toys" + ] + ] + } + ] + ] + } + } + }, + "MyHandlerApiPermissionTestGETv1toys499738A6": { + "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" + ] + ] + } + } + }, + "MyHandlerApiPermissionGETv1books376A9081": { + "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": "myapiDeploymentStagebeta96434BEB" + }, + "/GET/v1/books" + ] + ] + } + ] + ] + } + } + }, + "MyHandlerApiPermissionTestGETv1booksB64C41EB": { + "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" + ] + ] + } + } + }, + "MyHandlerApiPermissionPOSTv1booksAC487705": { + "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": "myapiDeploymentStagebeta96434BEB" + }, + "/POST/v1/books" + ] + ] + } + ] + ] + } + } + }, + "MyHandlerApiPermissionTestPOSTv1books6E15773F": { + "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": "myapiDeploymentStagebeta96434BEB" + }, + "/" + ] + ] + }, + "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 new file mode 100644 index 0000000000000..d91f32b99c4ab --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -0,0 +1,62 @@ +import lambda = require('@aws-cdk/aws-lambda'); +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', { + retainDeployments: true, + deployOptions: { + cacheClusterEnabled: true, + stageName: 'beta', + description: 'beta stage', + loggingLevel: apigateway.MethodLoggingLevel.Info, + dataTraceEnabled: true, + methodOptions: { + '/api/appliances/GET': { + cachingEnabled: true + } + } + } + }); + + const handler = new lambda.Function(this, 'MyHandler', { + runtime: lambda.Runtime.NodeJS610, + code: lambda.Code.inline(`exports.handler = ${handlerCode}`), + handler: 'index.handler', + }); + + const v1 = api.root.addResource('v1'); + + const integration = new apigateway.LambdaIntegration(handler); + + const toys = v1.addResource('toys'); + toys.addMethod('GET', integration); + toys.addMethod('POST'); + toys.addMethod('PUT'); + + const appliances = v1.addResource('appliances'); + appliances.addMethod('GET'); + + const books = v1.addResource('books'); + books.addMethod('GET', integration); + books.addMethod('POST', integration); + + function handlerCode(event: any, _: any, callback: any) { + return callback(undefined, { + isBase64Encoded: false, + statusCode: 200, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(event) + }); + } + } +} + +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 deleted file mode 100644 index db4c843199541..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/test.apigateway.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -exports = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); 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..ca8d40cf07035 --- /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.root.addMethod('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" + } + } + } + } + }); + + test.done(); + }, + + '"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.root.addMethod('GET'); + + // WHEN + new apigateway.Deployment(stack, 'deployment', { api, retainDeployments: true }); + + // 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(); + }, + + '"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.root.addMethod('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.root.addMethod('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.root.addMethod('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..413d17fc00a9d --- /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.root.addMethod('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.root.addMethod('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..ebe24b459fcc9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.lambda.ts @@ -0,0 +1,228 @@ +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.root.addMethod('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.root.addMethod('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.root.addMethod('GET', integ); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + Integration: { + Type: 'AWS' + } + })); + + 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.addMethod('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" + }, + "/", + { + "Fn::Join": [ + "", + [ + { + Ref: "testapiDeploymentStageprod5C9E92A4" + }, + "/*/" + ] + ] + } + ] + ] + } + })); + + 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..b71eea10acfd7 --- /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.root, + }); + + // 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.root, + 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.root, + 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.root, + }); + + // 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.root, + }); + + // 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.root, + }); + + // 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.root }); + + // 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.root.addMethod('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.root.addMethod('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.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 new file mode 100644 index 0000000000000..ddaf17380bf9c --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -0,0 +1,650 @@ +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'; +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.root.addMethod('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" ] + }, + 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.root.addMethod('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.root.addResource('foo'); + api.root.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.root.addMethod('GET'); + + // WHEN + const foo = api.root.addResource('foo'); + api.root.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.root.addResource('r1'); + + // WHEN + api.root.addMethod('GET'); + r1.addMethod('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.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.root.addResource('r2'); + + // THEN + test.deepEqual(api.root.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.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(); + }, + + '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.root.addMethod('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.root.addMethod('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.root.addMethod('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.root.addMethod('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.root.addMethod('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.root.addMethod('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.addMethod('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(); + + // WHEN + const api = new apigateway.RestApi(stack, 'api', { + endpointTypes: [ apigateway.EndpointType.Edge, apigateway.EndpointType.Private ] + }); + + api.root.addMethod('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.root.addMethod('GET'); + + expect(stack).to(haveResource('AWS::ApiGateway::RestApi', { + CloneFrom: "foobar", + Name: "api" + })); + + test.done(); + }, + + 'allow taking a dependency on the rest api (includes deployment and stage)'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'myapi'); + api.root.addMethod('GET'); + const resource = new cdk.Resource(stack, 'DependsOnRestApi', { type: 'My::Resource' }); + + // WHEN + resource.addDependency(api); + + // THEN + expect(stack).to(haveResource('My::Resource', { + DependsOn: [ + 'myapi162F20B8', // api + 'myapiDeploymentB7EF8EB75c091a668064a3f3a1f6d68a3fb22cf9', // deployment + 'myapiDeploymentStageprod329F21FF' // stage + ] + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + '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' + }); + + // WHEN + const api = new apigateway.RestApi(stack, 'myapi', { + defaultIntegration: rootInteg, + defaultMethodOptions: { + authorizerId: new apigateway.AuthorizerId('AUTHID'), + authorizationType: apigateway.AuthorizationType.IAM, + } + }); + + // CASE #1: should inherit integration and options from root resource + 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.addMethod('POST', undefined, { + authorizationType: apigateway.AuthorizationType.Cognito + }); + + const child2 = api.root.addResource('child2', { + defaultIntegration: new apigateway.MockIntegration(), + defaultMethodOptions: { + authorizerId: new apigateway.AuthorizerId('AUTHID2'), + } + }); + + // CASE #3: integartion and authorizer ID are inherited from child2 + child2.addMethod('DELETE'); + + // CASE #4: same as case #3, but integration is customized + child2.addMethod('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 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..18bc26913126e --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.stage.ts @@ -0,0 +1,246 @@ +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.root.addMethod('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" + } + } + }, + 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.root.addMethod('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.root.addMethod('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.root.addMethod('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.root.addMethod('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.root.addMethod('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.root.addMethod('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.root.addMethod('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 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/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-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": [ { 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", diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index f64f644504059..02ef9b9affd84 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -256,8 +256,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); 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/lib/cloudformation/arn.ts b/packages/@aws-cdk/cdk/lib/cloudformation/arn.ts index 07b6a3192ab69..ed66b033c3111 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/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 97e08df2cc486..4a505ce589486 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/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/package-lock.json b/packages/@aws-cdk/cdk/package-lock.json index 5ea779da80cef..c68b089e0c913 100644 --- a/packages/@aws-cdk/cdk/package-lock.json +++ b/packages/@aws-cdk/cdk/package-lock.json @@ -1,68 +1,68 @@ { - "name": "@aws-cdk/cdk", - "version": "0.9.0", - "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=" - } - } + "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/cdk/test/core/test.tokens.ts b/packages/@aws-cdk/cdk/test/core/test.tokens.ts index f62623f11bc2c..621539538895b 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tokens.ts @@ -242,6 +242,34 @@ export = { test.done(); }, + + 'tokens can be used in hash keys but must resolve to a string'(test: Test) { + // GIVEN + const token = new Token(() => 'I am a string'); + + // WHEN + const s = { + [token.toString()]: `boom ${token}` + }; + + // 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(); + } }; class Promise2 extends Token { 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": {