diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 810267a9ab428..edaef65689c87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -184,6 +184,17 @@ Integration tests perform a few functions in the CDK code base - 3. (Optionally) Acts as a way to validate that constructs set up the CloudFormation resources as expected. A successful CloudFormation deployment does not mean that the resources are set up correctly. +For Gitpod users only! The best way to supply CDK with your AWS credentials is to add them as +[persisting environment variables](https://www.gitpod.io/docs/environment-variables). +Adding them works as follows via terminal: + +```shell +eval $(gp env -e AWS_ACCESS_KEY_ID=XXXXXXXXX) +eval $(gp env -e AWS_SECRET_ACCESS_KEY=YYYYYYY) +eval $(gp env -e AWS_DEFAULT_REGION=ZZZZZZZZ) +eval $(gp env -e) +``` + If you are working on a new feature that is using previously unused CloudFormation resource types, or involves configuring resource types across services, you need to write integration tests that use these resource types or features. diff --git a/package.json b/package.json index 60b8110db0128..e1387a4881b48 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,12 @@ "devDependencies": { "conventional-changelog-cli": "^2.0.34", "fs-extra": "^9.0.1", + "graceful-fs": "^4.2.4", "jsii-diff": "^1.6.0", "jsii-pacmak": "^1.6.0", "jsii-rosetta": "^1.6.0", "lerna": "^3.22.1", "standard-version": "^8.0.0", - "graceful-fs": "^4.2.4", "typescript": "~3.8.3" }, "resolutions-comment": "should be removed or reviewed when nodeunit dependency is dropped or adjusted", diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index db77409db42d4..a844fe67cfd03 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -57,7 +57,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-amplify/lib/app.ts b/packages/@aws-cdk/aws-amplify/lib/app.ts index 483bbbb58d6c7..640c9684625df 100644 --- a/packages/@aws-cdk/aws-amplify/lib/app.ts +++ b/packages/@aws-cdk/aws-amplify/lib/app.ts @@ -372,6 +372,11 @@ export enum RedirectStatus { * Not found (404) */ NOT_FOUND = '404', + + /** + * Not found rewrite (404) + */ + NOT_FOUND_REWRITE = '404-200', } /** diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 3be9cea704d17..6045d52aa95d3 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -19,6 +19,7 @@ running on AWS Lambda, or any web application. ## Table of Contents - [Defining APIs](#defining-apis) + - [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks) - [AWS Lambda-backed APIs](#aws-lambda-backed-apis) - [Integration Targets](#integration-targets) - [Working with models](#working-with-models) @@ -99,6 +100,18 @@ item.addMethod('GET'); // GET /items/{item} item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); ``` +### Breaking up Methods and Resources across Stacks + +It is fairly common for REST APIs with a large number of Resources and Methods to hit the [CloudFormation +limit](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html) of 200 resources per +stack. + +To help with this, Resources and Methods for the same REST API can be re-organized across multiple stacks. A common +way to do this is to have a stack per Resource or groups of Resources, but this is not the only possible way. +The following example uses sets up two Resources '/pets' and '/books' in separate stacks using nested stacks: + +[Resources grouped into nested stacks](test/integ.restapi-import.lit.ts) + ## Integration Targets Methods are associated with backend integrations, which are invoked when this @@ -956,8 +969,20 @@ The following code creates a REST API using an external OpenAPI definition JSON const api = new apigateway.SpecRestApi(this, 'books-api', { apiDefinition: apigateway.ApiDefinition.fromAsset('path-to-file.json') }); + +const booksResource = api.root.addResource('books') +booksResource.addMethod('GET', ...); ``` +It is possible to use the `addResource()` API to define additional API Gateway Resources. + +**Note:** Deployment will fail if a Resource of the same name is already defined in the Open API specification. + +**Note:** Any default properties configured, such as `defaultIntegration`, `defaultMethodOptions`, etc. will only be +applied to Resources and Methods defined in the CDK, and not the ones defined in the spec. Use the [API Gateway +extensions to OpenAPI](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions.html) +to configure these. + 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. @@ -965,8 +990,6 @@ 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 diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts index a51f30e14514c..ea414a1c43584 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizer.ts @@ -1,6 +1,6 @@ import { Construct, Resource, ResourceProps } from '@aws-cdk/core'; import { AuthorizationType } from './method'; -import { RestApi } from './restapi'; +import { IRestApi } from './restapi'; const AUTHORIZER_SYMBOL = Symbol.for('@aws-cdk/aws-apigateway.Authorizer'); @@ -28,7 +28,7 @@ export abstract class Authorizer extends Resource implements IAuthorizer { * Called when the authorizer is used from a specific REST API. * @internal */ - public abstract _attachToApi(restApi: RestApi): void; + public abstract _attachToApi(restApi: IRestApi): void; } /** diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 9215c28de1e61..f79d675af1e7f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -3,7 +3,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, Duration, Lazy, Stack } from '@aws-cdk/core'; import { CfnAuthorizer } from '../apigateway.generated'; import { Authorizer, IAuthorizer } from '../authorizer'; -import { RestApi } from '../restapi'; +import { IRestApi } from '../restapi'; /** * Base properties for all lambda authorizers @@ -83,7 +83,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { * Attaches this authorizer to a specific REST API. * @internal */ - public _attachToApi(restApi: RestApi) { + public _attachToApi(restApi: IRestApi) { if (this.restApiId && this.restApiId !== restApi.restApiId) { throw new Error('Cannot attach authorizer to two different rest APIs'); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index 58c504ab4aa8a..a7af625a4d121 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -7,7 +7,7 @@ import { MethodResponse } from './methodresponse'; import { IModel } from './model'; import { IRequestValidator, RequestValidatorOptions } from './requestvalidator'; import { IResource } from './resource'; -import { RestApi } from './restapi'; +import { IRestApi, RestApi, RestApiBase } from './restapi'; import { validateHttpMethod } from './util'; export interface MethodOptions { @@ -159,13 +159,16 @@ export class Method extends Resource { public readonly httpMethod: string; public readonly resource: IResource; - public readonly restApi: RestApi; + /** + * The API Gateway RestApi associated with this method. + */ + public readonly api: IRestApi; constructor(scope: Construct, id: string, props: MethodProps) { super(scope, id); this.resource = props.resource; - this.restApi = props.resource.restApi; + this.api = props.resource.api; this.httpMethod = props.httpMethod.toUpperCase(); validateHttpMethod(this.httpMethod); @@ -186,12 +189,12 @@ export class Method extends Resource { } if (Authorizer.isAuthorizer(authorizer)) { - authorizer._attachToApi(this.restApi); + authorizer._attachToApi(this.api); } const methodProps: CfnMethodProps = { resourceId: props.resource.resourceId, - restApiId: this.restApi.restApiId, + restApiId: this.api.restApiId, httpMethod: this.httpMethod, operationName: options.operationName || defaultMethodOptions.operationName, apiKeyRequired: options.apiKeyRequired || defaultMethodOptions.apiKeyRequired, @@ -209,15 +212,25 @@ export class Method extends Resource { this.methodId = resource.ref; - props.resource.restApi._attachMethod(this); + if (RestApiBase._isRestApiBase(props.resource.api)) { + props.resource.api._attachMethod(this); + } - const deployment = props.resource.restApi.latestDeployment; + const deployment = props.resource.api.latestDeployment; if (deployment) { deployment.node.addDependency(resource); deployment.addToLogicalId({ method: methodProps }); } } + /** + * The RestApi associated with this Method + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ + public get restApi(): RestApi { + return this.resource.restApi; + } + /** * Returns an execute-api ARN for this method: * diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 2d916780bf3e2..102a17a5cdc27 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -4,7 +4,7 @@ import { Cors, CorsOptions } from './cors'; import { Integration } from './integration'; import { MockIntegration } from './integrations'; import { Method, MethodOptions } from './method'; -import { RestApi } from './restapi'; +import { IRestApi, RestApi } from './restapi'; export interface IResource extends IResourceBase { /** @@ -12,6 +12,13 @@ export interface IResource extends IResourceBase { */ readonly parentResource?: IResource; + /** + * The rest API that this resource is part of. + * + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ + readonly restApi: RestApi; + /** * The rest API that this resource is part of. * @@ -20,7 +27,7 @@ export interface IResource extends IResourceBase { * 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 restApi: RestApi; + readonly api: IRestApi; /** * The ID of the resource. @@ -154,7 +161,11 @@ export interface ResourceProps extends ResourceOptions { export abstract class ResourceBase extends ResourceConstruct implements IResource { public abstract readonly parentResource?: IResource; + /** + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ public abstract readonly restApi: RestApi; + public abstract readonly api: IRestApi; public abstract readonly resourceId: string; public abstract readonly path: string; public abstract readonly defaultIntegration?: Integration; @@ -353,6 +364,9 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc return resource.resourceForPath(parts.join('/')); } + /** + * @deprecated - Throws error in some use cases that have been enabled since this deprecation notice. Use `RestApi.urlForPath()` instead. + */ public get url(): string { return this.restApi.urlForPath(this.path); } @@ -360,7 +374,7 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc export class Resource extends ResourceBase { public readonly parentResource?: IResource; - public readonly restApi: RestApi; + public readonly api: IRestApi; public readonly resourceId: string; public readonly path: string; @@ -380,21 +394,21 @@ export class Resource extends ResourceBase { } const resourceProps: CfnResourceProps = { - restApiId: props.parent.restApi.restApiId, + restApiId: props.parent.api.restApiId, parentId: props.parent.resourceId, pathPart: props.pathPart, }; const resource = new CfnResource(this, 'Resource', resourceProps); this.resourceId = resource.ref; - this.restApi = props.parent.restApi; + this.api = props.parent.api; // render resource path (special case for root) this.path = props.parent.path; if (!this.path.endsWith('/')) { this.path += '/'; } this.path += props.pathPart; - const deployment = props.parent.restApi.latestDeployment; + const deployment = props.parent.api.latestDeployment; if (deployment) { deployment.node.addDependency(resource); deployment.addToLogicalId({ resource: resourceProps }); @@ -413,6 +427,17 @@ export class Resource extends ResourceBase { this.addCorsPreflight(this.defaultCorsPreflightOptions); } } + + /** + * The RestApi associated with this Resource + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ + public get restApi(): RestApi { + if (!this.parentResource) { + throw new Error('parentResource was unexpectedly not defined'); + } + return this.parentResource.restApi; + } } export interface ProxyResourceOptions extends ResourceOptions { diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 5a43b562ff279..4d08a0b01ce36 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -16,12 +16,36 @@ import { IResource, ResourceBase, ResourceOptions } from './resource'; import { Stage, StageOptions } from './stage'; import { UsagePlan, UsagePlanProps } from './usage-plan'; +const RESTAPI_SYMBOL = Symbol.for('@aws-cdk/aws-apigateway.RestApiBase'); + export interface IRestApi extends IResourceBase { /** * The ID of this API Gateway RestApi. * @attribute */ readonly restApiId: string; + + /** + * The resource ID of the root resource. + * @attribute + */ + readonly restApiRootResourceId: string; + + /** + * API Gateway deployment that represents the latest changes of the API. + * This resource will be automatically updated every time the REST API model changes. + * `undefined` when no deployment is configured. + */ + readonly latestDeployment?: Deployment; + + /** + * 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" + * + */ + readonly root: IResource; } /** @@ -197,7 +221,36 @@ export interface SpecRestApiProps extends RestApiOptions { readonly apiDefinition: ApiDefinition; } -abstract class RestApiBase extends Resource implements IRestApi { +/** + * Base implementation that are common to various implementations of IRestApi + */ +export abstract class RestApiBase extends Resource implements IRestApi { + + /** + * Checks if the given object is an instance of RestApiBase. + * @internal + */ + public static _isRestApiBase(x: any): x is RestApiBase { + return x !== null && typeof(x) === 'object' && RESTAPI_SYMBOL in x; + } + + /** + * 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 first domain name mapped to this API, if defined through the `domainName` + * configuration prop, or added via `addDomainName` + */ + public get domainName() { + return this._domainName; + } + /** * The ID of this API Gateway RestApi. */ @@ -210,6 +263,12 @@ abstract class RestApiBase extends Resource implements IRestApi { */ public abstract readonly restApiRootResourceId: string; + /** + * Represents the root resource of this API endpoint ('/'). + * Resources and Methods are added to this resource. + */ + public abstract readonly root: IResource; + /** * API Gateway stage that points to the latest deployment (if defined). * @@ -225,6 +284,8 @@ abstract class RestApiBase extends Resource implements IRestApi { super(scope, id, { physicalName: props.restApiName || id, }); + + Object.defineProperty(this, RESTAPI_SYMBOL, { value: true }); } /** @@ -240,15 +301,6 @@ abstract class RestApiBase 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 @@ -272,14 +324,6 @@ abstract class RestApiBase extends Resource implements IRestApi { return new UsagePlan(this, id, props); } - /** - * 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; - } - /** * Gets the "execute-api" ARN * @returns The "execute-api" ARN. @@ -316,6 +360,16 @@ abstract class RestApiBase 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) { + ignore(method); + } + protected configureCloudWatchRole(apiResource: CfnRestApi) { const role = new iam.Role(this, 'CloudWatchRole', { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), @@ -384,6 +438,8 @@ export class SpecRestApi extends RestApiBase { */ public readonly restApiRootResourceId: string; + public readonly root: IResource; + constructor(scope: Construct, id: string, props: SpecRestApiProps) { super(scope, id, props); const apiDefConfig = props.apiDefinition.bind(this); @@ -398,6 +454,7 @@ export class SpecRestApi extends RestApiBase { this.node.defaultChild = resource; this.restApiId = resource.ref; this.restApiRootResourceId = resource.attrRootResourceId; + this.root = new RootResource(this, props, this.restApiRootResourceId); this.configureDeployment(props); if (props.domainName) { @@ -411,6 +468,21 @@ export class SpecRestApi extends RestApiBase { } } +/** + * Attributes that can be specified when importing a RestApi + */ +export interface RestApiAttributes { + /** + * The ID of the API Gateway RestApi. + */ + readonly restApiId: string; + + /** + * The resource ID of the root resource. + */ + readonly rootResourceId: string; +} + /** * Represents a REST API in Amazon API Gateway. * @@ -419,34 +491,44 @@ export class SpecRestApi extends RestApiBase { * By default, the API will automatically be deployed and accessible from a * public endpoint. */ -export class RestApi extends RestApiBase implements IRestApi { +export class RestApi extends RestApiBase { + /** + * Import an existing RestApi. + */ public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { class Import extends Resource implements IRestApi { public readonly restApiId = restApiId; + + public get root(): IResource { + throw new Error('root is not configured when imported using `fromRestApiId()`. Use `fromRestApiAttributes()` API instead.'); + } + + public get restApiRootResourceId(): string { + throw new Error('restApiRootResourceId is not configured when imported using `fromRestApiId()`. Use `fromRestApiAttributes()` API instead.'); + } } return new Import(scope, id); } /** - * The ID of this API Gateway RestApi. + * Import an existing RestApi that can be configured with additional Methods and Resources. + * @experimental */ + public static fromRestApiAttributes(scope: Construct, id: string, attrs: RestApiAttributes): IRestApi { + class Import extends RestApiBase { + public readonly restApiId = attrs.restApiId; + public readonly restApiRootResourceId = attrs.rootResourceId; + public readonly root: IResource = new RootResource(this, {}, this.restApiRootResourceId); + } + + return new Import(scope, id); + } + 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; /** @@ -613,26 +695,47 @@ export enum EndpointType { class RootResource extends ResourceBase { public readonly parentResource?: IResource; - public readonly restApi: RestApi; + public readonly api: RestApiBase; public readonly resourceId: string; public readonly path: string; public readonly defaultIntegration?: Integration | undefined; public readonly defaultMethodOptions?: MethodOptions | undefined; public readonly defaultCorsPreflightOptions?: CorsOptions | undefined; - constructor(api: RestApi, props: RestApiProps, resourceId: string) { + private readonly _restApi?: RestApi; + + constructor(api: RestApiBase, props: ResourceOptions, resourceId: string) { super(api, 'Default'); this.parentResource = undefined; this.defaultIntegration = props.defaultIntegration; this.defaultMethodOptions = props.defaultMethodOptions; this.defaultCorsPreflightOptions = props.defaultCorsPreflightOptions; - this.restApi = api; + this.api = api; this.resourceId = resourceId; this.path = '/'; + if (api instanceof RestApi) { + this._restApi = api; + } + if (this.defaultCorsPreflightOptions) { this.addCorsPreflight(this.defaultCorsPreflightOptions); } } + + /** + * Get the RestApi associated with this Resource. + * @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead. + */ + public get restApi(): RestApi { + if (!this._restApi) { + throw new Error('RestApi is not available on Resource not connected to an instance of RestApi. Use `api` instead'); + } + return this._restApi; + } } + +function ignore(_x: any) { + return; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index c3003c458ea57..6816f6cc02ab7 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -176,14 +176,12 @@ "docs-public-apis:@aws-cdk/aws-apigateway.Method.httpMethod", "docs-public-apis:@aws-cdk/aws-apigateway.Method.methodId", "docs-public-apis:@aws-cdk/aws-apigateway.Method.resource", - "docs-public-apis:@aws-cdk/aws-apigateway.Method.restApi", "docs-public-apis:@aws-cdk/aws-apigateway.Model", "docs-public-apis:@aws-cdk/aws-apigateway.Model.fromModelName", "docs-public-apis:@aws-cdk/aws-apigateway.RequestValidator", "docs-public-apis:@aws-cdk/aws-apigateway.RequestValidator.fromRequestValidatorId", "docs-public-apis:@aws-cdk/aws-apigateway.Resource", "docs-public-apis:@aws-cdk/aws-apigateway.ResourceBase", - "docs-public-apis:@aws-cdk/aws-apigateway.RestApi.fromRestApiId", "docs-public-apis:@aws-cdk/aws-apigateway.RestApi.arnForExecuteApi", "docs-public-apis:@aws-cdk/aws-apigateway.Stage", "docs-public-apis:@aws-cdk/aws-apigateway.Stage.restApi", 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 index bcf74c12601fa..8946e415c6874 100644 --- 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 @@ -44,14 +44,63 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd": { + "myapibooks51D54548": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "myapi4C7BF186", + "RootResourceId" + ] + }, + "PathPart": "books", + "RestApiId": { + "Ref": "myapi4C7BF186" + } + } + }, + "myapibooksGETD6B2F597": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "myapibooks51D54548" + }, + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationResponses": [ + { + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": "{ \"statusCode\": 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "StatusCode": "200" + } + ] + } + }, + "myapiDeployment92F2CB49fe116fef7f552ff0fc433c9aa3930d2f": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "myapi4C7BF186" }, "Description": "Automatically created by the RestApi construct" - } + }, + "DependsOn": [ + "myapibooksGETD6B2F597", + "myapibooks51D54548" + ] }, "myapiDeploymentStageprod298F01AF": { "Type": "AWS::ApiGateway::Stage", @@ -60,7 +109,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd" + "Ref": "myapiDeployment92F2CB49fe116fef7f552ff0fc433c9aa3930d2f" }, "StageName": "prod" } @@ -163,6 +212,32 @@ ] ] } + }, + "BooksURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/books" + ] + ] + } } }, "Parameters": { 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 index 1b8531ccad8d5..63e6343f4de26 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts @@ -4,7 +4,8 @@ import * as apigateway from '../lib'; /* * Stack verification steps: - * * `curl -i ` should return HTTP code 200 + * * `curl -s -o /dev/null -w "%{http_code}" ` should return HTTP code 200 + * * `curl -s -o /dev/null -w "%{http_code}" ` should return HTTP code 200 */ const app = new cdk.App(); @@ -14,8 +15,24 @@ const api = new apigateway.SpecRestApi(stack, 'my-api', { apiDefinition: apigateway.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), }); +api.root.addResource('books').addMethod('GET', new apigateway.MockIntegration({ + integrationResponses: [{ + statusCode: '200', + }], + passthroughBehavior: apigateway.PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, +}), { + methodResponses: [ { statusCode: '200' } ], +}); + new cdk.CfnOutput(stack, 'PetsURL', { value: api.urlForPath('/pets'), }); +new cdk.CfnOutput(stack, 'BooksURL', { + value: api.urlForPath('/books'), +}); + app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.expected.json new file mode 100644 index 0000000000000..349ae37ce27c8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.expected.json @@ -0,0 +1,334 @@ +{ + "Resources": { + "RestApi0C43BF4B": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "RestApi" + } + }, + "RestApiCloudWatchRoleE3ED6605": { + "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" + ] + ] + } + ] + } + }, + "RestApiAccount7C83CF5A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "RestApiCloudWatchRoleE3ED6605", + "Arn" + ] + } + }, + "DependsOn": [ + "RestApi0C43BF4B" + ] + }, + "RestApiANYA7C1DC94": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "NONE", + "Integration": { + "Type": "MOCK" + } + } + }, + "integrestapiimportPetsStackNestedStackintegrestapiimportPetsStackNestedStackResource2B31898B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3BucketFE7B8A1B" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3VersionKeyB80604FE" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3VersionKeyB80604FE" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetointegrestapiimportRootStackRestApi2647DA4CRootResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "referencetointegrestapiimportRootStackRestApi2647DA4CRef": { + "Ref": "RestApi0C43BF4B" + } + } + } + }, + "integrestapiimportBooksStackNestedStackintegrestapiimportBooksStackNestedStackResource395C2C9B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3Bucket74F8A623" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3VersionKeyC855AC3B" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3VersionKeyC855AC3B" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetointegrestapiimportRootStackRestApi2647DA4CRootResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "referencetointegrestapiimportRootStackRestApi2647DA4CRef": { + "Ref": "RestApi0C43BF4B" + } + } + } + }, + "integrestapiimportDeployStackNestedStackintegrestapiimportDeployStackNestedStackResource0D0EE737": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3BucketADE4C6AE" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3VersionKeyF36B0062" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3VersionKeyF36B0062" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetointegrestapiimportRootStackRestApi2647DA4CRef": { + "Ref": "RestApi0C43BF4B" + } + } + }, + "DependsOn": [ + "integrestapiimportBooksStackNestedStackintegrestapiimportBooksStackNestedStackResource395C2C9B", + "integrestapiimportPetsStackNestedStackintegrestapiimportPetsStackNestedStackResource2B31898B" + ] + } + }, + "Outputs": { + "PetsURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/prod/pets" + ] + ] + } + }, + "BooksURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/prod/books" + ] + ] + } + } + }, + "Parameters": { + "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3BucketFE7B8A1B": { + "Type": "String", + "Description": "S3 bucket for asset \"c6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1ef\"" + }, + "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efS3VersionKeyB80604FE": { + "Type": "String", + "Description": "S3 key for asset version \"c6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1ef\"" + }, + "AssetParametersc6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1efArtifactHashED1A6259": { + "Type": "String", + "Description": "Artifact hash for asset \"c6464ef3a9925cfe5c28d912ee7fc0952eb5135b281419c8d450a3aa8825e1ef\"" + }, + "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3Bucket74F8A623": { + "Type": "String", + "Description": "S3 bucket for asset \"480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141\"" + }, + "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141S3VersionKeyC855AC3B": { + "Type": "String", + "Description": "S3 key for asset version \"480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141\"" + }, + "AssetParameters480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141ArtifactHash1198374C": { + "Type": "String", + "Description": "Artifact hash for asset \"480caddfb9aa669df64905982e75c672d967ce9d9ed261ee8c73f6bdcaf97141\"" + }, + "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3BucketADE4C6AE": { + "Type": "String", + "Description": "S3 bucket for asset \"04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86ab\"" + }, + "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abS3VersionKeyF36B0062": { + "Type": "String", + "Description": "S3 key for asset version \"04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86ab\"" + }, + "AssetParameters04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86abArtifactHash6DD5E125": { + "Type": "String", + "Description": "Artifact hash for asset \"04407a85c5bf6d4da110e25ee35b1f67903f760cd7835965518b0f7ad37e86ab\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.ts new file mode 100644 index 0000000000000..bea2be6c5b05f --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi-import.lit.ts @@ -0,0 +1,124 @@ +import { App, CfnOutput, Construct, NestedStack, NestedStackProps, Stack } from '@aws-cdk/core'; +import { Deployment, Method, MockIntegration, PassthroughBehavior, RestApi, Stage } from '../lib'; + +/** + * This file showcases how to split up a RestApi's Resources and Methods across nested stacks. + * + * The root stack 'RootStack' first defines a RestApi. + * Two nested stacks BooksStack and PetsStack, create corresponding Resources '/books' and '/pets'. + * They are then deployed to a 'prod' Stage via a third nested stack - DeployStack. + * + * To verify this worked, go to the APIGateway + */ + +class RootStack extends Stack { + constructor(scope: Construct) { + super(scope, 'integ-restapi-import-RootStack'); + + const restApi = new RestApi(this, 'RestApi', { + deploy: false, + }); + restApi.root.addMethod('ANY'); + + const petsStack = new PetsStack(this, { + restApiId: restApi.restApiId, + rootResourceId: restApi.restApiRootResourceId, + }); + const booksStack = new BooksStack(this, { + restApiId: restApi.restApiId, + rootResourceId: restApi.restApiRootResourceId, + }); + new DeployStack(this, { + restApiId: restApi.restApiId, + methods: [ ...petsStack.methods, ...booksStack.methods ], + }); + + new CfnOutput(this, 'PetsURL', { + value: `https://${restApi.restApiId}.execute-api.${this.region}.amazonaws.com/prod/pets`, + }); + + new CfnOutput(this, 'BooksURL', { + value: `https://${restApi.restApiId}.execute-api.${this.region}.amazonaws.com/prod/books`, + }); + } +} + +interface ResourceNestedStackProps extends NestedStackProps { + readonly restApiId: string; + + readonly rootResourceId: string; +} + +class PetsStack extends NestedStack { + public readonly methods: Method[] = []; + + constructor(scope: Construct, props: ResourceNestedStackProps) { + super(scope, 'integ-restapi-import-PetsStack', props); + + const api = RestApi.fromRestApiAttributes(this, 'RestApi', { + restApiId: props.restApiId, + rootResourceId: props.rootResourceId, + }); + + const method = api.root.addResource('pets').addMethod('GET', new MockIntegration({ + integrationResponses: [{ + statusCode: '200', + }], + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, + }), { + methodResponses: [ { statusCode: '200' } ], + }); + + this.methods.push(method); + } +} + +class BooksStack extends NestedStack { + public readonly methods: Method[] = []; + + constructor(scope: Construct, props: ResourceNestedStackProps) { + super(scope, 'integ-restapi-import-BooksStack', props); + + const api = RestApi.fromRestApiAttributes(this, 'RestApi', { + restApiId: props.restApiId, + rootResourceId: props.rootResourceId, + }); + + const method = api.root.addResource('books').addMethod('GET', new MockIntegration({ + integrationResponses: [{ + statusCode: '200', + }], + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, + }), { + methodResponses: [ { statusCode: '200' } ], + }); + + this.methods.push(method); + } +} + +interface DeployStackProps extends NestedStackProps { + readonly restApiId: string; + + readonly methods?: Method[]; +} + +class DeployStack extends NestedStack { + constructor(scope: Construct, props: DeployStackProps) { + super(scope, 'integ-restapi-import-DeployStack', props); + + const deployment = new Deployment(this, 'Deployment', { + api: RestApi.fromRestApiId(this, 'RestApi', props.restApiId), + }); + (props.methods ?? []).forEach((method) => deployment.node.addDependency(method)); + new Stage(this, 'Stage', { deployment }); + } +} + +new RootStack(new App()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts b/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts index ce5a5279228e3..89b6905fddb4e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.authorizer.ts @@ -1,12 +1,12 @@ import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { Authorizer, RestApi } from '../lib'; +import { Authorizer, IRestApi } from '../lib'; export = { 'isAuthorizer correctly detects an instance of type Authorizer'(test: Test) { class MyAuthorizer extends Authorizer { public readonly authorizerId = 'test-authorizer-id'; - public _attachToApi(_: RestApi): void { + public _attachToApi(_: IRestApi): void { // do nothing } } diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index e4383ecf768ac..bb0d28b976094 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -928,4 +928,38 @@ export = { test.done(); }, + + '"restApi" and "api" properties return the RestApi correctly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = new apigw.RestApi(stack, 'test-api'); + const method = api.root.addResource('pets').addMethod('GET'); + + // THEN + test.ok(method.restApi); + test.ok(method.api); + test.deepEqual(stack.resolve(method.api.restApiId), stack.resolve(method.restApi.restApiId)); + + test.done(); + }, + + '"restApi" throws an error on imported while "api" returns correctly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const api = apigw.RestApi.fromRestApiAttributes(stack, 'test-api', { + restApiId: 'test-rest-api-id', + rootResourceId: 'test-root-resource-id', + }); + const method = api.root.addResource('pets').addMethod('GET'); + + // THEN + test.throws(() => method.restApi, /not available on Resource not connected to an instance of RestApi/); + test.ok(method.api); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index d512b924cfe98..4df5bd3fd2755 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -335,18 +335,6 @@ export = { test.done(); }, - 'fromRestApiId'(test: Test) { - // GIVEN - const stack = new Stack(); - - // WHEN - const imported = apigw.RestApi.fromRestApiId(stack, 'imported-api', 'api-rxt4498f'); - - // THEN - test.deepEqual(stack.resolve(imported.restApiId), 'api-rxt4498f'); - test.done(); - }, - '"url" and "urlForPath" return the URL endpoints of the deployed API'(test: Test) { // GIVEN const stack = new Stack(); @@ -933,4 +921,102 @@ export = { test.done(); }, + + '"restApi" and "api" properties return the RestApi correctly'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = new apigw.RestApi(stack, 'test-api'); + const method = api.root.addResource('pets').addMethod('GET'); + + // THEN + test.ok(method.restApi); + test.ok(method.api); + test.deepEqual(stack.resolve(method.api.restApiId), stack.resolve(method.restApi.restApiId)); + + test.done(); + }, + + '"restApi" throws an error on imported while "api" returns correctly'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const api = apigw.RestApi.fromRestApiAttributes(stack, 'test-api', { + restApiId: 'test-rest-api-id', + rootResourceId: 'test-root-resource-id', + }); + const method = api.root.addResource('pets').addMethod('GET'); + + // THEN + test.throws(() => method.restApi, /not available on Resource not connected to an instance of RestApi/); + test.ok(method.api); + + test.done(); + }, + + 'Import': { + 'fromRestApiId()'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const imported = apigw.RestApi.fromRestApiId(stack, 'imported-api', 'api-rxt4498f'); + + // THEN + test.deepEqual(stack.resolve(imported.restApiId), 'api-rxt4498f'); + test.done(); + }, + + 'fromRestApiAttributes()'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const imported = apigw.RestApi.fromRestApiAttributes(stack, 'imported-api', { + restApiId: 'test-restapi-id', + rootResourceId: 'test-root-resource-id', + }); + const resource = imported.root.addResource('pets'); + resource.addMethod('GET'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + PathPart: 'pets', + ParentId: stack.resolve(imported.restApiRootResourceId), + })); + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + ResourceId: stack.resolve(resource.resourceId), + })); + + test.done(); + }, + }, + + 'SpecRestApi': { + 'add Methods and Resources'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.SpecRestApi(stack, 'SpecRestApi', { + apiDefinition: apigw.ApiDefinition.fromInline({ foo: 'bar' }), + }); + + // WHEN + const resource = api.root.addResource('pets'); + resource.addMethod('GET'); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::Resource', { + PathPart: 'pets', + ParentId: stack.resolve(api.restApiRootResourceId), + })); + expect(stack).to(haveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + ResourceId: stack.resolve(resource.resourceId), + })); + test.done(); + }, + }, }; diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 5f4b909ff44b4..2799c6d520683 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -66,7 +66,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index bcb0fb3eff572..0c0b27c8e6f92 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -54,6 +54,7 @@ type Mutation { saveCustomer(id: String!, customer: SaveCustomerInput!): Customer removeCustomer(id: String!): Customer saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order + doPostOnAws: String! } ``` @@ -157,6 +158,40 @@ export class ApiStack extends Stack { requestMappingTemplate: MappingTemplate.dynamoDbDeleteItem('id', 'id'), responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), }); + + const httpDS = api.addHttpDataSource('http', 'The http data source', 'https://aws.amazon.com/'); + + httpDS.createResolver({ + typeName: 'Mutation', + fieldName: 'doPostOnAws', + requestMappingTemplate: MappingTemplate.fromString(`{ + "version": "2018-05-29", + "method": "POST", + # if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts + "resourcePath": "/path/123", + "params":{ + "body": $util.toJson($ctx.args), + "headers":{ + "Content-Type": "application/json", + "Authorization": "$ctx.request.headers.Authorization" + } + } + }`), + responseMappingTemplate: MappingTemplate.fromString(` + ## Raise a GraphQL field error in case of a datasource invocation error + #if($ctx.error) + $util.error($ctx.error.message, $ctx.error.type) + #end + ## if the response status code is not 200, then return an error. Else return the body ** + #if($ctx.result.statusCode == 200) + ## If response is 200, return the body. + $ctx.result.body + #else + ## If response is not 200, append the response to error block. + $utils.appendError($ctx.result.body, "$ctx.result.statusCode") + #end + `), + }); } } -``` \ No newline at end of file +``` diff --git a/packages/@aws-cdk/aws-appsync/jest.config.js b/packages/@aws-cdk/aws-appsync/jest.config.js index cd664e1d069e5..d9634b8ea0c5d 100644 --- a/packages/@aws-cdk/aws-appsync/jest.config.js +++ b/packages/@aws-cdk/aws-appsync/jest.config.js @@ -1,2 +1,10 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); -module.exports = baseConfig; +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + branches: 1, + statements: 1, + } + } +}; diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 6487d401401b8..2d4f802f72048 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -408,6 +408,21 @@ export class GraphQLApi extends Construct { }); } + /** + * add a new http data source to this API + * @param name The name of the data source + * @param description The description of the data source + * @param endpoint The http endpoint + */ + public addHttpDataSource(name: string, description: string, endpoint: string): HttpDataSource { + return new HttpDataSource(this, `${name}DS`, { + api: this, + description, + endpoint, + name, + }); + } + /** * add a new Lambda data source to this API * @param name The name of the data source @@ -751,6 +766,30 @@ export class DynamoDbDataSource extends BackedDataSource { } } +/** + * Properties for an AppSync http datasource + */ +export interface HttpDataSourceProps extends BaseDataSourceProps { + /** + * The http endpoint + */ + readonly endpoint: string; +} + +/** + * An AppSync datasource backed by a http endpoint + */ +export class HttpDataSource extends BaseDataSource { + constructor(scope: Construct, id: string, props: HttpDataSourceProps) { + super(scope, id, props, { + httpConfig: { + endpoint: props.endpoint, + }, + type: 'HTTP', + }); + } +} + /** * Properties for an AppSync Lambda datasource */ @@ -986,7 +1025,7 @@ export class Assign { * Renders the assignment as a map element. */ public putInMap(map: string): string { - return `$util.qr($${map}.put("${this.attr}", "${this.arg}"))`; + return `$util.qr($${map}.put("${this.attr}", ${this.arg}))`; } } @@ -1057,8 +1096,8 @@ export class PrimaryKey { assignments.push(this.skey.renderAsAssignment()); } return `"key" : { - ${assignments.join(',')} - }`; + ${assignments.join(',')} + }`; } } @@ -1092,14 +1131,19 @@ export class AttributeValues { return new AttributeValuesStep(attr, this.container, this.assignments); } + /** + * Renders the variables required for `renderTemplate`. + */ + public renderVariables(): string { + return `#set($input = ${this.container}) + ${this.assignments.map(a => a.putInMap('input')).join('\n')}`; + } + /** * Renders the attribute value assingments to a VTL string. */ public renderTemplate(): string { - return ` - #set($input = ${this.container}) - ${this.assignments.map(a => a.putInMap('input')).join('\n')} - "attributeValues": $util.dynamodb.toMapValuesJson($input)`; + return '"attributeValues": $util.dynamodb.toMapValuesJson($input)'; } } @@ -1216,12 +1260,14 @@ export abstract class MappingTemplate { * @param values the assignment of Mutation values to the table attributes */ public static dynamoDbPutItem(key: PrimaryKey, values: AttributeValues): MappingTemplate { - return this.fromString(`{ - "version" : "2017-02-28", - "operation" : "PutItem", - ${key.renderTemplate()}, - ${values.renderTemplate()} - }`); + return this.fromString(` + ${values.renderVariables()} + { + "version": "2017-02-28", + "operation": "PutItem", + ${key.renderTemplate()}, + ${values.renderTemplate()} + }`); } /** diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index 9cc5bfa71f0e3..ed431e29e7367 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -67,6 +67,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^25.5.4", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts new file mode 100644 index 0000000000000..37e1ecc0ea57b --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-dynamodb.test.ts @@ -0,0 +1,73 @@ +import '@aws-cdk/assert/jest'; +import { MappingTemplate, PrimaryKey, Values } from '../lib'; + +function joined(str: string): string { + return str.replace(/\s+/g, ''); +} + +describe('DynamoDB Mapping Templates', () => { + test('PutItem projecting all', () => { + const template = MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition('id').is('id'), + Values.projecting(), + ); + + const rendered = joined(template.renderTemplate()); + + expect(rendered).toStrictEqual(joined(` + #set($input = $ctx.args) + { + "version" : "2017-02-28", + "operation" : "PutItem", + "key" : { + "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) + }, + "attributeValues": $util.dynamodb.toMapValuesJson($input) + }`), + ); + }); + + test('PutItem with invididual attributes', () => { + const template = MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition('id').is('id'), + Values.attribute('val').is('ctx.args.val'), + ); + + const rendered = joined(template.renderTemplate()); + + expect(rendered).toStrictEqual(joined(` + #set($input = {}) + $util.qr($input.put("val", ctx.args.val)) + { + "version" : "2017-02-28", + "operation" : "PutItem", + "key" : { + "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) + }, + "attributeValues": $util.dynamodb.toMapValuesJson($input) + }`), + ); + }); + + test('PutItem with additional attributes', () => { + const template = MappingTemplate.dynamoDbPutItem( + PrimaryKey.partition('id').is('id'), + Values.projecting().attribute('val').is('ctx.args.val'), + ); + + const rendered = joined(template.renderTemplate()); + + expect(rendered).toStrictEqual(joined(` + #set($input = $ctx.args) + $util.qr($input.put("val", ctx.args.val)) + { + "version" : "2017-02-28", + "operation" : "PutItem", + "key" : { + "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) + }, + "attributeValues": $util.dynamodb.toMapValuesJson($input) + }`), + ); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json index 07215fce52330..e1d4a0ba17a4b 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json @@ -100,7 +100,7 @@ "ApiId": { "Fn::GetAtt": ["ApiF70053CD", "ApiId"] }, - "Definition": "type ServiceVersion {\n version: String!\n}\n\ntype Customer {\n id: String!\n name: String!\n}\n\ninput SaveCustomerInput {\n name: String!\n}\n\ntype Order {\n customer: String!\n order: String!\n}\n\ntype Query {\n getServiceVersion: ServiceVersion\n getCustomers: [Customer]\n getCustomer(id: String): Customer\n getCustomerOrdersEq(customer: String): Order\n getCustomerOrdersLt(customer: String): Order\n getCustomerOrdersLe(customer: String): Order\n getCustomerOrdersGt(customer: String): Order\n getCustomerOrdersGe(customer: String): Order\n getCustomerOrdersFilter(customer: String, order: String): Order\n getCustomerOrdersBetween(customer: String, order1: String, order2: String): Order\n}\n\ninput FirstOrderInput {\n product: String!\n quantity: Int!\n}\n\ntype Mutation {\n addCustomer(customer: SaveCustomerInput!): Customer\n saveCustomer(id: String!, customer: SaveCustomerInput!): Customer\n removeCustomer(id: String!): Customer\n saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order\n}" + "Definition": "type ServiceVersion {\n version: String!\n}\n\ntype Customer {\n id: String!\n name: String!\n}\n\ninput SaveCustomerInput {\n name: String!\n}\n\ntype Order {\n customer: String!\n order: String!\n}\n\ntype Query {\n getServiceVersion: ServiceVersion\n getCustomers: [Customer]\n getCustomer(id: String): Customer\n getCustomerOrdersEq(customer: String): Order\n getCustomerOrdersLt(customer: String): Order\n getCustomerOrdersLe(customer: String): Order\n getCustomerOrdersGt(customer: String): Order\n getCustomerOrdersGe(customer: String): Order\n getCustomerOrdersFilter(customer: String, order: String): Order\n getCustomerOrdersBetween(customer: String, order1: String, order2: String): Order\n}\n\ninput FirstOrderInput {\n product: String!\n quantity: Int!\n}\n\ntype Mutation {\n addCustomer(customer: SaveCustomerInput!): Customer\n saveCustomer(id: String!, customer: SaveCustomerInput!): Customer\n removeCustomer(id: String!): Customer\n saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order\n doPostOnAws: String!\n}\n" } }, "ApiNoneDSB4E6495F": { @@ -280,7 +280,7 @@ "TypeName": "Mutation", "DataSourceName": "Customer", "Kind": "UNIT", - "RequestMappingTemplate": "{\n \"version\" : \"2017-02-28\",\n \"operation\" : \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \n #set($input = $ctx.args.customer)\n \n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", + "RequestMappingTemplate": "\n #set($input = $ctx.args.customer)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ @@ -301,7 +301,7 @@ "TypeName": "Mutation", "DataSourceName": "Customer", "Kind": "UNIT", - "RequestMappingTemplate": "{\n \"version\" : \"2017-02-28\",\n \"operation\" : \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($ctx.args.id)\n },\n \n #set($input = $ctx.args.customer)\n \n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", + "RequestMappingTemplate": "\n #set($input = $ctx.args.customer)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($ctx.args.id)\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ @@ -322,7 +322,7 @@ "TypeName": "Mutation", "DataSourceName": "Customer", "Kind": "UNIT", - "RequestMappingTemplate": "{\n \"version\" : \"2017-02-28\",\n \"operation\" : \"PutItem\",\n \"key\" : {\n \"order\" : $util.dynamodb.toDynamoDBJson($util.autoId()),\"customer\" : $util.dynamodb.toDynamoDBJson($ctx.args.customer.id)\n },\n \n #set($input = $ctx.args.order)\n $util.qr($input.put(\"referral\", \"referral\"))\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", + "RequestMappingTemplate": "\n #set($input = $ctx.args.order)\n $util.qr($input.put(\"referral\", referral))\n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"order\" : $util.dynamodb.toDynamoDBJson($util.autoId()),\"customer\" : $util.dynamodb.toDynamoDBJson($ctx.args.customer.id)\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }", "ResponseMappingTemplate": "$util.toJson($ctx.result)" }, "DependsOn": [ @@ -585,6 +585,67 @@ "ApiSchema510EECD7" ] }, + "ApihttpDSServiceRole8B5C9457": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ApihttpDS91F12990": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "Name": "http", + "Type": "HTTP", + "Description": "The http data source", + "HttpConfig": { + "Endpoint": "https://aws.amazon.com/" + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "ApihttpDSServiceRole8B5C9457", + "Arn" + ] + } + } + }, + "ApihttpDSMutationdoPostOnAwsResolverA9027953": { + "Type": "AWS::AppSync::Resolver", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "FieldName": "doPostOnAws", + "TypeName": "Mutation", + "DataSourceName": "http", + "Kind": "UNIT", + "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n # if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts\n \"resourcePath\": \"/path/123\",\n \"params\":{\n \"body\": $util.toJson($ctx.args),\n \"headers\":{\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"$ctx.request.headers.Authorization\"\n }\n }\n }", + "ResponseMappingTemplate": "\n ## Raise a GraphQL field error in case of a datasource invocation error\n #if($ctx.error)\n $util.error($ctx.error.message, $ctx.error.type)\n #end\n ## if the response status code is not 200, then return an error. Else return the body **\n #if($ctx.result.statusCode == 200)\n ## If response is 200, return the body.\n $ctx.result.body\n #else\n ## If response is not 200, append the response to error block.\n $utils.appendError($ctx.result.body, \"$ctx.result.statusCode\")\n #end\n " + }, + "DependsOn": [ + "ApihttpDS91F12990", + "ApiSchema510EECD7" + ] + }, "CustomerTable260DCC08": { "Type": "AWS::DynamoDB::Table", "Properties": { @@ -634,4 +695,4 @@ "DeletionPolicy": "Delete" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts index 07cf7f028d41c..fb2be9eeac531 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts @@ -148,4 +148,38 @@ orderDS.createResolver({ responseMappingTemplate: MappingTemplate.dynamoDbResultList(), }); -app.synth(); \ No newline at end of file +const httpDS = api.addHttpDataSource('http', 'The http data source', 'https://aws.amazon.com/'); + +httpDS.createResolver({ + typeName: 'Mutation', + fieldName: 'doPostOnAws', + requestMappingTemplate: MappingTemplate.fromString(`{ + "version": "2018-05-29", + "method": "POST", + # if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts + "resourcePath": "/path/123", + "params":{ + "body": $util.toJson($ctx.args), + "headers":{ + "Content-Type": "application/json", + "Authorization": "$ctx.request.headers.Authorization" + } + } + }`), + responseMappingTemplate: MappingTemplate.fromString(` + ## Raise a GraphQL field error in case of a datasource invocation error + #if($ctx.error) + $util.error($ctx.error.message, $ctx.error.type) + #end + ## if the response status code is not 200, then return an error. Else return the body ** + #if($ctx.result.statusCode == 200) + ## If response is 200, return the body. + $ctx.result.body + #else + ## If response is not 200, append the response to error block. + $utils.appendError($ctx.result.body, "$ctx.result.statusCode") + #end + `), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-appsync/test/schema.graphql b/packages/@aws-cdk/aws-appsync/test/schema.graphql index 5f82e9279ccbc..24af9a154ec59 100644 --- a/packages/@aws-cdk/aws-appsync/test/schema.graphql +++ b/packages/@aws-cdk/aws-appsync/test/schema.graphql @@ -39,4 +39,5 @@ type Mutation { saveCustomer(id: String!, customer: SaveCustomerInput!): Customer removeCustomer(id: String!): Customer saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order -} \ No newline at end of file + doPostOnAws: String! +} diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index 4c31c377051ca..3cfba9f722db7 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -62,7 +62,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-autoscaling/README.md b/packages/@aws-cdk/aws-autoscaling/README.md index 260d8d0e0b693..ce55c411671f7 100644 --- a/packages/@aws-cdk/aws-autoscaling/README.md +++ b/packages/@aws-cdk/aws-autoscaling/README.md @@ -224,6 +224,11 @@ To enable the max instance lifetime support, specify `maxInstanceLifetime` prope for the `AutoscalingGroup` resource. The value must be between 7 and 365 days(inclusive). To clear a previously set value, just leave this property undefinied. +### Instance Monitoring + +To disable detailed instance monitoring, specify `instanceMonitoring` property +for the `AutoscalingGroup` resource as `Monitoring.BASIC`. Otherwise detailed monitoring +will be enabled. ### Future work diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 52ca56748b0f3..31483d53d40a6 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -21,6 +21,21 @@ import { BlockDevice, BlockDeviceVolume, EbsDeviceVolumeType } from './volume'; */ const NAME_TAG: string = 'Name'; +/** + * The monitoring mode for instances launched in an autoscaling group + */ +export enum Monitoring { + /** + * Generates metrics every 5 minutes + */ + BASIC, + + /** + * Generates metrics every minute + */ + DETAILED, +} + /** * Basic properties of an AutoScalingGroup, except the exact machines to run and where they should run * @@ -207,6 +222,18 @@ export interface CommonAutoScalingGroupProps { * @default none */ readonly maxInstanceLifetime?: Duration; + + /** + * Controls whether instances in this group are launched with detailed or basic monitoring. + * + * When detailed monitoring is enabled, Amazon CloudWatch generates metrics every minute and your account + * is charged a fee. When you disable detailed monitoring, CloudWatch generates metrics every 5 minutes. + * + * @see https://docs.aws.amazon.com/autoscaling/latest/userguide/as-instance-monitoring.html#enable-as-instance-metrics + * + * @default - Monitoring.DETAILED + */ + readonly instanceMonitoring?: Monitoring; } /** @@ -477,6 +504,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements imageId: imageConfig.imageId, keyName: props.keyName, instanceType: props.instanceType.toString(), + instanceMonitoring: (props.instanceMonitoring !== undefined ? (props.instanceMonitoring === Monitoring.DETAILED) : undefined), securityGroups: securityGroupsToken, iamInstanceProfile: iamProfile.ref, userData: userDataToken, diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index c17c90b17f06e..faea5fe7cd8f9 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -831,6 +831,45 @@ export = { test.done(); }, + 'can configure instance monitoring'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyStack', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + instanceMonitoring: autoscaling.Monitoring.BASIC, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + InstanceMonitoring: false, + })); + test.done(); + }, + + 'instance monitoring defaults to absent'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyStack', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + InstanceMonitoring: ABSENT, + })); + test.done(); + }, + 'throws if ephemeral volumeIndex < 0'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 450cd9fc52e8a..a49ff09fe6634 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -297,6 +297,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly STORAGE_GATEWAY = new InterfaceVpcEndpointAwsService('storagegateway'); public static readonly REKOGNITION = new InterfaceVpcEndpointAwsService('rekognition'); public static readonly REKOGNITION_FIPS = new InterfaceVpcEndpointAwsService('rekognition-fips'); + public static readonly STEP_FUNCTIONS = new InterfaceVpcEndpointAwsService('states'); /** * The name of the service. diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 7bed5e57ba6f5..458f2d1b4bc2e 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -231,6 +231,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.STORAGE_GATEWAY", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.REKOGNITION", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.REKOGNITION_FIPS", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.STEP_FUNCTIONS", "docs-public-apis:@aws-cdk/aws-ec2.Port.toString", "docs-public-apis:@aws-cdk/aws-ec2.PrivateSubnet.fromPrivateSubnetAttributes", "docs-public-apis:@aws-cdk/aws-ec2.PublicSubnet.fromPublicSubnetAttributes", diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 01b211d16e142..fb7283b2643d8 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -266,6 +266,9 @@ The `logRetention` property can be used to set a different expiration period. It is possible to obtain the function's log group as a `logs.ILogGroup` by calling the `logGroup` property of the `Function` construct. +By default, CDK uses the AWS SDK retry options when creating a log group. The `logRetentionRetryOptions` property +allows you to customize the maximum number of retries and base backoff duration. + *Note* that, if either `logRetention` is set or `logGroup` property is called, a [CloudFormation custom resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html) is added to the stack that pre-creates the log group as part of the stack deployment, if it already doesn't exist, and sets the diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index ea2d2bf1f18ef..d99d1b1ce8377 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -12,7 +12,7 @@ import { calculateFunctionHash, trimFromStart } from './function-hash'; import { Version, VersionOptions } from './lambda-version'; import { CfnFunction } from './lambda.generated'; import { ILayerVersion } from './layers'; -import { LogRetention } from './log-retention'; +import { LogRetention, LogRetentionRetryOptions } from './log-retention'; import { Runtime } from './runtime'; /** @@ -232,6 +232,14 @@ export interface FunctionOptions extends EventInvokeConfigOptions { */ readonly logRetentionRole?: iam.IRole; + /** + * When log retention is specified, a custom resource attempts to create the CloudWatch log group. + * These options control the retry policy when interacting with CloudWatch APIs. + * + * @default - Default AWS SDK retry options. + */ + readonly logRetentionRetryOptions?: LogRetentionRetryOptions; + /** * Options for the `lambda.Version` resource automatically created by the * `fn.currentVersion` method. @@ -544,6 +552,7 @@ export class Function extends FunctionBase { logGroupName: `/aws/lambda/${this.functionName}`, retention: props.logRetention, role: props.logRetentionRole, + logRetentionRetryOptions: props.logRetentionRetryOptions, }); this._logGroup = logs.LogGroup.fromLogGroupArn(this, 'LogGroup', logretention.logGroupArn); } diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts index 38ac1fb709d8b..16d44a5fd83de 100644 --- a/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts @@ -2,15 +2,23 @@ // eslint-disable-next-line import/no-extraneous-dependencies import * as AWS from 'aws-sdk'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { RetryDelayOptions } from 'aws-sdk/lib/config'; + +interface SdkRetryOptions { + maxRetries?: number; + retryOptions?: RetryDelayOptions; +} /** * Creates a log group and doesn't throw if it exists. * - * @param logGroupName the name of the log group to create + * @param logGroupName the name of the log group to create. + * @param options CloudWatch API SDK options. */ -async function createLogGroupSafe(logGroupName: string) { +async function createLogGroupSafe(logGroupName: string, options?: SdkRetryOptions) { try { // Try to create the log group - const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' }); + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', ...options }); await cloudwatchlogs.createLogGroup({ logGroupName }).promise(); } catch (e) { if (e.code !== 'ResourceAlreadyExistsException') { @@ -23,10 +31,11 @@ async function createLogGroupSafe(logGroupName: string) { * Puts or deletes a retention policy on a log group. * * @param logGroupName the name of the log group to create + * @param options CloudWatch API SDK options. * @param retentionInDays the number of days to retain the log events in the specified log group. */ -async function setRetentionPolicy(logGroupName: string, retentionInDays?: number) { - const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' }); +async function setRetentionPolicy(logGroupName: string, options?: SdkRetryOptions, retentionInDays?: number) { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', ...options }); if (!retentionInDays) { await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise(); } else { @@ -41,10 +50,13 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent // The target log group const logGroupName = event.ResourceProperties.LogGroupName; + // Parse to AWS SDK retry options + const retryOptions = parseRetryOptions(event.ResourceProperties.SdkRetry); + if (event.RequestType === 'Create' || event.RequestType === 'Update') { // Act on the target log group - await createLogGroupSafe(logGroupName); - await setRetentionPolicy(logGroupName, parseInt(event.ResourceProperties.RetentionInDays, 10)); + await createLogGroupSafe(logGroupName, retryOptions); + await setRetentionPolicy(logGroupName, retryOptions, parseInt(event.ResourceProperties.RetentionInDays, 10)); if (event.RequestType === 'Create') { // Set a retention policy of 1 day on the logs of this function. The log @@ -56,8 +68,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent // same time. This can sometime result in an OperationAbortedException. To // avoid this and because this operation is not critical we catch all errors. try { - await createLogGroupSafe(`/aws/lambda/${context.functionName}`); - await setRetentionPolicy(`/aws/lambda/${context.functionName}`, 1); + await createLogGroupSafe(`/aws/lambda/${context.functionName}`, retryOptions); + await setRetentionPolicy(`/aws/lambda/${context.functionName}`, retryOptions, 1); } catch (e) { console.log(e); } @@ -108,4 +120,19 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent } }); } + + function parseRetryOptions(rawOptions: any): SdkRetryOptions { + const retryOptions: SdkRetryOptions = {}; + if (rawOptions) { + if (rawOptions.maxRetries) { + retryOptions.maxRetries = parseInt(rawOptions.maxRetries, 10); + } + if (rawOptions.base) { + retryOptions.retryOptions = { + base: parseInt(rawOptions.base, 10), + }; + } + } + return retryOptions; + } } diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention.ts index 74feeb5e62794..6c5fec2da7cd9 100644 --- a/packages/@aws-cdk/aws-lambda/lib/log-retention.ts +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention.ts @@ -26,6 +26,31 @@ export interface LogRetentionProps { * @default - A new role is created */ readonly role?: iam.IRole; + + /** + * Retry options for all AWS API calls. + * + * @default - AWS SDK default retry options + */ + readonly logRetentionRetryOptions?: LogRetentionRetryOptions; +} + +/** + * Retry options for all AWS API calls. + */ +export interface LogRetentionRetryOptions { + /** + * The maximum amount of retries. + * + * @default 3 (AWS SDK default) + */ + readonly maxRetries?: number; + /** + * The base duration to use in the exponential backoff for operation retries. + * + * @default Duration.millis(100) (AWS SDK default) + */ + readonly base?: cdk.Duration; } /** @@ -64,11 +89,16 @@ export class LogRetention extends cdk.Construct { // Need to use a CfnResource here to prevent lerna dependency cycles // @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation + const retryOptions = props.logRetentionRetryOptions; const resource = new cdk.CfnResource(this, 'Resource', { type: 'Custom::LogRetention', properties: { ServiceToken: provider.functionArn, LogGroupName: props.logGroupName, + SdkRetry: retryOptions ? { + maxRetries: retryOptions.maxRetries, + base: retryOptions.base?.toMilliseconds(), + } : undefined, RetentionInDays: props.retention === logs.RetentionDays.INFINITE ? undefined : props.retention, }, }); diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json index 4ca25f4d0ba99..3e45c9ce6d65d 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json @@ -133,7 +133,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049" }, "S3Key": { "Fn::Join": [ @@ -146,7 +146,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1" } ] } @@ -159,7 +159,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1" } ] } @@ -331,17 +331,17 @@ } }, "Parameters": { - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049": { "Type": "String", - "Description": "S3 bucket for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "S3 bucket for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" }, - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1": { "Type": "String", - "Description": "S3 key for asset version \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "S3 key for asset version \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" }, - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eArtifactHashB967D42A": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3ArtifactHash31AA1F7C": { "Type": "String", - "Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "Artifact hash for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts b/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts index bfc37c0d1b6f2..007f2836bf44b 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts @@ -296,4 +296,41 @@ export = { test.done(); }, + + async 'custom log retention retry options'(test: Test) { + AWS.mock('CloudWatchLogs', 'createLogGroup', sinon.fake.resolves({})); + AWS.mock('CloudWatchLogs', 'putRetentionPolicy', sinon.fake.resolves({})); + AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', sinon.fake.resolves({})); + + const event = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + RetentionInDays: '30', + LogGroupName: 'group', + SdkRetry: { + maxRetries: '5', + base: '300', + }, + }, + }; + + const request = createRequest('SUCCESS'); + + await provider.handler(event as AWSLambda.CloudFormationCustomResourceCreateEvent, context); + + sinon.assert.calledWith(AWSSDK.CloudWatchLogs as any, { + apiVersion: '2014-03-28', + maxRetries: 5, + retryOptions: { + base: 300, + }, + }); + + test.equal(request.isDone(), true); + + test.done(); + }, + }; diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 070ac5ca1698c..5ce914f99f47a 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -47,7 +47,7 @@ your instances will be launched privately or publicly: ```ts const instance = new DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.ORACLE_SE1, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', vpc }); @@ -62,7 +62,7 @@ Example for max storage configuration: ```ts const instance = new DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.ORACLE_SE1, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', vpc, maxAllocatedStorage: 200 @@ -76,14 +76,13 @@ a source database respectively: new DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.POSTGRES, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc }); new DatabaseInstanceReadReplica(stack, 'ReadReplica', { sourceDatabaseInstance: sourceInstance, - engine: rds.DatabaseInstanceEngine.POSTGRES, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc }); ``` diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 5ed0925bf5d7d..7af0ebe13a58c 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -313,7 +313,7 @@ export interface DatabaseInstanceNewProps { /** * The name of the compute and memory capacity classes. */ - readonly instanceClass: ec2.InstanceType; + readonly instanceType: ec2.InstanceType; /** * Specifies if the database instance is a multiple Availability Zone deployment. @@ -610,7 +610,7 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData availabilityZone: props.multiAz ? undefined : props.availabilityZone, backupRetentionPeriod: props.backupRetention ? props.backupRetention.toDays() : undefined, copyTagsToSnapshot: props.copyTagsToSnapshot !== undefined ? props.copyTagsToSnapshot : true, - dbInstanceClass: `db.${props.instanceClass}`, + dbInstanceClass: `db.${props.instanceType}`, dbInstanceIdentifier: props.instanceIdentifier, dbSubnetGroupName: subnetGroup.ref, deleteAutomatedBackups: props.deleteAutomatedBackups, @@ -995,7 +995,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme /** * Construction properties for a DatabaseInstanceReadReplica. */ -export interface DatabaseInstanceReadReplicaProps extends DatabaseInstanceSourceProps { +export interface DatabaseInstanceReadReplicaProps extends DatabaseInstanceNewProps { /** * The source database instance. * diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index d5c7708151b53..01687488c079e 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -967,7 +967,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049" }, "S3Key": { "Fn::Join": [ @@ -980,7 +980,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1" } ] } @@ -993,7 +993,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583" + "Ref": "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1" } ] } @@ -1108,17 +1108,17 @@ } }, "Parameters": { - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3Bucket7046E6CE": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3Bucket21D86049": { "Type": "String", - "Description": "S3 bucket for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "S3 bucket for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" }, - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eS3VersionKey3194A583": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3S3VersionKey1F67C4C1": { "Type": "String", - "Description": "S3 key for asset version \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "S3 key for asset version \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" }, - "AssetParameters82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4eArtifactHashB967D42A": { + "AssetParameters3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3ArtifactHash31AA1F7C": { "Type": "String", - "Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" + "Description": "Artifact hash for asset \"3974ceb096f16a0d6c372c0c821ca2ab0333112497b2d3bc462ccaf2fc6037c3\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts index f386c04b0a1d6..d37fd89f9b935 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts @@ -46,7 +46,7 @@ class DatabaseInstanceStack extends cdk.Stack { const instance = new rds.DatabaseInstance(this, 'Instance', { engine: rds.DatabaseInstanceEngine.ORACLE_SE1, licenseModel: rds.LicenseModel.BRING_YOUR_OWN_LICENSE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM), multiAz: true, storageType: rds.StorageType.IO1, masterUsername: 'syscdk', diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index baefed5b6b157..c22f926c95fbe 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -18,7 +18,7 @@ export = { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.ORACLE_SE1, licenseModel: rds.LicenseModel.BRING_YOUR_OWN_LICENSE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MEDIUM), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MEDIUM), multiAz: true, storageType: rds.StorageType.IO1, masterUsername: 'syscdk', @@ -215,7 +215,7 @@ export = { // WHEN new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', masterUserPassword: cdk.SecretValue.plainText('tooshort'), vpc, @@ -244,7 +244,7 @@ export = { new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.POSTGRES, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, }); @@ -264,7 +264,7 @@ export = { test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, generateMasterUserPassword: true, }), '`masterUsername` must be specified when `generateMasterUserPassword` is set to true.'); @@ -281,7 +281,7 @@ export = { test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, masterUsername: 'superadmin', }), 'Cannot specify `masterUsername` when `generateMasterUserPassword` is set to false.'); @@ -298,7 +298,7 @@ export = { test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, masterUserPassword: cdk.SecretValue.plainText('supersecret'), generateMasterUserPassword: true, @@ -313,7 +313,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const sourceInstance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -321,8 +321,7 @@ export = { // WHEN new rds.DatabaseInstanceReadReplica(stack, 'ReadReplica', { sourceDatabaseInstance: sourceInstance, - engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, }); @@ -354,7 +353,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -421,7 +420,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -474,7 +473,7 @@ export = { // WHEN const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -499,7 +498,7 @@ export = { // WHEN const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, }); @@ -530,7 +529,7 @@ export = { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, backupRetention: cdk.Duration.seconds(0), @@ -583,7 +582,7 @@ export = { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, monitoringInterval: cdk.Duration.minutes(1), @@ -612,7 +611,7 @@ export = { // WHEN const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, securityGroups: [securityGroup], @@ -649,7 +648,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const instance = new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', masterUserPassword: cdk.SecretValue.plainText('tooshort'), vpc, @@ -667,7 +666,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const instance = new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'syscdk', vpc, }); @@ -694,7 +693,7 @@ export = { tzSupportedEngines.forEach((engine) => { test.ok(new rds.DatabaseInstance(stack, `${engine.name}-db`, { engine, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), masterUsername: 'master', timezone: 'Europe/Zurich', vpc, @@ -704,7 +703,7 @@ export = { tzUnsupportedEngines.forEach((engine) => { test.throws(() => new rds.DatabaseInstance(stack, `${engine.name}-db`, { engine, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), masterUsername: 'master', timezone: 'Europe/Zurich', vpc, @@ -723,7 +722,7 @@ export = { new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.POSTGRES, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.LARGE), vpc, maxAllocatedStorage: 200, }); @@ -744,7 +743,7 @@ export = { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, - instanceClass: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), masterUsername: 'admin', vpc, backupRetention: cdk.Duration.seconds(0), diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh b/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh index ddcbf4807ab0d..cc9df52d130df 100755 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh @@ -27,7 +27,7 @@ cd ${staging} # install python requirements # Must use --prefix to because --target cannot be used on # platforms that have a default --prefix set. -pip3 install --ignore-installed --prefix ${piptemp} -r ${staging}/requirements.txt +pip3 install --ignore-installed --prefix ${piptemp} --no-user -r ${staging}/requirements.txt mv ${piptemp}/lib/python*/*-packages/* . [ -d ${piptemp}/lib64 ] && mv ${piptemp}/lib64/python*/*-packages/* . rm -fr ./awscli/examples diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh b/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh index a11706a3cee31..e87f8dfc2492b 100755 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh @@ -4,6 +4,7 @@ # # prepares a staging directory with the requirements set -e +set -x scriptdir=$(cd $(dirname $0) && pwd) # prepare staging directory @@ -16,7 +17,7 @@ cp -f ${scriptdir}/src/* $PWD cp -f ${scriptdir}/test/* $PWD # install deps -pip3 install -r requirements.txt -t . +pip3 install --no-user -r requirements.txt -t . # run our tests exec python3 test.py $@ diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 88342e0ba3835..f7c1e6015cfdc 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -33,7 +33,7 @@ "@types/string-width": "^4.0.1", "@types/table": "^4.0.7", "cdk-build-tools": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "jest": "^25.5.4", "pkglint": "0.0.0", "ts-jest": "^26.1.0" diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 8680356027cc2..519844689177c 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -1,5 +1,6 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import { AssetHashType, AssetOptions } from './assets'; import { BundlingOptions } from './bundling'; @@ -150,6 +151,16 @@ export class AssetStaging extends Construct { // Create temporary directory for bundling const bundleDir = FileSystem.mkdtemp('cdk-asset-bundle-'); + let user: string; + if (options.user) { + user = options.user; + } else { // Default to current user + const userInfo = os.userInfo(); + user = userInfo.uid !== -1 // uid is -1 on Windows + ? `${userInfo.uid}:${userInfo.gid}` + : '1000:1000'; + } + // Always mount input and output dir const volumes = [ { @@ -166,6 +177,7 @@ export class AssetStaging extends Construct { try { options.image._run({ command: options.command, + user, volumes, environment: options.environment, workingDirectory: options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR, diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index 269a0a5fc172c..1034517534f10 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -42,6 +42,17 @@ export interface BundlingOptions { * @default /asset-input */ readonly workingDirectory?: string; + + /** + * The user to use when running the container. + * + * user | user:group | uid | uid:gid | user:gid | uid:group + * + * @see https://docs.docker.com/engine/reference/run/#user + * + * @default - uid:gid of the current user or 1000:1000 on Windows + */ + readonly user?: string; } /** @@ -98,6 +109,9 @@ export class BundlingDockerImage { const dockerArgs: string[] = [ 'run', '--rm', + ...options.user + ? ['-u', options.user] + : [], ...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}`])), ...flatten(Object.entries(environment).map(([k, v]) => ['--env', `${k}=${v}`])), ...options.workingDirectory @@ -157,6 +171,13 @@ interface DockerRunOptions { * @default - image default */ readonly workingDirectory?: string; + + /** + * The user to use when running the container. + * + * @default - root or image default + */ + readonly user?: string; } /** diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index c1066fd4799a6..6d9d38199e6a1 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -158,7 +158,7 @@ "@types/sinon": "^9.0.4", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^1.24.2", + "fast-check": "^1.25.0", "lodash": "^4.17.15", "nodeunit": "^0.11.3", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/core/test/test.bundling.ts b/packages/@aws-cdk/core/test/test.bundling.ts index 658aa99901bb6..2ba23a83ffce9 100644 --- a/packages/@aws-cdk/core/test/test.bundling.ts +++ b/packages/@aws-cdk/core/test/test.bundling.ts @@ -28,10 +28,12 @@ export = { }, volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], workingDirectory: '/working-directory', + user: 'user:group', }); test.ok(spawnSyncStub.calledWith('docker', [ 'run', '--rm', + '-u', 'user:group', '-v', '/host-path:/container-path', '--env', 'VAR1=value1', '--env', 'VAR2=value2', diff --git a/packages/@aws-cdk/core/test/test.staging.ts b/packages/@aws-cdk/core/test/test.staging.ts index 5c6b48629c4b1..9bd123a50da0d 100644 --- a/packages/@aws-cdk/core/test/test.staging.ts +++ b/packages/@aws-cdk/core/test/test.staging.ts @@ -1,6 +1,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import { Test } from 'nodeunit'; +import * as os from 'os'; import * as path from 'path'; import { App, AssetHashType, AssetStaging, BundlingDockerImage, Stack } from '../lib'; @@ -12,6 +13,9 @@ enum DockerStubCommand { SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT' } +const userInfo = os.userInfo(); +const USER_ARG = `-u ${userInfo.uid}:${userInfo.gid}`; + // this is a way to provide a custom "docker" command for staging. process.env.CDK_DOCKER = `${__dirname}/docker-stub.sh`; @@ -111,7 +115,10 @@ export = { // THEN const assembly = app.synth(); - test.deepEqual(readDockerStubInput(), 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS'); + test.deepEqual( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); test.deepEqual(fs.readdirSync(assembly.directory), [ 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00', 'cdk.out', @@ -138,8 +145,10 @@ export = { }, }), /Bundling did not produce any output/); - test.equal(readDockerStubInput(), - 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS_NO_OUTPUT'); + test.equal( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS_NO_OUTPUT`, + ); test.done(); }, @@ -160,7 +169,10 @@ export = { }); // THEN - test.equal(readDockerStubInput(), 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS'); + test.equal( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); test.done(); @@ -201,7 +213,10 @@ export = { assetHash: 'my-custom-hash', assetHashType: AssetHashType.BUNDLE, }), /Cannot specify `bundle` for `assetHashType`/); - test.equal(readDockerStubInput(), 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS'); + test.equal( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input alpine DOCKER_STUB_SUCCESS`, + ); test.done(); }, @@ -252,7 +267,10 @@ export = { command: [ DockerStubCommand.FAIL ], }, }), /Failed to run bundling Docker image for asset stack\/Asset/); - test.equal(readDockerStubInput(), 'run --rm -v /input:/asset-input -v /output:/asset-output -w /asset-input this-is-an-invalid-docker-image DOCKER_STUB_FAIL'); + test.equal( + readDockerStubInput(), + `run --rm ${USER_ARG} -v /input:/asset-input -v /output:/asset-output -w /asset-input this-is-an-invalid-docker-image DOCKER_STUB_FAIL`, + ); test.done(); }, diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index d9a1f74eb18e8..8e7df3d741e57 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -35,7 +35,7 @@ "typescript": "~3.8.3" }, "devDependencies": { - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/jest": "^25.2.3", "@types/node": "^10.17.25", "cdk-build-tools": "0.0.0", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 7a056f1b1ba02..f679127cf4612 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -43,7 +43,7 @@ "@aws-cdk/core": "0.0.0", "@types/archiver": "^3.1.0", "@types/fs-extra": "^8.1.0", - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/jest": "^25.2.3", "@types/minimatch": "^3.0.3", "@types/mockery": "^1.4.29", diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 91c272bb380c8..b2fbabf9b77ab 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -31,7 +31,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/archiver": "^3.1.0", - "@types/glob": "^7.1.1", + "@types/glob": "^7.1.2", "@types/jest": "^25.2.3", "@types/mock-fs": "^4.10.0", "@types/node": "^10.17.25", diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 7a7601093e329..71096964a4b7a 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^3.1.0", + "@typescript-eslint/eslint-plugin": "^3.2.0", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 63d2c1379acd2..af3aac24745c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1427,11 +1427,6 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== -"@types/events@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - "@types/fs-extra@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.0.tgz#1114834b53c3914806cd03b3304b37b3bd221a4d" @@ -1446,12 +1441,11 @@ dependencies: "@types/node" "*" -"@types/glob@*", "@types/glob@^7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" - integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== +"@types/glob@*", "@types/glob@^7.1.1", "@types/glob@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987" + integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA== dependencies: - "@types/events" "*" "@types/minimatch" "*" "@types/node" "*" @@ -1648,12 +1642,12 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.3.tgz#38fb31d82ed07dea87df6bd565721d11979fd761" integrity sha512-mhdQq10tYpiNncMkg1vovCud5jQm+rWeRVz6fxjCJlY6uhDlAn9GnMSmBa2DQwqPf/jS5YR0K/xChDEh1jdOQg== -"@typescript-eslint/eslint-plugin@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz#4ac00ecca3bbea740c577f1843bc54fa69c3def2" - integrity sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg== +"@typescript-eslint/eslint-plugin@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.2.0.tgz#7fb997f391af32ae6ca1dbe56bcefe4dd30bda14" + integrity sha512-t9RTk/GyYilIXt6BmZurhBzuMT9kLKw3fQoJtK9ayv0tXTlznXEAnx07sCLXdkN3/tZDep1s1CEV95CWuARYWA== dependencies: - "@typescript-eslint/experimental-utils" "3.1.0" + "@typescript-eslint/experimental-utils" "3.2.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" @@ -1669,13 +1663,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz#2d5dba7c2ac2a3da3bfa3f461ff64de38587a872" - integrity sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w== +"@typescript-eslint/experimental-utils@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.2.0.tgz#4dab8fc9f44f059ec073470a81bb4d7d7d51e6c5" + integrity sha512-UbJBsk+xO9dIFKtj16+m42EvUvsjZbbgQ2O5xSTSfVT1Z3yGkL90DVu0Hd3029FZ5/uBgl+F3Vo8FAcEcqc6aQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "3.1.0" + "@typescript-eslint/typescript-estree" "3.2.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1702,10 +1696,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz#eaff52d31e615e05b894f8b9d2c3d8af152a5dd2" - integrity sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ== +"@typescript-eslint/typescript-estree@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.2.0.tgz#c735f1ca6b4d3cd671f30de8c9bde30843e7ead8" + integrity sha512-uh+Y2QO7dxNrdLw7mVnjUqkwO/InxEqwN0wF+Za6eo3coxls9aH9kQ/5rSvW2GcNanebRTmsT5w1/92lAOb1bA== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -4123,13 +4117,13 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-check@^1.24.2: - version "1.24.2" - resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-1.24.2.tgz#1f5e4a3c20530c3a85a861e60f680c32229d4fcb" - integrity sha512-ZL48cyZZLJnVsUj127Zi1mfFLM98yzw0LlSSH8CMeVmpL5RCfSRcZSZZ0kJWrRK4eOgNFnXXKNDbzuRb3Vsdhg== +fast-check@^1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-1.25.0.tgz#ae22f452e2d53577c32c7903d5b4e1eb3aacf7d5" + integrity sha512-HhiFqNBfty56YNuPLBlEKBmyCmUtAAV8d9qr/n3DJT1mi8dM34uK542tnO5HAgrhVjICLQuH3jrD6kULw+uCJQ== dependencies: pure-rand "^2.0.0" - tslib "^1.10.0" + tslib "^2.0.0" fast-deep-equal@^2.0.1: version "2.0.1" @@ -9480,11 +9474,16 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== +tslib@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3" + integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g== + tslint@^5.20.1: version "5.20.1" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d"