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": {