diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index d8361d290767a..6ad94b6a3e22f 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -1,4 +1,5 @@ ## Amazon API Gateway Construct Library + --- @@ -31,9 +32,10 @@ running on AWS Lambda, or any web application. - [Deep dive: Invalidation of deployments](#deep-dive-invalidation-of-deployments) - [Custom Domains](#custom-domains) - [Access Logging](#access-logging) -- [Cross Origin Resource Sharing (CORS)](cross-origin-resource-sharing-cors) +- [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) - [Endpoint Configuration](#endpoint-configuration) - [Gateway Response](#gateway-response) +- [OpenAPI Definition](#openapi-definition) - [APIGateway v2](#apigateway-v2) ## Defining APIs @@ -101,11 +103,11 @@ item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); 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 +* `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. +* `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: @@ -179,6 +181,7 @@ This construct lets you specify rate limiting properties which should be applied The API key created has the specified rate limits, such as quota and throttles, applied. The following example shows how to use a rate limited api key : + ```ts const hello = new lambda.Function(this, 'hello', { runtime: lambda.Runtime.NODEJS_10_X, @@ -450,9 +453,9 @@ iamUser.attachInlinePolicy(new iam.Policy(this, 'AllowBooks', { API Gateway also allows [lambda functions to be used as authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html). This module provides support for token-based Lambda authorizers. When a client makes a request to an API's methods configured with such -an authorizer, API Gateway calls the Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output. +an authorizer, API Gateway calls the Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output. A token-based Lambda authorizer (also called a token authorizer) receives the caller's identity in a bearer token, such as -a JSON Web Token (JWT) or an OAuth token. +a JSON Web Token (JWT) or an OAuth token. API Gateway interacts with the authorizer Lambda function handler by passing input and expecting the output in a specific format. The event object that the handler is called with contains the `authorizationToken` and the `methodArn` from the request to the @@ -491,7 +494,7 @@ depending on where the defaults were specified. This module provides support for request-based Lambda authorizers. When a client makes a request to an API's methods configured with such an authorizer, API Gateway calls the Lambda authorizer, which takes specified parts of the request, known as identity sources, -as input and returns an IAM policy as output. A request-based Lambda authorizer (also called a request authorizer) receives +as input and returns an IAM policy as output. A request-based Lambda authorizer (also called a request authorizer) receives the identity sources in a series of values pulled from the request, from the headers, stage variables, query strings, and the context. API Gateway interacts with the authorizer Lambda function handler by passing input and expecting the output in a specific format. @@ -634,8 +637,8 @@ new apigw.DomainName(this, 'custom-domain', { ``` Once you have a domain, you can map base paths of the domain to APIs. -The following example will map the URL https://example.com/go-to-api1 -to the `api1` API and https://example.com/boom to the `api2` API. +The following example will map the URL +to the `api1` API and to the `api2` API. ```ts domain.addBasePathMapping(api1, { basePath: 'go-to-api1' }); @@ -643,7 +646,7 @@ domain.addBasePathMapping(api2, { basePath: 'boom' }); ``` You can specify the API `Stage` to which this base path URL will map to. By default, this will be the -`deploymentStage` of the `RestApi`. +`deploymentStage` of the `RestApi`. ```ts const betaDeploy = new Deployment(this, 'beta-deployment', { @@ -787,7 +790,7 @@ running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own. -You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS +You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS HTTP method to any API resource via the `defaultCorsPreflightOptions` option or by calling the `addCorsPreflight` on a specific resource. The following example will enable CORS for all methods and all origins on all resources of the API: @@ -802,7 +805,7 @@ new apigateway.RestApi(this, 'api', { ``` The following example will add an OPTIONS method to the `myResource` API resource, which -only allows GET and PUT HTTP requests from the origin https://amazon.com. +only allows GET and PUT HTTP requests from the origin ```ts myResource.addCorsPreflight({ @@ -833,8 +836,8 @@ features which are not yet supported. ## Endpoint Configuration -API gateway allows you to specify an -[API Endpoint Type](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html). +API gateway allows you to specify an +[API Endpoint Type](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html). To define an endpoint type for the API gateway, use `endpointConfiguration` property: ```ts @@ -894,6 +897,30 @@ api.addGatewayResponse('test-response', { }); ``` +## OpenAPI Definition + +CDK supports creating a REST API by importing an OpenAPI definition file. It currently supports OpenAPI v2.0 and OpenAPI +v3.0 definition files. Read more about [Configuring a REST API using +OpenAPI](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-import-api.html). + +The following code creates a REST API using an external OpenAPI definition JSON file - + +```ts +const api = new apigateway.SpecRestApi(this, 'books-api', { + apiDefinition: apigateway.ApiDefinition.fromAsset('path-to-file.json') +}); +``` + +There are a number of limitations in using OpenAPI definitions in API Gateway. Read the [Amazon API Gateway important +notes for REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html#api-gateway-known-issues-rest-apis) +for more details. + +**Note:** When starting off with an OpenAPI definition using `SpecRestApi`, it is not possible to configure some +properties that can be configured directly in the OpenAPI specification file. This is to prevent people duplication +of these properties and potential confusion. +Further, it is currently also not possible to configure Methods and Resources in addition to the ones in the +specification file. + ## APIGateway v2 APIGateway v2 APIs are now moved to its own package named `aws-apigatewayv2`. For backwards compatibility, existing diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts new file mode 100644 index 0000000000000..652c531de9c38 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts @@ -0,0 +1,205 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; + +/** + * Represents an OpenAPI definition asset. + * @experimental + */ +export abstract class ApiDefinition { + /** + * Creates an API definition from a specification file in an S3 bucket + * @experimental + */ + public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3ApiDefinition { + return new S3ApiDefinition(bucket, key, objectVersion); + } + + /** + * Create an API definition from an inline object. The inline object must follow the + * schema of OpenAPI 2.0 or OpenAPI 3.0 + * + * @example + * ApiDefinition.fromInline({ + * openapi: '3.0.2', + * paths: { + * '/pets': { + * get: { + * 'responses': { + * 200: { + * content: { + * 'application/json': { + * schema: { + * $ref: '#/components/schemas/Empty', + * }, + * }, + * }, + * }, + * }, + * 'x-amazon-apigateway-integration': { + * responses: { + * default: { + * statusCode: '200', + * }, + * }, + * requestTemplates: { + * 'application/json': '{"statusCode": 200}', + * }, + * passthroughBehavior: 'when_no_match', + * type: 'mock', + * }, + * }, + * }, + * }, + * components: { + * schemas: { + * Empty: { + * title: 'Empty Schema', + * type: 'object', + * }, + * }, + * }, + * }); + */ + public static fromInline(definition: any): InlineApiDefinition { + return new InlineApiDefinition(definition); + } + + /** + * Loads the API specification from a local disk asset. + * @experimental + */ + public static fromAsset(file: string, options?: s3_assets.AssetOptions): AssetApiDefinition { + return new AssetApiDefinition(file, options); + } + + /** + * Called when the specification is initialized to allow this object to bind + * to the stack, add resources and have fun. + * + * @param scope The binding scope. Don't be smart about trying to down-cast or + * assume it's initialized. You may just use it as a construct scope. + */ + public abstract bind(scope: cdk.Construct): ApiDefinitionConfig; +} + +/** + * S3 location of the API definition file + * @experimental + */ +export interface ApiDefinitionS3Location { + /** The S3 bucket */ + readonly bucket: string; + /** The S3 key */ + readonly key: string; + /** + * An optional version + * @default - latest version + */ + readonly version?: string; +} + +/** + * Post-Binding Configuration for a CDK construct + * @experimental + */ +export interface ApiDefinitionConfig { + /** + * The location of the specification in S3 (mutually exclusive with `inlineDefinition`). + * + * @default - API definition is not an S3 location + */ + readonly s3Location?: ApiDefinitionS3Location; + + /** + * Inline specification (mutually exclusive with `s3Location`). + * + * @default - API definition is not defined inline + */ + readonly inlineDefinition?: any; +} + +/** + * OpenAPI specification from an S3 archive. + * @experimental + */ +export class S3ApiDefinition extends ApiDefinition { + private bucketName: string; + + constructor(bucket: s3.IBucket, private key: string, private objectVersion?: string) { + super(); + + if (!bucket.bucketName) { + throw new Error('bucketName is undefined for the provided bucket'); + } + + this.bucketName = bucket.bucketName; + } + + public bind(_scope: cdk.Construct): ApiDefinitionConfig { + return { + s3Location: { + bucket: this.bucketName, + key: this.key, + version: this.objectVersion, + }, + }; + } +} + +/** + * OpenAPI specification from an inline JSON object. + * @experimental + */ +export class InlineApiDefinition extends ApiDefinition { + constructor(private definition: any) { + super(); + + if (typeof(definition) !== 'object') { + throw new Error('definition should be of type object'); + } + + if (Object.keys(definition).length === 0) { + throw new Error('JSON definition cannot be empty'); + } + } + + public bind(_scope: cdk.Construct): ApiDefinitionConfig { + return { + inlineDefinition: this.definition, + }; + } +} + +/** + * OpenAPI specification from a local file. + * @experimental + */ +export class AssetApiDefinition extends ApiDefinition { + private asset?: s3_assets.Asset; + + constructor(private readonly path: string, private readonly options: s3_assets.AssetOptions = { }) { + super(); + } + + public bind(scope: cdk.Construct): ApiDefinitionConfig { + // If the same AssetAPIDefinition is used multiple times, retain only the first instantiation. + if (this.asset === undefined) { + this.asset = new s3_assets.Asset(scope, 'APIDefinition', { + path: this.path, + ...this.options, + }); + } + + if (this.asset.isZipArchive) { + throw new Error(`Asset cannot be a .zip file or a directory (${this.path})`); + } + + return { + s3Location: { + bucket: this.asset.s3BucketName, + key: this.asset.s3ObjectKey, + }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index a0b9c7529cde2..cdb63b19d2e07 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -20,6 +20,7 @@ export * from './base-path-mapping'; export * from './cors'; export * from './authorizers'; export * from './access-log'; +export * from './api-definition'; export * from './gateway-response'; // AWS::ApiGateway CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 5785574023b17..4e372f91c996c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,6 +1,7 @@ import { IVpcEndpoint } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { CfnOutput, Construct, IResource as IResourceBase, Resource, Stack } from '@aws-cdk/core'; +import { ApiDefinition } from './api-definition'; import { ApiKey, IApiKey } from './api-key'; import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { CorsOptions } from './cors'; @@ -23,7 +24,10 @@ export interface IRestApi extends IResourceBase { readonly restApiId: string; } -export interface RestApiProps extends ResourceOptions { +/** + * Represents the props that all Rest APIs share + */ +export interface RestApiOptions extends ResourceOptions { /** * Indicates if a Deployment should be automatically created for this API, * and recreated when the API model (resources, methods) changes. @@ -88,52 +92,53 @@ export interface RestApiProps extends ResourceOptions { readonly policy?: iam.PolicyDocument; /** - * A description of the purpose of this API Gateway RestApi resource. + * Indicates whether to roll back the resource if a warning occurs while API + * Gateway is creating the RestApi resource. * - * @default - No description. + * @default false */ - readonly description?: string; + readonly failOnWarnings?: boolean; /** - * The EndpointConfiguration property type specifies the endpoint types of a REST API - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-restapi-endpointconfiguration.html + * Configure a custom domain name and map it to this API. * - * @default - No endpoint configuration + * @default - no domain name is defined, use `addDomainName` or directly define a `DomainName`. */ - readonly endpointConfiguration?: EndpointConfiguration; + readonly domainName?: DomainNameOptions; /** - * A list of the endpoint types of the API. Use this property when creating - * an API. + * Automatically configure an AWS CloudWatch role for API Gateway. * - * @default - No endpoint types. - * @deprecated this property is deprecated, use endpointConfiguration instead + * @default true */ - readonly endpointTypes?: EndpointType[]; + readonly cloudWatchRole?: boolean; /** - * The source of the API key for metering requests according to a usage - * plan. + * Export name for the CfnOutput containing the API endpoint * - * @default - Metering is disabled. + * @default - when no export name is given, output will be created without export */ - readonly apiKeySourceType?: ApiKeySourceType; + readonly endpointExportName?: string; +} +/** + * Props to create a new instance of RestApi + */ +export interface RestApiProps extends RestApiOptions { /** - * The list of binary media mime-types that are supported by the RestApi - * resource, such as "image/png" or "application/octet-stream" + * A description of the purpose of this API Gateway RestApi resource. * - * @default - RestApi supports only UTF-8-encoded text payloads. + * @default - No description. */ - readonly binaryMediaTypes?: string[]; + readonly description?: string; /** - * Indicates whether to roll back the resource if a warning occurs while API - * Gateway is creating the RestApi resource. + * The list of binary media mime-types that are supported by the RestApi + * resource, such as "image/png" or "application/octet-stream" * - * @default false + * @default - RestApi supports only UTF-8-encoded text payloads. */ - readonly failOnWarnings?: boolean; + readonly binaryMediaTypes?: string[]; /** * A nullable integer that is used to enable compression (with non-negative @@ -155,65 +160,55 @@ export interface RestApiProps extends ResourceOptions { readonly cloneFrom?: IRestApi; /** - * Automatically configure an AWS CloudWatch role for API Gateway. + * The source of the API key for metering requests according to a usage + * plan. * - * @default true + * @default - Metering is disabled. */ - readonly cloudWatchRole?: boolean; + readonly apiKeySourceType?: ApiKeySourceType; /** - * Configure a custom domain name and map it to this API. + * The EndpointConfiguration property type specifies the endpoint types of a REST API + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-restapi-endpointconfiguration.html * - * @default - no domain name is defined, use `addDomainName` or directly define a `DomainName`. + * @default - No endpoint configuration */ - readonly domainName?: DomainNameOptions; + readonly endpointConfiguration?: EndpointConfiguration; /** - * Export name for the CfnOutput containing the API endpoint + * A list of the endpoint types of the API. Use this property when creating + * an API. * - * @default - when no export name is given, output will be created without export + * @default - No endpoint types. + * @deprecated this property is deprecated, use endpointConfiguration instead */ - readonly endpointExportName?: string; + readonly endpointTypes?: EndpointType[]; } /** - * 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. + * Props to instantiate a new SpecRestApi + * @experimental */ -export class RestApi extends Resource implements IRestApi { - - public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { - class Import extends Resource implements IRestApi { - public readonly restApiId = restApiId; - } - - return new Import(scope, id); - } +export interface SpecRestApiProps extends RestApiOptions { + /** + * An OpenAPI definition compatible with API Gateway. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-import-api.html + */ + readonly apiDefinition: ApiDefinition; +} +abstract class RestApiBase extends Resource implements IRestApi { /** * The ID of this API Gateway RestApi. */ - public readonly restApiId: string; + public abstract readonly restApiId: string; /** * The resource ID of the root resource. * * @attribute */ - public readonly restApiRootResourceId: string; - - /** - * 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: IResource; + public abstract readonly restApiRootResourceId: string; /** * API Gateway stage that points to the latest deployment (if defined). @@ -223,72 +218,13 @@ export class RestApi extends Resource implements IRestApi { */ public deploymentStage!: Stage; - /** - * The list of methods bound to this RestApi - */ - public readonly methods = new Array(); - + private _latestDeployment?: Deployment; private _domainName?: DomainName; - private _latestDeployment: Deployment | undefined; - constructor(scope: Construct, id: string, props: RestApiProps = { }) { + constructor(scope: Construct, id: string, props: RestApiOptions = { }) { super(scope, id, { physicalName: props.restApiName || id, }); - - const resource = new CfnRestApi(this, 'Resource', { - name: this.physicalName, - description: props.description, - policy: props.policy, - failOnWarnings: props.failOnWarnings, - minimumCompressionSize: props.minimumCompressionSize, - binaryMediaTypes: props.binaryMediaTypes, - endpointConfiguration: this.configureEndpoints(props), - apiKeySourceType: props.apiKeySourceType, - cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, - parameters: props.parameters, - }); - this.node.defaultChild = resource; - - this.restApiId = resource.ref; - - this.configureDeployment(props); - - const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; - if (cloudWatchRole) { - this.configureCloudWatchRole(resource); - } - - this.root = new RootResource(this, props, resource.attrRootResourceId); - this.restApiRootResourceId = resource.attrRootResourceId; - - if (props.domainName) { - this.addDomainName('CustomDomain', props.domainName); - } - } - - /** - * The first domain name mapped to this API, if defined through the `domainName` - * configuration prop, or added via `addDomainName` - */ - public get domainName() { - return this._domainName; - } - - /** - * 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 get latestDeployment() { - return this._latestDeployment; - } - - /** - * The deployed root URL of this REST API. - */ - public get url() { - return this.urlForPath(); } /** @@ -304,6 +240,15 @@ export class RestApi extends Resource implements IRestApi { return this.deploymentStage.urlForPath(path); } + /** + * 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 get latestDeployment() { + return this._latestDeployment; + } + /** * Defines an API Gateway domain name and maps it to this API. * @param id The construct id @@ -328,35 +273,15 @@ export class RestApi extends Resource implements IRestApi { } /** - * Add an ApiKey - */ - public addApiKey(id: string): IApiKey { - return new ApiKey(this, id, { - resources: [this], - }); - } - - /** - * Adds a new model. - */ - public addModel(id: string, props: ModelOptions): Model { - return new Model(this, id, { - ...props, - restApi: this, - }); - } - - /** - * Adds a new request validator. + * The first domain name mapped to this API, if defined through the `domainName` + * configuration prop, or added via `addDomainName` */ - public addRequestValidator(id: string, props: RequestValidatorOptions): RequestValidator { - return new RequestValidator(this, id, { - ...props, - restApi: this, - }); + public get domainName() { + return this._domainName; } /** + * Gets the "execute-api" ARN * @returns The "execute-api" ARN. * @default "*" returns the execute API ARN for all methods/resources in * this API. @@ -381,16 +306,6 @@ export class RestApi extends Resource implements IRestApi { }); } - /** - * Internal API used by `Method` to keep an inventory of methods at the API - * level for validation purposes. - * - * @internal - */ - public _attachMethod(method: Method) { - this.methods.push(method); - } - /** * Adds a new gateway response. */ @@ -401,18 +316,20 @@ export class RestApi extends Resource implements IRestApi { }); } - /** - * Performs validation of the REST API. - */ - protected validate() { - if (this.methods.length === 0) { - return [ 'The REST API doesn\'t contain any methods' ]; - } + protected configureCloudWatchRole(apiResource: CfnRestApi) { + const role = new iam.Role(this, 'CloudWatchRole', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs')], + }); - return []; + const resource = new CfnAccount(this, 'Account', { + cloudWatchRoleArn: role.roleArn, + }); + + resource.node.addDependency(apiResource); } - private configureDeployment(props: RestApiProps) { + protected configureDeployment(props: RestApiOptions) { const deploy = props.deploy === undefined ? true : props.deploy; if (deploy) { @@ -438,18 +355,192 @@ export class RestApi extends Resource implements IRestApi { } } } +} - private configureCloudWatchRole(apiResource: CfnRestApi) { - const role = new iam.Role(this, 'CloudWatchRole', { - assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), - managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs')], +/** + * Represents a REST API in Amazon API Gateway, created with an OpenAPI specification. + * + * Some properties normally accessible on @see {@link RestApi} - such as the description - + * must be declared in the specification. All Resources and Methods need to be defined as + * part of the OpenAPI specification file, and cannot be added via the CDK. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + * + * @experimental + * + * @resource AWS::ApiGateway::RestApi + */ +export class SpecRestApi extends RestApiBase { + /** + * The ID of this API Gateway RestApi. + */ + public readonly restApiId: string; + + /** + * The resource ID of the root resource. + * + * @attribute + */ + public readonly restApiRootResourceId: string; + + constructor(scope: Construct, id: string, props: SpecRestApiProps) { + super(scope, id, props); + const apiDefConfig = props.apiDefinition.bind(this); + const resource = new CfnRestApi(this, 'Resource', { + name: this.physicalName, + policy: props.policy, + failOnWarnings: props.failOnWarnings, + body: apiDefConfig.inlineDefinition ? apiDefConfig.inlineDefinition : undefined, + bodyS3Location: apiDefConfig.inlineDefinition ? undefined : apiDefConfig.s3Location, + parameters: props.parameters, }); + this.node.defaultChild = resource; + this.restApiId = resource.ref; + this.restApiRootResourceId = resource.attrRootResourceId; - const resource = new CfnAccount(this, 'Account', { - cloudWatchRoleArn: role.roleArn, + this.configureDeployment(props); + if (props.domainName) { + this.addDomainName('CustomDomain', props.domainName); + } + + const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + if (cloudWatchRole) { + this.configureCloudWatchRole(resource); + } + } +} + +/** + * 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 RestApiBase implements IRestApi { + public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { + class Import extends Resource implements IRestApi { + public readonly restApiId = restApiId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway RestApi. + */ + public readonly restApiId: string; + + /** + * 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: IResource; + + /** + * The resource ID of the root resource. + * + * @attribute + */ + public readonly restApiRootResourceId: string; + + /** + * The list of methods bound to this RestApi + */ + public readonly methods = new Array(); + + constructor(scope: Construct, id: string, props: RestApiProps = { }) { + super(scope, id, props); + + const resource = new CfnRestApi(this, 'Resource', { + name: this.physicalName, + description: props.description, + policy: props.policy, + failOnWarnings: props.failOnWarnings, + minimumCompressionSize: props.minimumCompressionSize, + binaryMediaTypes: props.binaryMediaTypes, + endpointConfiguration: this.configureEndpoints(props), + apiKeySourceType: props.apiKeySourceType, + cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, + parameters: props.parameters, }); + this.node.defaultChild = resource; + this.restApiId = resource.ref; - resource.node.addDependency(apiResource); + const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + if (cloudWatchRole) { + this.configureCloudWatchRole(resource); + } + + this.configureDeployment(props); + if (props.domainName) { + this.addDomainName('CustomDomain', props.domainName); + } + + this.root = new RootResource(this, props, resource.attrRootResourceId); + this.restApiRootResourceId = resource.attrRootResourceId; + } + + /** + * The deployed root URL of this REST API. + */ + public get url() { + return this.urlForPath(); + } + + /** + * Add an ApiKey + */ + public addApiKey(id: string): IApiKey { + return new ApiKey(this, id, { + resources: [this], + }); + } + + /** + * Adds a new model. + */ + public addModel(id: string, props: ModelOptions): Model { + return new Model(this, id, { + ...props, + restApi: this, + }); + } + + /** + * Adds a new request validator. + */ + public addRequestValidator(id: string, props: RequestValidatorOptions): RequestValidator { + return new RequestValidator(this, id, { + ...props, + restApi: this, + }); + } + + /** + * Internal API used by `Method` to keep an inventory of methods at the API + * level for validation purposes. + * + * @internal + */ + public _attachMethod(method: Method) { + this.methods.push(method); + } + + /** + * Performs validation of the REST API. + */ + protected validate() { + if (this.methods.length === 0) { + return [ "The REST API doesn't contain any methods" ]; + } + + return []; } private configureEndpoints(props: RestApiProps): CfnRestApi.EndpointConfigurationProperty | undefined { diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 3a0d997c6f292..afa03d3b12c58 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -71,24 +71,32 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.0.2" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.0.2" }, "engines": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json new file mode 100644 index 0000000000000..71dd02f17ab9a --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json @@ -0,0 +1,182 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BodyS3Location": { + "Bucket": { + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29" + }, + "Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F" + } + ] + } + ] + } + ] + ] + } + }, + "Name": "my-api" + } + }, + "myapiDeployment92F2CB49": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + } + }, + "myapiDeploymentStageprod298F01AF": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiDeployment92F2CB49" + }, + "StageName": "prod" + } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + } + }, + "PetsURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/pets" + ] + ] + } + } + }, + "Parameters": { + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29": { + "Type": "String", + "Description": "S3 bucket for asset \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" + }, + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F": { + "Type": "String", + "Description": "S3 key for asset version \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" + }, + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbArtifactHashA9C91B6D": { + "Type": "String", + "Description": "Artifact hash for asset \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts new file mode 100644 index 0000000000000..1b8531ccad8d5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts @@ -0,0 +1,21 @@ +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import * as apigateway from '../lib'; + +/* + * Stack verification steps: + * * `curl -i ` should return HTTP code 200 + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integtest-restapi-fromdefinition-asset'); + +const api = new apigateway.SpecRestApi(stack, 'my-api', { + apiDefinition: apigateway.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), +}); + +new cdk.CfnOutput(stack, 'PetsURL', { + value: api.urlForPath('/pets'), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json new file mode 100644 index 0000000000000..3eaae1ff8fd58 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json @@ -0,0 +1,177 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "openapi": "3.0.2", + "info": { + "version": "1.0.0", + "title": "Test API for CDK" + }, + "paths": { + "/pets": { + "get": { + "summary": "Test Method", + "operationId": "testMethod", + "responses": { + "200": { + "description": "A paged array of pets", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Empty" + } + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200" + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "passthroughBehavior": "when_no_match", + "type": "mock" + } + } + } + }, + "components": { + "schemas": { + "Empty": { + "title": "Empty Schema", + "type": "object" + } + } + } + }, + "Name": "my-api" + } + }, + "myapiDeployment92F2CB49": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + } + }, + "myapiDeploymentStageprod298F01AF": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiDeployment92F2CB49" + }, + "StageName": "prod" + } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + } + }, + "PetsURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/pets" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts new file mode 100644 index 0000000000000..4d9f3bcf76364 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts @@ -0,0 +1,66 @@ +import * as cdk from '@aws-cdk/core'; +import * as apigateway from '../lib'; + +/* + * Stack verification steps: + * * `curl -i ` should return HTTP code 200 + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integtest-restapi-fromdefinition-inline'); + +const api = new apigateway.SpecRestApi(stack, 'my-api', { + apiDefinition: apigateway.ApiDefinition.fromInline({ + openapi: '3.0.2', + info: { + version: '1.0.0', + title: 'Test API for CDK', + }, + paths: { + '/pets': { + get: { + 'summary': 'Test Method', + 'operationId': 'testMethod', + 'responses': { + 200: { + description: 'A paged array of pets', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Empty', + }, + }, + }, + }, + }, + 'x-amazon-apigateway-integration': { + responses: { + default: { + statusCode: '200', + }, + }, + requestTemplates: { + 'application/json': '{"statusCode": 200}', + }, + passthroughBehavior: 'when_no_match', + type: 'mock', + }, + }, + }, + }, + components: { + schemas: { + Empty: { + title: 'Empty Schema', + type: 'object', + }, + }, + }, + }), +}); + +new cdk.CfnOutput(stack, 'PetsURL', { + value: api.urlForPath('/pets'), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml new file mode 100644 index 0000000000000..a0dd197f67c37 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml @@ -0,0 +1,30 @@ +openapi: "3.0.2" +info: + version: 1.0.0 + title: Test API for CDK +paths: + /pets: + get: + summary: Test Method + operationId: testMethod + responses: + "200": + description: A paged array of pets + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-integration: + responses: + default: + statusCode: "200" + requestTemplates: + application/json: "{\"statusCode\": 200}" + passthroughBehavior: when_no_match + type: mock + +components: + schemas: + Empty: + title: Empty Schema + type: object \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts b/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts new file mode 100644 index 0000000000000..7753c83fe47a8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts @@ -0,0 +1,102 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as path from 'path'; +import * as apigw from '../lib'; + +export = { + 'apigateway.ApiDefinition.fromJson': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const definition = { + key1: 'val1', + }; + const config = apigw.ApiDefinition.fromInline(definition).bind(stack); + test.deepEqual(config.inlineDefinition, definition); + test.ok(config.s3Location === undefined); + test.done(); + }, + + 'fails if Json definition is empty'(test: Test) { + test.throws( + () => defineRestApi(apigw.ApiDefinition.fromInline({})), + /cannot be empty/); + test.done(); + }, + + 'fails if definition is not an object'(test: Test) { + test.throws( + () => defineRestApi(apigw.ApiDefinition.fromInline('not-json')), + /should be of type object/); + test.done(); + }, + }, + + 'apigateway.ApiDefinition.fromAsset': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const config = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')).bind(stack); + test.ok(config.inlineDefinition === undefined); + test.ok(config.s3Location !== undefined); + test.deepEqual(stack.resolve(config.s3Location!.bucket), { + Ref: 'AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29', + }); + test.done(); + }, + + 'fails if a directory is given for an asset'(test: Test) { + // GIVEN + const fileAsset = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'authorizers')); + + // THEN + test.throws(() => defineRestApi(fileAsset), /Asset cannot be a \.zip file or a directory/); + test.done(); + }, + + 'only one Asset object gets created even if multiple functions use the same AssetApiDefinition'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'MyStack'); + const directoryAsset = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + + // WHEN + new apigw.SpecRestApi(stack, 'API1', { + apiDefinition: directoryAsset, + }); + + new apigw.SpecRestApi(stack, 'API2', { + apiDefinition: directoryAsset, + }); + + // THEN + const assembly = app.synth(); + const synthesized = assembly.stacks[0]; + + // API1 has an asset, API2 does not + test.deepEqual(synthesized.assets.length, 1); + test.done(); + }, + }, + + 'apigateway.ApiDefinition.fromBucket': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'my-bucket'); + const config = apigw.ApiDefinition.fromBucket(bucket, 'my-key', 'my-version').bind(stack); + test.ok(config.inlineDefinition === undefined); + test.ok(config.s3Location !== undefined); + test.deepEqual(stack.resolve(config.s3Location!.bucket), { + Ref: 'mybucket15D133BF', + }); + test.equals(config.s3Location!.key, 'my-key'); + test.done(); + }, + }, +}; + +function defineRestApi(definition: apigw.ApiDefinition) { + const stack = new cdk.Stack(); + return new apigw.SpecRestApi(stack, 'API', { + apiDefinition: definition, + }); +} diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 6997bcf31b220..2be05b8e8c1f1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -13,7 +13,7 @@ export = { // WHEN const api = new apigw.RestApi(stack, 'my-api'); - api.root.addMethod('GET'); // must have at least one method + api.root.addMethod('GET'); // must have at least one method or an API definition // THEN expect(stack).toMatch({ @@ -127,7 +127,7 @@ export = { test.done(); }, - 'fails in synthesis if there are no methods'(test: Test) { + 'fails in synthesis if there are no methods or definition'(test: Test) { // GIVEN const app = new App(); const stack = new Stack(app, 'my-stack');