diff --git a/packages/@aws-cdk/assert/jest.ts b/packages/@aws-cdk/assert/jest.ts index 8523318a11376..5c6db5727ed8d 100644 --- a/packages/@aws-cdk/assert/jest.ts +++ b/packages/@aws-cdk/assert/jest.ts @@ -1,5 +1,6 @@ import * as core from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; +import { countResources } from './lib'; import { JestFriendlyAssertion } from './lib/assertion'; import { haveOutput, HaveOutputProperties } from './lib/assertions/have-output'; import { HaveResourceAssertion, ResourcePart } from './lib/assertions/have-resource'; @@ -25,6 +26,8 @@ declare global { comparison?: ResourcePart): R; toHaveOutput(props: HaveOutputProperties): R; + + toCountResources(resourceType: string, count: number): R; } } } @@ -77,6 +80,14 @@ expect.extend({ return applyAssertion(haveOutput(props), actual); }, + + toCountResources( + actual: cxapi.CloudFormationStackArtifact | core.Stack, + resourceType: string, + count = 1) { + + return applyAssertion(countResources(resourceType, count), actual); + }, }); function applyAssertion(assertion: JestFriendlyAssertion, actual: cxapi.CloudFormationStackArtifact | core.Stack) { diff --git a/packages/@aws-cdk/assert/lib/assertions/count-resources.ts b/packages/@aws-cdk/assert/lib/assertions/count-resources.ts index ba9405fd45820..0827ba1f18306 100644 --- a/packages/@aws-cdk/assert/lib/assertions/count-resources.ts +++ b/packages/@aws-cdk/assert/lib/assertions/count-resources.ts @@ -1,11 +1,11 @@ -import { Assertion } from '../assertion'; +import { Assertion, JestFriendlyAssertion } from '../assertion'; import { StackInspector } from '../inspector'; import { isSuperObject } from './have-resource'; /** * An assertion to check whether a resource of a given type and with the given properties exists, disregarding properties */ -export function countResources(resourceType: string, count = 1): Assertion { +export function countResources(resourceType: string, count = 1): JestFriendlyAssertion { return new CountResourcesAssertion(resourceType, count); } @@ -16,7 +16,7 @@ export function countResourcesLike(resourceType: string, count = 1, props: any): return new CountResourcesAssertion(resourceType, count, props); } -class CountResourcesAssertion extends Assertion { +class CountResourcesAssertion extends JestFriendlyAssertion { private inspected: number = 0; private readonly props: any; @@ -48,6 +48,10 @@ class CountResourcesAssertion extends Assertion { return counted === this.count; } + public generateErrorMessage(): string { + return this.description; + } + public get description(): string { return `stack only has ${this.inspected} resource of type ${this.resourceType}${this.props ? ' with specified properties' : ''} but we expected to find ${this.count}`; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/.npmignore b/packages/@aws-cdk/aws-apigatewayv2/.npmignore index 683e3e0847e1f..4bed40b6573da 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/.npmignore +++ b/packages/@aws-cdk/aws-apigatewayv2/.npmignore @@ -4,6 +4,7 @@ *.snk !*.d.ts !*.js +**/cdk.out # Coverage coverage diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index a885fa9979695..cbf0958608abd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -21,6 +21,7 @@ - [Defining HTTP APIs](#defining-http-apis) - [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) - [Publishing HTTP APIs](#publishing-http-apis) + - [Custom Domain](#custom-domain) ## Introduction @@ -134,3 +135,68 @@ If you omit the `stageName` will create a `$default` stage. A `$default` stage i the API's URL - `https://{api_id}.execute-api.{region}.amazonaws.com/`. Note that, `HttpApi` will always creates a `$default` stage, unless the `createDefaultStage` property is unset. + + + +### Custom Domain + +Custom domain names are simpler and more intuitive URLs that you can provide to your API users. Custom domain name are associated to API stages. + +The code snippet below creates a custom domain and configures a default domain mapping for your API that maps the +custom domain to the `$default` stage of the API. + +```ts +const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; +const domainName = 'example.com'; + +const dn = new DomainName(stack, 'DN', { + domainName, + certificate: acm.Certificate.fromCertificateArn(stack, 'cert', certArn), +}); + +const api = new HttpApi(stack, 'HttpProxyProdApi', { + defaultIntegration: new LambdaProxyIntegration({ handler }), + // https://${dn.domainName} goes to prodApi $default stage + defaultDomainMapping: { + domainName: dn, + mappingKey: '/', + }, +}); +``` + +To associate a specifc `Stage` to a custom domain mapping - + +```ts +api.addStage('beta', { + stageName: 'beta', + autoDeploy: true, + // https://${dn.domainName}/beta goes to the beta stage + domainMapping: { + domainName: dn, + mappingKey: 'beta', + }, +}); +``` + +The same domain name can be associated with stages across different `HttpApi` as so - + +```ts +const apiDemo = new HttpApi(stack, 'DemoApi', { + defaultIntegration: new LambdaProxyIntegration({ handler }), + // https://${dn.domainName}/demo goes to apiDemo $default stage + defaultDomainMapping: { + domainName: dn, + mappingKey: 'demo', + }, +}); +``` + +The `mappingKey` determines the `path` of the URL with the custom domain. Each custom domain is only allowed +to have one API mapping with the root(/) `mappingKey`. In the sample above, the custom domain is associated +with 3 API mapping resources across different APIs and Stages. + +| API | Stage | URL | +| :------------: | :---------: | :----: | +| api | $default | `https://${domainName}` | +| api | beta | `https://${domainName}/beta` | +| apiDemo | $default | `https://${domainName}/demo` | diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts new file mode 100644 index 0000000000000..d843b51f8b315 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -0,0 +1,13 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Represents an ApiGatewayV2 ApiMapping resource + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-apimapping.html + */ +export interface IApiMapping extends IResource { + /** + * ID of the api mapping + * @attribute + */ + readonly apiMappingId: string; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts new file mode 100644 index 0000000000000..93234807bbf09 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts @@ -0,0 +1,117 @@ +import { ICertificate } from '@aws-cdk/aws-certificatemanager'; +import { Construct, IResource, Resource, Token } from '@aws-cdk/core'; +import { CfnDomainName, CfnDomainNameProps } from '../apigatewayv2.generated'; + +/** + * Represents an APIGatewayV2 DomainName + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-domainname.html + */ +export interface IDomainName extends IResource { + /** + * The custom domain name + * + * @attribute + * + */ + readonly domainName: string; + + /** + * The domain name associated with the regional endpoint for this custom domain name. + * + * @attribute + */ + readonly regionalDomainName: string; + + /** + * The region-specific Amazon Route 53 Hosted Zone ID of the regional endpoint. + * + * @attribute + */ + readonly regionalHostedZoneId: string; +} + +/** + * custom domain name attributes + */ +export interface DomainNameAttributes { + /** + * domain name string + */ + readonly domainName: string; + + /** + * The domain name associated with the regional endpoint for this custom domain name. + */ + readonly regionalDomainName: string; + + /** + * The region-specific Amazon Route 53 Hosted Zone ID of the regional endpoint. + */ + readonly regionalHostedZoneId: string; +} + +/** + * properties used for creating the DomainName + */ +export interface DomainNameProps { + /** + * The custom domain name + */ + readonly domainName: string; + /** + * The ACM certificate for this domain name + */ + readonly certificate: ICertificate; +} + +/** + * Custom domain resource for the API + */ +export class DomainName extends Resource implements IDomainName { + /** + * import from attributes + */ + public static fromDomainNameAttributes(scope: Construct, id: string, attrs: DomainNameAttributes): IDomainName { + class Import extends Resource implements IDomainName { + public readonly regionalDomainName = attrs.regionalDomainName; + public readonly regionalHostedZoneId = attrs.regionalHostedZoneId; + public readonly domainName = attrs.domainName; + } + return new Import(scope, id); + } + + /** + * The custom domain name for your API in Amazon API Gateway. + * + * @attribute + */ + public readonly domainName: string; + + /** + * The domain name associated with the regional endpoint for this custom domain name. + */ + public readonly regionalDomainName: string; + + /** + * The region-specific Amazon Route 53 Hosted Zone ID of the regional endpoint. + */ + public readonly regionalHostedZoneId: string; + + constructor(scope: Construct, id: string, props: DomainNameProps) { + super(scope, id); + + const domainNameProps: CfnDomainNameProps = { + domainName: props.domainName, + domainNameConfigurations: [ + { + certificateArn: props.certificate.certificateArn, + endpointType: 'REGIONAL', + }, + ], + }; + const resource = new CfnDomainName(this, 'Resource', domainNameProps); + this.domainName = props.domainName ?? resource.ref; + this.regionalDomainName = Token.asString(resource.getAtt('RegionalDomainName')); + this.regionalHostedZoneId = Token.asString(resource.getAtt('RegionalHostedZoneId')); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts index 5995c40125978..d727436b86c99 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts @@ -1,3 +1,5 @@ export * from './integration'; export * from './route'; -export * from './stage'; \ No newline at end of file +export * from './stage'; +export * from './domain-name'; +export * from './api-mapping'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts new file mode 100644 index 0000000000000..855c6a1f30638 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts @@ -0,0 +1,78 @@ +import { Construct, Resource } from '@aws-cdk/core'; +import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated'; +import { IApiMapping, IDomainName } from '../common'; +import { IHttpApi } from '../http/api'; +import { IHttpStage } from './stage'; + +/** + * Properties used to create the HttpApiMapping resource + */ +export interface HttpApiMappingProps { + /** + * Api mapping key. The path where this stage should be mapped to on the domain + * @default '/' + */ + readonly apiMappingKey?: string; + + /** + * The HttpApi to which this mapping is applied + */ + readonly api: IHttpApi; + + /** + * custom domain name of the mapping target + */ + readonly domainName: IDomainName; + + /** + * stage for the HttpApiMapping resource + * + * @default - the $default stage + */ + readonly stage?: IHttpStage; +} + +/** + * The attributes used to import existing HttpApiMapping + */ +export interface HttpApiMappingAttributes { + /** + * The API mapping ID + */ + readonly apiMappingId: string; +} + +/** + * Create a new API mapping for API Gateway HTTP API endpoint. + * @resource AWS::ApiGatewayV2::ApiMapping + */ +export class HttpApiMapping extends Resource implements IApiMapping { + /** + * import from API ID + */ + public static fromHttpApiMappingAttributes(scope: Construct, id: string, attrs: HttpApiMappingAttributes): IApiMapping { + class Import extends Resource implements IApiMapping { + public readonly apiMappingId = attrs.apiMappingId; + } + return new Import(scope, id); + } + /** + * ID of the API Mapping + */ + public readonly apiMappingId: string; + + constructor(scope: Construct, id: string, props: HttpApiMappingProps) { + super(scope, id); + + const apiMappingProps: CfnApiMappingProps = { + apiId: props.api.httpApiId, + domainName: props.domainName.domainName, + stage: props.stage?.stageName ?? '$default', + apiMappingKey: props.apiMappingKey, + }; + + const resource = new CfnApiMapping(this, 'Resource', apiMappingProps); + this.apiMappingId = resource.ref; + } + +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 7856ff5f87bf7..c8a12d35ed354 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -1,5 +1,6 @@ import { Construct, Duration, IResource, Resource } from '@aws-cdk/core'; import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; +import { DefaultDomainMappingOptions } from '../http/stage'; import { IHttpRouteIntegration } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; import { HttpStage, HttpStageOptions } from './stage'; @@ -43,6 +44,13 @@ export interface HttpApiProps { * @default - CORS disabled. */ readonly corsPreflight?: CorsPreflightOptions; + + /** + * Configure a custom domain with the API mapping resource to the HTTP API + * + * @default - no default domain mapping configured. meaningless if `createDefaultStage` is `false`. + */ + readonly defaultDomainMapping?: DefaultDomainMappingOptions; } /** @@ -118,6 +126,9 @@ export class HttpApi extends Resource implements IHttpApi { } public readonly httpApiId: string; + /** + * default stage of the api resource + */ private readonly defaultStage: HttpStage | undefined; constructor(scope: Construct, id: string, props?: HttpApiProps) { @@ -166,8 +177,14 @@ export class HttpApi extends Resource implements IHttpApi { this.defaultStage = new HttpStage(this, 'DefaultStage', { httpApi: this, autoDeploy: true, + domainMapping: props?.defaultDomainMapping, }); } + + if (props?.createDefaultStage === false && props.defaultDomainMapping) { + throw new Error('defaultDomainMapping not supported with createDefaultStage disabled', + ); + } } /** @@ -182,10 +199,11 @@ export class HttpApi extends Resource implements IHttpApi { * Add a new stage. */ public addStage(id: string, options: HttpStageOptions): HttpStage { - return new HttpStage(this, id, { + const stage = new HttpStage(this, id, { httpApi: this, ...options, }); + return stage; } /** diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts index 4fbb1c4e76f6a..c42e089aa1d08 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts @@ -2,4 +2,5 @@ export * from './api'; export * from './route'; export * from './integration'; export * from './integrations'; -export * from './stage'; \ No newline at end of file +export * from './stage'; +export * from './api-mapping'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index 1335cbf839984..8fc3e605d87b4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -1,14 +1,27 @@ import { Construct, Resource, Stack } from '@aws-cdk/core'; import { CfnStage } from '../apigatewayv2.generated'; -import { CommonStageOptions, IStage } from '../common'; +import { CommonStageOptions, IDomainName, IStage } from '../common'; import { IHttpApi } from './api'; +import { HttpApiMapping } from './api-mapping'; const DEFAULT_STAGE_NAME = '$default'; +/** + * Represents the HttpStage + */ +export interface IHttpStage extends IStage { +} + /** * Options to create a new stage for an HTTP API. */ export interface HttpStageOptions extends CommonStageOptions { + /** + * The options for custom domain and api mapping + * + * @default - no custom domain and api mapping configuration + */ + readonly domainMapping?: DomainMappingOptions; } /** @@ -21,6 +34,36 @@ export interface HttpStageProps extends HttpStageOptions { readonly httpApi: IHttpApi; } +/** + * Options for defaultDomainMapping + */ +export interface DefaultDomainMappingOptions { + /** + * The domain name for the mapping + * + */ + readonly domainName: IDomainName; + + /** + * The API mapping key. Specify '/' for the root path mapping. + * + */ + readonly mappingKey: string; + +} + +/** + * Options for DomainMapping + */ +export interface DomainMappingOptions extends DefaultDomainMappingOptions { + /** + * The API Stage + * + * @default - the $default stage + */ + readonly stage?: IStage; +} + /** * Represents a stage where an instance of the API is deployed. * @resource AWS::ApiGatewayV2::Stage @@ -52,6 +95,16 @@ export class HttpStage extends Resource implements IStage { this.stageName = this.physicalName; this.httpApi = props.httpApi; + + if (props.domainMapping) { + new HttpApiMapping(this, `${props.domainMapping.domainName}${props.domainMapping.mappingKey}`, { + api: props.httpApi, + domainName: props.domainMapping.domainName, + stage: this, + apiMappingKey: props.domainMapping.mappingKey, + }); + } + } /** @@ -62,4 +115,4 @@ export class HttpStage extends Resource implements IStage { const urlPath = this.stageName === DEFAULT_STAGE_NAME ? '' : this.stageName; return `https://${this.httpApi.httpApiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 0920b42061902..bc22bfc9ed2f7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -71,6 +71,7 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/core": "0.0.0", @@ -79,6 +80,7 @@ "peerDependencies": { "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, @@ -89,7 +91,9 @@ "exclude": [ "from-method:@aws-cdk/aws-apigatewayv2.HttpIntegration", "from-method:@aws-cdk/aws-apigatewayv2.HttpRoute", + "from-method:@aws-cdk/aws-apigatewayv2.HttpStage", "props-physical-name-type:@aws-cdk/aws-apigatewayv2.HttpStageProps.stageName", + "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpApiMappingProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps" ] diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts new file mode 100644 index 0000000000000..d2a536ae52862 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts @@ -0,0 +1,89 @@ +import '@aws-cdk/assert/jest'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; +import { Stack } from '@aws-cdk/core'; +import { DomainName, HttpApi, HttpApiMapping } from '../../lib'; + +const domainName = 'example.com'; +const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; + +describe('ApiMapping', () => { + test('default stage', () => { + + const stack = new Stack(); + const api = new HttpApi(stack, 'Api'); + + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + new HttpApiMapping(stack, 'Mapping', { + api, + domainName: dn, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::ApiMapping', { + ApiId: { + Ref: 'ApiF70053CD', + }, + DomainName: 'example.com', + Stage: '$default', + }); + }); + + test('beta stage mapping', () => { + + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: false, + }); + + const beta = api.addStage('beta', { + stageName: 'beta', + }); + + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + new HttpApiMapping(stack, 'Mapping', { + api, + domainName: dn, + stage: beta, + apiMappingKey: 'beta', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::ApiMapping', { + ApiId: { + Ref: 'ApiF70053CD', + }, + DomainName: 'example.com', + Stage: 'beta', + ApiMappingKey: 'beta', + }); + }); + + test('import mapping', () => { + + const stack = new Stack(); + const api = new HttpApi(stack, 'Api'); + + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + const mapping = new HttpApiMapping(stack, 'Mapping', { + api, + domainName: dn, + apiMappingKey: '/', + }); + + const imported = HttpApiMapping.fromHttpApiMappingAttributes(stack, 'ImportedMapping', { + apiMappingId: mapping.apiMappingId, + } ); + + expect(imported.apiMappingId).toEqual(mapping.apiMappingId); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index 3f89ff312b14f..5e88dbfe3cbf5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -25,6 +25,15 @@ describe('HttpApi', () => { expect(api.url).toBeDefined(); }); + test('import', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'api', { apiName: 'customName' }); + const imported = HttpApi.fromApiId(stack, 'imported', api.httpApiId ); + + expect(imported.httpApiId).toEqual(api.httpApiId); + + }); + test('unsetting createDefaultStage', () => { const stack = new Stack(); const api = new HttpApi(stack, 'api', { @@ -104,4 +113,4 @@ describe('HttpApi', () => { RouteKey: 'ANY /pets', }); }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts new file mode 100644 index 0000000000000..12d3327300df3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts @@ -0,0 +1,154 @@ +import '@aws-cdk/assert/jest'; +// import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; +import { Stack } from '@aws-cdk/core'; +import { DomainName, HttpApi } from '../../lib'; + +const domainName = 'example.com'; +const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; + +describe('DomainName', () => { + test('create domain name correctly', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::DomainName', { + DomainName: 'example.com', + DomainNameConfigurations: [ + { + CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate', + EndpointType: 'REGIONAL', + }, + ], + }); + }); + + test('import domain name correctly', () => { + // GIVEN + const stack = new Stack(); + + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // WHEN + const imported = DomainName.fromDomainNameAttributes(stack, 'dn', { + domainName: dn.domainName, + regionalDomainName: dn.regionalDomainName, + regionalHostedZoneId: dn.regionalHostedZoneId, + }); + + // THEN; + expect(imported.domainName).toEqual(dn.domainName); + expect(imported.regionalDomainName).toEqual(dn.regionalDomainName); + expect(imported.regionalHostedZoneId).toEqual(dn.regionalHostedZoneId); + }); + + test('addStage with domainNameMapping', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: true, + }); + + // WHEN + const dn = new DomainName(stack, 'DN', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + api.addStage('beta', { + stageName: 'beta', + autoDeploy: true, + domainMapping: { + domainName: dn, + mappingKey: 'beta', + }, + }); + + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::DomainName', { + DomainName: 'example.com', + DomainNameConfigurations: [ + { + CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate', + EndpointType: 'REGIONAL', + }, + ], + }); + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::ApiMapping', { + ApiId: { + Ref: 'ApiF70053CD', + }, + DomainName: 'example.com', + Stage: 'beta', + ApiMappingKey: 'beta', + }); + }); + + test('api with defaultDomainMapping', () => { + // GIVEN + const stack = new Stack(); + const dn = new DomainName(stack, 'DN', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // WHEN + new HttpApi(stack, 'Api', { + createDefaultStage: true, + defaultDomainMapping: { + domainName: dn, + mappingKey: '/', + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::DomainName', { + DomainName: 'example.com', + DomainNameConfigurations: [ + { + CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate', + EndpointType: 'REGIONAL', + }, + ], + }); + + expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::ApiMapping', { + ApiId: { + Ref: 'ApiF70053CD', + }, + DomainName: 'example.com', + Stage: '$default', + }); + }); + + test('throws when defaultDomainMapping enabled with createDefaultStage disabled', () => { + // GIVEN + const stack = new Stack(); + const dn = new DomainName(stack, 'DN', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + const t = () => { + new HttpApi(stack, 'Api', { + createDefaultStage: false, + defaultDomainMapping: { + domainName: dn, + mappingKey: '/', + }, + }); + }; + + // WHEN/THEN + expect(t).toThrow('defaultDomainMapping not supported with createDefaultStage disabled'); + + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.expected.json new file mode 100644 index 0000000000000..2fb796e79265c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.expected.json @@ -0,0 +1,348 @@ +{ + "Resources": { + "echohandlerServiceRole833A8F7A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [{ + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + }], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [{ + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + }] + } + }, + "echohandler8F648AB2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, headers: { \"content-type\": \"application/json\" }, body: JSON.stringify(event) }; };" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "echohandlerServiceRole833A8F7A", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "echohandlerServiceRole833A8F7A" + ] + }, + "echohandlerinteghttpproxyHttpProxyProdApiDefaultRoute20082F68PermissionBE86C6B3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "echohandler8F648AB2", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "HttpProxyProdApi368B6161" + }, + "/*/*" + ] + ] + } + } + }, + "echohandlerinteghttpproxyHttpProxyBetaApiDefaultRouteC328B302Permission40FB964B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "echohandler8F648AB2", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "HttpProxyBetaApiBFB4DA5E" + }, + "/*/*" + ] + ] + } + } + }, + "DNFDC76583": { + "Type": "AWS::ApiGatewayV2::DomainName", + "Properties": { + "DomainName": "apigv2.demo.com", + "DomainNameConfigurations": [{ + "CertificateArn": "arn:aws:acm:us-east-1:111111111111:certificate", + "EndpointType": "REGIONAL" + }] + } + }, + "HttpProxyProdApi368B6161": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpProxyProdApi", + "ProtocolType": "HTTP" + } + }, + "HttpProxyProdApiDefaultRouteDefaultRouteIntegration702F0DF7": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpProxyProdApi368B6161" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "echohandler8F648AB2", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, + "HttpProxyProdApiDefaultRoute40EFC108": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyProdApi368B6161" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyProdApiDefaultRouteDefaultRouteIntegration702F0DF7" + } + ] + ] + } + } + }, + "HttpProxyProdApiDefaultStage0038B180": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpProxyProdApi368B6161" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "HttpProxyProdApiDefaultStageinteghttpproxyDN4CD83A2F": { + "Type": "AWS::ApiGatewayV2::ApiMapping", + "Properties": { + "ApiId": { + "Ref": "HttpProxyProdApi368B6161" + }, + "ApiMappingKey": "/", + "DomainName": "apigv2.demo.com", + "Stage": "$default" + } + }, + "HttpProxyProdApitesting225373A0": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpProxyProdApi368B6161" + }, + "StageName": "testing", + "AutoDeploy": true + } + }, + "HttpProxyProdApitestinginteghttpproxyDNtestingBEBAEF7B": { + "Type": "AWS::ApiGatewayV2::ApiMapping", + "Properties": { + "ApiId": { + "Ref": "HttpProxyProdApi368B6161" + }, + "DomainName": "apigv2.demo.com", + "Stage": "testing", + "ApiMappingKey": "testing" + } + }, + "HttpProxyBetaApiBFB4DA5E": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpProxyBetaApi", + "ProtocolType": "HTTP" + } + }, + "HttpProxyBetaApiDefaultRouteDefaultRouteIntegration24A25241": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpProxyBetaApiBFB4DA5E" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "echohandler8F648AB2", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, + "HttpProxyBetaApiDefaultRoute12DC547F": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyBetaApiBFB4DA5E" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyBetaApiDefaultRouteDefaultRouteIntegration24A25241" + } + ] + ] + } + } + }, + "HttpProxyBetaApiDefaultStage4890F8A1": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpProxyBetaApiBFB4DA5E" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "HttpProxyBetaApiDefaultStageinteghttpproxyDNbeta0904192E": { + "Type": "AWS::ApiGatewayV2::ApiMapping", + "Properties": { + "ApiId": { + "Ref": "HttpProxyBetaApiBFB4DA5E" + }, + "ApiMappingKey": "beta", + "DomainName": "apigv2.demo.com", + "Stage": "$default" + } + } + }, + "Outputs": { + "RegionalDomainName": { + "Value": { + "Fn::GetAtt": [ + "DNFDC76583", + "RegionalDomainName" + ] + } + }, + "DomainName": { + "Value": "apigv2.demo.com" + }, + "CustomUDomainURL": { + "Value": "https://apigv2.demo.com" + }, + "ProdApiEndpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "HttpProxyProdApi368B6161" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + }, + "BetaApiEndpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "HttpProxyBetaApiBFB4DA5E" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + }, + "Region": { + "Value": { + "Ref": "AWS::Region" + } + } + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.ts new file mode 100644 index 0000000000000..4064a256d961f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integ.custom-domain.ts @@ -0,0 +1,57 @@ +import * as acm from '@aws-cdk/aws-certificatemanager'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { DomainName, HttpApi, LambdaProxyIntegration } from '../../lib'; + +const app = new App(); + +const stack = new Stack(app, 'integ-http-proxy'); + +const handler = new lambda.Function(stack, 'echohandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { return { statusCode: 200, headers: { "content-type": "application/json" }, body: JSON.stringify(event) }; };'), +}); + +const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; +const domainName = 'apigv2.demo.com'; + +const dn = new DomainName(stack, 'DN', { + domainName, + certificate: acm.Certificate.fromCertificateArn(stack, 'cert', certArn), +}); + +const prodApi = new HttpApi(stack, 'HttpProxyProdApi', { + defaultIntegration: new LambdaProxyIntegration({ handler }), + // https://${dn.domainName} goes to prodApi $default stage + defaultDomainMapping: { + domainName: dn, + mappingKey: '/', + }, +}); + +const betaApi = new HttpApi(stack, 'HttpProxyBetaApi', { + defaultIntegration: new LambdaProxyIntegration({ handler }), + // https://${dn.domainName}/beta goes to betaApi $default stage + defaultDomainMapping: { + domainName: dn, + mappingKey: 'beta', + }, +}); + +prodApi.addStage('testing', { + stageName: 'testing', + autoDeploy: true, + // https://${dn.domainName}/testing goes to prodApi testing stage + domainMapping: { + domainName: dn, + mappingKey: 'testing', + }, +} ); + +new CfnOutput(stack, 'RegionalDomainName', { value: dn.regionalDomainName }); +new CfnOutput(stack, 'DomainName', { value: dn.domainName }); +new CfnOutput(stack, 'CustomUDomainURL', { value: `https://${dn.domainName}` }); +new CfnOutput(stack, 'ProdApiEndpoint', { value: prodApi.url! }); +new CfnOutput(stack, 'BetaApiEndpoint', { value: betaApi.url! }); +new CfnOutput(stack, 'Region', { value: Stack.of(stack).region}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts index cc5ed64df20eb..8f187d0ab9c78 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts @@ -1,7 +1,7 @@ import { ABSENT } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Duration, Stack } from '@aws-cdk/core'; -import { HttpApi, HttpMethod, HttpProxyIntegration, HttpRoute, HttpRouteKey } from '../../../lib'; +import { HttpApi, HttpIntegration, HttpIntegrationType, HttpMethod, HttpProxyIntegration, HttpRoute, HttpRouteKey, PayloadFormatVersion } from '../../../lib'; describe('HttpProxyIntegration', () => { test('default', () => { @@ -40,6 +40,40 @@ describe('HttpProxyIntegration', () => { }); }); + test('custom payload format version is allowed', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpIntegration(stack, 'HttpInteg', { + payloadFormatVersion: PayloadFormatVersion.custom('99.99'), + httpApi: api, + integrationType: HttpIntegrationType.HTTP_PROXY, + integrationUri: 'some-target-url', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + IntegrationUri: 'some-target-url', + PayloadFormatVersion: '99.99', + }); + }); + + test('HttpIntegration without payloadFormatVersion is allowed', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + new HttpIntegration(stack, 'HttpInteg', { + httpApi: api, + integrationType: HttpIntegrationType.HTTP_PROXY, + integrationUri: 'some-target-url', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + IntegrationUri: 'some-target-url', + }); + }); +}); + +describe('CORS', () => { test('CORS Configuration is correctly configured.', () => { const stack = new Stack(); new HttpApi(stack, 'HttpApi', { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index 25df12528e63e..f129db5186486 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -52,6 +52,29 @@ describe('HttpRoute', () => { IntegrationUri: 'some-uri', }); }); + + test('throws when path not start with /', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + expect(() => new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new DummyIntegration(), + routeKey: HttpRouteKey.with('books', HttpMethod.GET), + })).toThrowError(/path must always start with a "\/" and not end with a "\/"/); + }); + + test('throws when path ends with /', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + expect(() => new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new DummyIntegration(), + routeKey: HttpRouteKey.with('/books/', HttpMethod.GET), + })).toThrowError(/path must always start with a "\/" and not end with a "\/"/); + }); + }); class DummyIntegration implements IHttpRouteIntegration { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts index 336d3a74852a4..06e7a9efbc4ce 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts @@ -19,6 +19,21 @@ describe('HttpStage', () => { }); }); + test('import', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: false, + }); + + const stage = new HttpStage(stack, 'Stage', { + httpApi: api, + }); + + const imported = HttpStage.fromStageName(stack, 'Import', stage.stageName ); + + expect(imported.stageName).toEqual(stage.stageName); + }); + test('url returns the correct path', () => { const stack = new Stack(); const api = new HttpApi(stack, 'Api', { diff --git a/packages/@aws-cdk/aws-batch/README.md b/packages/@aws-cdk/aws-batch/README.md index a1ad987fd09bd..83589b65a12f8 100644 --- a/packages/@aws-cdk/aws-batch/README.md +++ b/packages/@aws-cdk/aws-batch/README.md @@ -1,4 +1,5 @@ ## AWS Batch Construct Library + --- @@ -15,37 +16,206 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -## Launch template support +AWS Batch is a batch processing tool for efficiently running hundreds of thousands computing jobs in AWS. Batch can dynamically provision different types of compute resources based on the resource requirements of submitted jobs. + +AWS Batch simplifies the planning, scheduling, and executions of your batch workloads across a full range of compute services like [Amazon EC2](https://aws.amazon.com/ec2/) and [Spot Resources](https://aws.amazon.com/ec2/spot/). + +Batch achieves this by utilizing queue processing of batch job requests. To successfully submit a job for execution, you need the following resources: + +1. [Job Definition](#job-definition) - *Group various job properties (container image, resource requirements, env variables...) into a single definition. These definitions are used at job submission time.* +2. [Compute Environment](#compute-environment) - *the execution runtime of submitted batch jobs* +3. [Job Queue](#job-queue) - *the queue where batch jobs can be submitted to via AWS SDK/CLI* + +For more information on **AWS Batch** visit the [AWS Docs for Batch](https://docs.aws.amazon.com/batch/index.html). + +## Compute Environment + +At the core of AWS Batch is the compute environment. All batch jobs are processed within a compute environment, which uses resource like OnDemand or Spot EC2 instances. + +In **MANAGED** mode, AWS will handle the provisioning of compute resources to accommodate the demand. Otherwise, in **UNMANAGED** mode, you will need to manage the provisioning of those resources. + +Below is an example of each available type of compute environment: + +```ts +const defaultVpc = new ec2.Vpc(this, 'VPC'); + +// default is managed +const awsManagedEnvironment = new batch.ComputeEnvironment(stack, 'AWS-Managed-Compute-Env', { + computeResources: { + vpc + } +}); + +const customerManagedEnvironment = new batch.ComputeEnvironment(stack, 'Customer-Managed-Compute-Env', { + managed: false // unmanaged environment +}); +``` + +### Spot-Based Compute Environment + +It is possible to have AWS Batch submit spotfleet requests for obtaining compute resources. Below is an example of how this can be done: + +```ts +const vpc = new ec2.Vpc(this, 'VPC'); + +const spotEnvironment = new batch.ComputeEnvironment(stack, 'MySpotEnvironment', { + computeResources: { + type: batch.ComputeResourceType.SPOT, + bidPercentage: 75, // Bids for resources at 75% of the on-demand price + vpc, + }, +}); +``` + +### Understanding Progressive Allocation Strategies + +AWS Batch uses an [allocation strategy](https://docs.aws.amazon.com/batch/latest/userguide/allocation-strategies.html) to determine what compute resource will efficiently handle incoming job requests. By default, **BEST_FIT** will pick an available compute instance based on vCPU requirements. If none exist, the job will wait until resources become available. However, with this strategy, you may have jobs waiting in the queue unnecessarily despite having more powerful instances available. Below is an example of how that situation might look like: + +``` +Compute Environment: + +1. m5.xlarge => 4 vCPU +2. m5.2xlarge => 8 vCPU +``` + +``` +Job Queue: +--------- +| A | B | +--------- + +Job Requirements: +A => 4 vCPU - ALLOCATED TO m5.xlarge +B => 2 vCPU - WAITING +``` + +In this situation, Batch will allocate **Job A** to compute resource #1 because it is the most cost efficient resource that matches the vCPU requirement. However, with this `BEST_FIT` strategy, **Job B** will not be allocated to our other available compute resource even though it is strong enough to handle it. Instead, it will wait until the first job is finished processing or wait a similar `m5.xlarge` resource to be provisioned. + +The alternative would be to use the `BEST_FIT_PROGRESSIVE` strategy in order for the remaining job to be handled in larger containers regardless of vCPU requirement and costs. + +### Launch template support -### Usage Simply define your Launch Template: ```typescript - const myLaunchTemplate = new ec2.CfnLaunchTemplate(this, 'LaunchTemplate', { - launchTemplateName: 'extra-storage-template', - launchTemplateData: { - blockDeviceMappings: [ - { - deviceName: '/dev/xvdcz', - ebs: { - encrypted: true, - volumeSize: 100, - volumeType: 'gp2' - } - } - ] +const myLaunchTemplate = new ec2.CfnLaunchTemplate(this, 'LaunchTemplate', { + launchTemplateName: 'extra-storage-template', + launchTemplateData: { + blockDeviceMappings: [ + { + deviceName: '/dev/xvdcz', + ebs: { + encrypted: true, + volumeSize: 100, + volumeType: 'gp2' + } } - }); + ] + } +}); ``` + and use it: ```typescript - const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { - computeResources: { - launchTemplate: { - launchTemplateName: myLaunchTemplate.launchTemplateName as string, //or simply use an existing template name - }, - vpc, - }, - computeEnvironmentName: 'MyStorageCapableComputeEnvironment', - }); +const myComputeEnv = new batch.ComputeEnvironment(this, 'ComputeEnv', { + computeResources: { + launchTemplate: { + launchTemplateName: myLaunchTemplate.launchTemplateName as string, //or simply use an existing template name + }, + vpc, + }, + computeEnvironmentName: 'MyStorageCapableComputeEnvironment', +}); +``` + +### Importing an existing Compute Environment + +To import an existing batch compute environment, call `ComputeEnvironment.fromComputeEnvironmentArn()`. + +Below is an example: + +```ts +const computeEnv = batch.ComputeEnvironment.fromComputeEnvironmentArn(this, 'imported-compute-env', 'arn:aws:batch:us-east-1:555555555555:compute-environment/My-Compute-Env'); +``` + +## Job Queue + +Jobs are always submitted to a specific queue. This means that you have to create a queue before you can start submitting jobs. Each queue is mapped to at least one (and no more than three) compute environment. When the job is scheduled for execution, AWS Batch will select the compute environment based on ordinal priority and available capacity in each environment. + +```ts +const jobQueue = new batch.JobQueue(stack, 'JobQueue', { + computeEnvironments: [ + { + // Defines a collection of compute resources to handle assigned batch jobs + computeEnvironment, + // Order determines the allocation order for jobs (i.e. Lower means higher preferance for job assignment) + order: 1, + }, + ], +}); +``` + +### Priorty-Based Queue Example + +Sometimes you might have jobs that are more important than others, and when submitted, should take precedence over the existing jobs. To achieve this, you can create a priority based execution strategy, by assigning each queue its own priority: + +```ts +const highPrioQueue = new batch.JobQueue(stack, 'JobQueue', { + computeEnvironments: sharedComputeEnvs, + priority: 2, +}); + +const lowPrioQueue = new batch.JobQueue(stack, 'JobQueue', { + computeEnvironments: sharedComputeEnvs, + priority: 1, +}); +``` + +By making sure to use the same compute environments between both job queues, we will give precedence to the `highPrioQueue` for the assigning of jobs to available compute environments. + +### Importing an existing Job Queue + +To import an existing batch job queue, call `JobQueue.fromJobQueueArn()`. + +Below is an example: + +```ts +const jobQueue = batch.JobQueue.fromJobQueueArn(this, 'imported-job-queue', 'arn:aws:batch:us-east-1:555555555555:job-queue/High-Prio-Queue'); +``` + +## Job Definition + +A Batch Job definition helps AWS Batch understand important details about how to run your application in the scope of a Batch Job. This involves key information like resource requirements, what containers to run, how the compute environment should be prepared, and more. Below is a simple example of how to create a job definition: + +```ts +const repo = ecr.Repository.fromRepositoryName(stack, 'batch-job-repo', 'todo-list'); + +new batch.JobDefinition(stack, 'batch-job-def-from-ecr', { + container: { + image: new ecs.EcrImage(repo, 'latest'), + }, +}); +``` + +### Using a local Docker project + +Below is an example of how you can create a Batch Job Definition from a local Docker application. + +```ts +new batch.JobDefinition(stack, 'batch-job-def-from-local', { + container: { + // todo-list is a directory containing a Dockerfile to build the application + image: ecs.ContainerImage.fromAsset('../todo-list'), + }, +}); +``` + +### Importing an existing Job Definition + +To import an existing batch job definition, call `JobDefinition.fromJobDefinitionArn()`. + +Below is an example: + +```ts +const job = batch.JobDefinition.fromJobDefinitionArn(this, 'imported-job-definition', 'arn:aws:batch:us-east-1:555555555555:job-definition/my-job-definition'); ``` diff --git a/packages/@aws-cdk/aws-config/lib/rule.ts b/packages/@aws-cdk/aws-config/lib/rule.ts index 0659fefa8089b..12dd86b8ab1bd 100644 --- a/packages/@aws-cdk/aws-config/lib/rule.ts +++ b/packages/@aws-cdk/aws-config/lib/rule.ts @@ -122,10 +122,10 @@ abstract class RuleNew extends RuleBase { * @param identifier the resource identifier */ public scopeToResource(type: string, identifier?: string) { - this.scopeTo({ + this.scope = { complianceResourceId: identifier, complianceResourceTypes: [type], - }); + }; } /** @@ -136,9 +136,9 @@ abstract class RuleNew extends RuleBase { * @param types resource types */ public scopeToResources(...types: string[]) { - this.scopeTo({ + this.scope = { complianceResourceTypes: types, - }); + }; } /** @@ -148,18 +148,10 @@ abstract class RuleNew extends RuleBase { * @param value the tag value */ public scopeToTag(key: string, value?: string) { - this.scopeTo({ + this.scope = { tagKey: key, tagValue: value, - }); - } - - private scopeTo(scope: CfnConfigRule.ScopeProperty) { - if (!this.isManaged && !this.isCustomWithChanges) { - throw new Error('Cannot scope rule when `configurationChanges` is set to false.'); - } - - this.scope = scope; + }; } } diff --git a/packages/@aws-cdk/aws-config/test/integ.scoped-rule.expected.json b/packages/@aws-cdk/aws-config/test/integ.scoped-rule.expected.json new file mode 100644 index 0000000000000..99d314d0c45af --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/integ.scoped-rule.expected.json @@ -0,0 +1,109 @@ +{ + "Resources": { + "CustomFunctionServiceRoleD3F73B79": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSConfigRulesExecutionRole" + ] + ] + } + ] + } + }, + "CustomFunctionBADD59E7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(event);" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomFunctionServiceRoleD3F73B79", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "CustomFunctionServiceRoleD3F73B79" + ] + }, + "CustomFunctionPermission41887A5E": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "CustomFunctionBADD59E7", + "Arn" + ] + }, + "Principal": "config.amazonaws.com" + } + }, + "Custom8166710A": { + "Type": "AWS::Config::ConfigRule", + "Properties": { + "Source": { + "Owner": "CUSTOM_LAMBDA", + "SourceDetails": [ + { + "EventSource": "aws.config", + "MessageType": "ScheduledNotification" + } + ], + "SourceIdentifier": { + "Fn::GetAtt": [ + "CustomFunctionBADD59E7", + "Arn" + ] + } + }, + "Scope": { + "ComplianceResourceTypes": [ + "AWS::EC2::Instance" + ] + } + }, + "DependsOn": [ + "CustomFunctionPermission41887A5E", + "CustomFunctionBADD59E7", + "CustomFunctionServiceRoleD3F73B79" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/integ.scoped-rule.ts b/packages/@aws-cdk/aws-config/test/integ.scoped-rule.ts new file mode 100644 index 0000000000000..aee8392f402f0 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/integ.scoped-rule.ts @@ -0,0 +1,22 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as config from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-config-rule-scoped-integ'); + +const fn = new lambda.Function(stack, 'CustomFunction', { + code: lambda.AssetCode.fromInline('exports.handler = (event) => console.log(event);'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, +}); + +const customRule = new config.CustomRule(stack, 'Custom', { + lambdaFunction: fn, + periodic: true, +}); + +customRule.scopeToResource('AWS::EC2::Instance'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-config/test/test.rule.ts b/packages/@aws-cdk/aws-config/test/test.rule.ts index 0f19a826de6d2..c13dacf2c8a3f 100644 --- a/packages/@aws-cdk/aws-config/test/test.rule.ts +++ b/packages/@aws-cdk/aws-config/test/test.rule.ts @@ -204,7 +204,7 @@ export = { test.done(); }, - 'throws when scoping a custom rule without configuration changes'(test: Test) { + 'allows scoping a custom rule without configurationChanges enabled'(test: Test) { // GIVEN const stack = new cdk.Stack(); const fn = new lambda.Function(stack, 'Function', { @@ -220,7 +220,7 @@ export = { }); // THEN - test.throws(() => rule.scopeToResource('resource'), /`configurationChanges`/); + test.doesNotThrow(() => rule.scopeToResource('resource')); test.done(); }, diff --git a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts index c7640916d70c1..3ab5287b9f782 100644 --- a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts @@ -263,8 +263,18 @@ export class Nodegroup extends Resource implements INodegroup { tags: props.tags, }); - // As managed nodegroup will auto map the instance role to RBAC behind the scene and users don't have to manually - // do it anymore. We don't need to print out the instance role arn now. + // managed nodegroups update the `aws-auth` on creation, but we still need to track + // its state for consistency. + if (this.cluster.kubectlEnabled) { + // see https://docs.aws.amazon.com/en_us/eks/latest/userguide/add-user-role.html + this.cluster.awsAuth.addRoleMapping(this.role, { + username: 'system:node:{{EC2PrivateDNSName}}', + groups: [ + 'system:bootstrappers', + 'system:nodes', + ], + }); + } this.nodegroupArn = this.getResourceArnAttribute(resource.attrArn, { service: 'eks', diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index f957a467b48aa..afec1aeda1a31 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -894,6 +894,13 @@ ] }, "\\\",\\\"groups\\\":[\\\"system:masters\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterNodegroupDefaultCapacityNodeGroupRole55953B04", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]},{\\\"rolearn\\\":\\\"", { "Fn::GetAtt": [ "ClusterfargateprofiledefaultPodExecutionRole09952CFF", @@ -928,6 +935,13 @@ "Arn" ] }, + "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterNodegroupextrangNodeGroupRole23AE23D0", + "Arn" + ] + }, "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]}]\",\"mapUsers\":\"[]\",\"mapAccounts\":\"[]\"}}]" ] ] diff --git a/packages/@aws-cdk/aws-eks/test/test.awsauth.ts b/packages/@aws-cdk/aws-eks/test/test.awsauth.ts index cf3ddc99e09e8..ca13492487027 100644 --- a/packages/@aws-cdk/aws-eks/test/test.awsauth.ts +++ b/packages/@aws-cdk/aws-eks/test/test.awsauth.ts @@ -53,6 +53,13 @@ export = { '', [ '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterNodegroupDefaultCapacityNodeGroupRole55953B04', + 'Arn', + ], + }, + '\\",\\"username\\":\\"system:node:{{EC2PrivateDNSName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\"]},{\\"rolearn\\":\\"', { 'Fn::GetAtt': [ 'roleC7B7E775', @@ -120,7 +127,14 @@ export = { 'Fn::Join': [ '', [ - '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"username\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"groups\\":[\\"group1\\"]}]","mapUsers":"[{\\"userarn\\":\\"arn:', + '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterNodegroupDefaultCapacityNodeGroupRole55953B04', + 'Arn', + ], + }, + '\\",\\"username\\":\\"system:node:{{EC2PrivateDNSName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\"]},{\\"rolearn\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"username\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"groups\\":[\\"group1\\"]}]\",\"mapUsers\":\"[{\\"userarn\\":\\"arn:', { Ref: 'AWS::Partition', }, @@ -142,6 +156,53 @@ export = { }, })); + test.done(); + }, + 'addMastersRole after addNodegroup correctly'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + const cluster = new Cluster(stack, 'Cluster', { version: CLUSTER_VERSION }); + cluster.addNodegroup('NG'); + const role = iam.Role.fromRoleArn(stack, 'imported-role', 'arn:aws:iam::123456789012:role/S3Access'); + + // WHEN + cluster.awsAuth.addMastersRole(role); + + // THEN + expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + Manifest: { + 'Fn::Join': [ + '', + [ + '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterNodegroupDefaultCapacityNodeGroupRole55953B04', + 'Arn', + ], + }, + '\\",\\"username\\":\\"system:node:{{EC2PrivateDNSName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\"]},{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'ClusterNodegroupNGNodeGroupRole7C078920', + 'Arn', + ], + }, + '\\",\\"username\\":\\"system:node:{{EC2PrivateDNSName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\"]},{\\"rolearn\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"username\\":\\"arn:aws:iam::123456789012:role/S3Access\\",\\"groups\\":[\\"system:masters\\"]}]","mapUsers":"[]","mapAccounts":"[]"}}]', + ], + ], + }, + ClusterName: { + Ref: 'Cluster9EE0221C', + }, + RoleArn: { + 'Fn::GetAtt': [ + 'ClusterCreationRole360249B6', + 'Arn', + ], + }, + })); + test.done(); }, }; diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 1acc770b47129..4101348a06774 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -923,6 +923,18 @@ export = { ], }, }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', + 'Outputs.StackawscdkawseksKubectlProviderHandlerServiceRole2C52B3ECArn', + ], + }, + }, + }, ], Version: '2012-10-17', }, diff --git a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts index 160259706d727..226fd09bc30e9 100644 --- a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts @@ -52,6 +52,49 @@ export = { )); test.done(); }, + 'aws-auth will be updated'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + kubectlEnabled: true, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { cluster }); + + // THEN + // THEN + expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Manifest: { + 'Fn::Join': [ + '', + [ + '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'NodegroupNodeGroupRole038A128B', + 'Arn', + ], + }, + '\\",\\"username\\":\\"system:node:{{EC2PrivateDNSName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\"]}]","mapUsers":"[]","mapAccounts":"[]"}}]', + ], + ], + }, + ClusterName: { + Ref: 'Cluster9EE0221C', + }, + RoleArn: { + 'Fn::GetAtt': [ + 'ClusterCreationRole360249B6', + 'Arn', + ], + }, + })); + test.done(); + }, 'create nodegroup correctly with security groups provided'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index b4e4579991f9e..20b79f7878cdc 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -34,7 +34,7 @@ automatically transpiled and bundled whether it's written in JavaScript or TypeS Alternatively, an entry file and handler can be specified: ```ts new lambda.NodejsFunction(this, 'MyFunction', { - entry: '/path/to/my/file.ts', + entry: '/path/to/my/file.ts', // accepts .js, .jsx, .ts and .tsx files handler: 'myExportedFunc' }); ``` diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts index 1138807443fec..1215330212436 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts @@ -47,7 +47,7 @@ export class NodejsFunction extends lambda.Function { } // Entry and defaults - const entry = findEntry(id, props.entry); + const entry = path.resolve(findEntry(id, props.entry)); const handler = props.handler ?? 'handler'; const defaultRunTime = nodeMajorVersion() >= 12 ? lambda.Runtime.NODEJS_12_X @@ -63,9 +63,9 @@ export class NodejsFunction extends lambda.Function { ...props, runtime, code: Bundling.parcel({ + ...props, entry, runtime, - ...props, }), handler: `index.${handler}`, }); @@ -84,7 +84,7 @@ export class NodejsFunction extends lambda.Function { */ function findEntry(id: string, entry?: string): string { if (entry) { - if (!/\.(js|ts)$/.test(entry)) { + if (!/\.(jsx?|tsx?)$/.test(entry)) { throw new Error('Only JavaScript or TypeScript entry files are supported.'); } if (!fs.existsSync(entry)) { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts index 6b3f7f8173a02..9bdab8776c86c 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts @@ -54,12 +54,16 @@ export function nodeMajorVersion(): number { * Find a file by walking up parent directories */ export function findUp(name: string, directory: string = process.cwd()): string | undefined { - const { root } = path.parse(directory); - if (directory === root && !fs.existsSync(path.join(directory, name))) { - return undefined; - } + const absoluteDirectory = path.resolve(directory); + if (fs.existsSync(path.join(directory, name))) { return directory; } - return findUp(name, path.dirname(directory)); + + const { root } = path.parse(absoluteDirectory); + if (absoluteDirectory === root) { + return undefined; + } + + return findUp(name, path.dirname(absoluteDirectory)); } diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts index 200200fdeb1ce..e8f559fdfbec5 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts @@ -1,6 +1,8 @@ import '@aws-cdk/assert/jest'; import { Runtime } from '@aws-cdk/aws-lambda'; import { Stack } from '@aws-cdk/core'; +import * as fs from 'fs'; +import * as path from 'path'; import { NodejsFunction } from '../lib'; import { Bundling } from '../lib/bundling'; @@ -67,6 +69,18 @@ test('throws when entry is not js/ts', () => { })).toThrow(/Only JavaScript or TypeScript entry files are supported/); }); +test('accepts tsx', () => { + const entry = path.join(__dirname, 'handler.tsx'); + + fs.symlinkSync(path.join(__dirname, 'function.test.handler1.ts'), entry); + + expect(() => new NodejsFunction(stack, 'Fn', { + entry, + })).not.toThrow(); + + fs.unlinkSync(entry); +}); + test('throws when entry does not exist', () => { expect(() => new NodejsFunction(stack, 'Fn', { entry: 'notfound.ts', @@ -82,3 +96,14 @@ test('throws with the wrong runtime family', () => { runtime: Runtime.PYTHON_3_8, })).toThrow(/Only `NODEJS` runtimes are supported/); }); + +test('resolves entry to an absolute path', () => { + // WHEN + new NodejsFunction(stack, 'fn', { + entry: 'lib/index.ts', + }); + + expect(Bundling.parcel).toHaveBeenCalledWith(expect.objectContaining({ + entry: expect.stringMatching(/@aws-cdk\/aws-lambda-nodejs\/lib\/index.ts$/), + })); +}); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/util.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/util.test.ts index 026f2bcb519e3..a85b0064cef85 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/util.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/util.test.ts @@ -10,4 +10,10 @@ test('findUp', () => { // Starting at a specific path expect(findUp('util.test.ts', path.join(__dirname, 'integ-handlers'))).toMatch(/aws-lambda-nodejs\/test$/); + + // Non existing file starting at a non existing relative path + expect(findUp('not-to-be-found.txt', 'non-existing/relative/path')).toBe(undefined); + + // Starting at a relative path + expect(findUp('util.test.ts', 'test/integ-handlers')).toMatch(/aws-lambda-nodejs\/test$/); }); diff --git a/packages/@aws-cdk/aws-s3-deployment/.gitignore b/packages/@aws-cdk/aws-s3-deployment/.gitignore index e99004d67b1a1..48b0b20af63bf 100644 --- a/packages/@aws-cdk/aws-s3-deployment/.gitignore +++ b/packages/@aws-cdk/aws-s3-deployment/.gitignore @@ -18,3 +18,5 @@ nyc.config.js *.snk !.eslintrc.js + +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/.npmignore b/packages/@aws-cdk/aws-s3-deployment/.npmignore index d1bc4d39e6428..5ad383d334409 100644 --- a/packages/@aws-cdk/aws-s3-deployment/.npmignore +++ b/packages/@aws-cdk/aws-s3-deployment/.npmignore @@ -22,5 +22,6 @@ lambda/*.sh tsconfig.json .eslintrc.js +jest.config.js # exclude cdk artifacts -**/cdk.out \ No newline at end of file +**/cdk.out diff --git a/packages/@aws-cdk/aws-s3-deployment/README.md b/packages/@aws-cdk/aws-s3-deployment/README.md index 80bd7a4e9e515..a90ea32f1ae20 100644 --- a/packages/@aws-cdk/aws-s3-deployment/README.md +++ b/packages/@aws-cdk/aws-s3-deployment/README.md @@ -41,7 +41,7 @@ This is what happens under the hood: is set to point to the assets bucket. 3. The custom resource downloads the .zip archive, extracts it and issues `aws s3 sync --delete` against the destination bucket (in this case - `websiteBucket`). If there is more than one source, the sources will be + `websiteBucket`). If there is more than one source, the sources will be downloaded and merged pre-deployment at this step. ## Supported sources @@ -59,10 +59,44 @@ all but a single file: ## Retain on Delete -By default, the contents of the destination bucket will be deleted when the +By default, the contents of the destination bucket will **not** be deleted when the `BucketDeployment` resource is removed from the stack or when the destination is -changed. You can use the option `retainOnDelete: true` to disable this behavior, -in which case the contents will be retained. +changed. You can use the option `retainOnDelete: false` to disable this behavior, +in which case the contents will be deleted. + +## Prune + +By default, files in the destination bucket that don't exist in the source will be deleted +when the `BucketDeployment` resource is created or updated. You can use the option `prune: false` to disable +this behavior, in which case the files will not be deleted. + +```typescript +new s3deploy.BucketDeployment(this, 'DeployMeWithoutDeletingFilesOnDestination', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket, + prune: false, +}); +``` + +This option also enables you to specify multiple bucket deployments for the same destination bucket & prefix, +each with its own characteristics. For example, you can set different cache-control headers +based on file extensions: + +```typescript +new BucketDeployment(this, 'BucketDeployment', { + sources: [Source.asset('./website', { exclude: ['index.html' })], + destinationBucket: bucket, + cacheControl: [CacheControl.fromString('max-age=31536000,public,immutable')], + prune: false, +}); + +new BucketDeployment(this, 'HTMLBucketDeployment', { + sources: [Source.asset('./website', { exclude: ['!index.html'] })], + destinationBucket: bucket, + cacheControl: [CacheControl.fromString('max-age=0,no-cache,no-store,must-revalidate')], + prune: false, +}); +``` ## Objects metadata diff --git a/packages/@aws-cdk/aws-s3-deployment/jest.config.js b/packages/@aws-cdk/aws-s3-deployment/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/src/index.py b/packages/@aws-cdk/aws-s3-deployment/lambda/src/index.py index 4daff5f3bce5d..474eb6d69226c 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/src/index.py +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/src/index.py @@ -47,6 +47,7 @@ def cfn_error(message=None): distribution_id = props.get('DistributionId', '') user_metadata = props.get('UserMetadata', {}) system_metadata = props.get('SystemMetadata', {}) + prune = props.get('Prune', 'true').lower() == 'true' default_distribution_path = dest_bucket_prefix if not default_distribution_path.endswith("/"): @@ -98,7 +99,7 @@ def cfn_error(message=None): aws_command("s3", "rm", old_s3_dest, "--recursive") if request_type == "Update" or request_type == "Create": - s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata) + s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune) if distribution_id: cloudfront_invalidate(distribution_id, distribution_paths) @@ -112,7 +113,7 @@ def cfn_error(message=None): #--------------------------------------------------------------------------------------------------- # populate all files from s3_source_zips to a destination bucket -def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata): +def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune): # create a temporary working directory workdir=tempfile.mkdtemp() logger.info("| workdir: %s" % workdir) @@ -131,7 +132,16 @@ def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata): zip.extractall(contents_dir) # sync from "contents" to destination - aws_command("s3", "sync", "--delete", contents_dir, s3_dest, *create_metadata_args(user_metadata, system_metadata)) + + s3_command = ["s3", "sync"] + + if prune: + s3_command.append("--delete") + + s3_command.extend([contents_dir, s3_dest]) + s3_command.extend(create_metadata_args(user_metadata, system_metadata)) + aws_command(*s3_command) + shutil.rmtree(workdir) #--------------------------------------------------------------------------------------------------- diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/test/aws b/packages/@aws-cdk/aws-s3-deployment/lambda/test/aws index 1796d021f650b..969bb982cd08c 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/test/aws +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/test/aws @@ -21,7 +21,7 @@ if sys.argv[2] == "cp" and not sys.argv[4].startswith("s3://"): sys.argv[4] = "archive.zip" if sys.argv[2] == "sync": - sys.argv[4] = "contents.zip" + sys.argv[4 if '--delete' in sys.argv else 3] = "contents.zip" with open("./aws.out", "a") as myfile: myfile.write(json.dumps(sys.argv[1:]) + "\n") diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/test/test.py b/packages/@aws-cdk/aws-s3-deployment/lambda/test/test.py index 30ac3ba26ada7..24552c98d45ea 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/test/test.py +++ b/packages/@aws-cdk/aws-s3-deployment/lambda/test/test.py @@ -16,7 +16,7 @@ class TestHandler(unittest.TestCase): def setUp(self): logger = logging.getLogger() - + # clean up old aws.out file (from previous runs) try: os.remove("aws.out") except OSError: pass @@ -37,6 +37,34 @@ def test_create_update(self): ["s3", "sync", "--delete", "contents.zip", "s3:///"] ) + def test_create_no_delete(self): + invoke_handler("Create", { + "SourceBucketNames": [""], + "SourceObjectKeys": [""], + "DestinationBucketName": "", + "Prune": "false" + }) + + self.assertAwsCommands( + ["s3", "cp", "s3:///", "archive.zip"], + ["s3", "sync", "contents.zip", "s3:///"] + ) + + def test_update_no_delete(self): + invoke_handler("Update", { + "SourceBucketNames": [""], + "SourceObjectKeys": [""], + "DestinationBucketName": "", + "Prune": "false" + }, old_resource_props={ + "DestinationBucketName": "", + }, physical_id="") + + self.assertAwsCommands( + ["s3", "cp", "s3:///", "archive.zip"], + ["s3", "sync", "contents.zip", "s3:///"] + ) + def test_create_update_multiple_sources(self): invoke_handler("Create", { "SourceBucketNames": ["", ""], diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts index b46e239a75a83..c564b2da9d6c4 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts @@ -30,6 +30,16 @@ export interface BucketDeploymentProps { */ readonly destinationKeyPrefix?: string; + /** + * If this is set to false, files in the destination bucket that + * do not exist in the asset, will NOT be deleted during deployment (create/update). + * + * @see https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html + * + * @default true + */ + readonly prune?: boolean + /** * If this is set to "false", the destination files will be deleted when the * resource is deleted or the destination is updated. @@ -197,12 +207,14 @@ export class BucketDeployment extends cdk.Construct { DestinationBucketName: props.destinationBucket.bucketName, DestinationBucketKeyPrefix: props.destinationKeyPrefix, RetainOnDelete: props.retainOnDelete, + Prune: props.prune ?? true, UserMetadata: props.metadata ? mapUserMetadata(props.metadata) : undefined, SystemMetadata: mapSystemMetadata(props), DistributionId: props.distribution ? props.distribution.distributionId : undefined, DistributionPaths: props.distributionPaths, }, }); + } private renderSingletonUuid(memoryLimit?: number) { diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index 9291eb8292125..cde79ab1cfc0f 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -51,7 +51,8 @@ ], "test": [ "/bin/bash lambda/test.sh" - ] + ], + "jest": true }, "keywords": [ "aws", @@ -77,10 +78,10 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.31", + "@types/jest": "^25.2.3", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^25.5.4", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts new file mode 100644 index 0000000000000..ce64692d9a9ef --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts @@ -0,0 +1,651 @@ +import { arrayWith } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import * as s3deploy from '../lib'; + +// tslint:disable:max-line-length +// tslint:disable:object-literal-key-quotes + +test('deploy from local directory asset', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: bucket, + }); + + // THEN + expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + 'ServiceToken': { + 'Fn::GetAtt': [ + 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', + 'Arn', + ], + }, + 'SourceBucketNames': [{ + 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A', + }], + 'SourceObjectKeys': [{ + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C', + }, + ], + }, + ], + }, + ], + ], + }], + 'DestinationBucketName': { + 'Ref': 'DestC383B82A', + }, + }); +}); + +test('deploy from local directory assets', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [ + s3deploy.Source.asset(path.join(__dirname, 'my-website')), + s3deploy.Source.asset(path.join(__dirname, 'my-website-second')), + ], + destinationBucket: bucket, + }); + + // THEN + expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + 'ServiceToken': { + 'Fn::GetAtt': [ + 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', + 'Arn', + ], + }, + 'SourceBucketNames': [ + { + 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A', + }, + { + 'Ref': 'AssetParametersa94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cbS3Bucket99793559', + }, + ], + 'SourceObjectKeys': [ + { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C', + }, + ], + }, + ], + }, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + 'Ref': 'AssetParametersa94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cbS3VersionKeyD9ACE665', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + 'Ref': 'AssetParametersa94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cbS3VersionKeyD9ACE665', + }, + ], + }, + ], + }, + ], + ], + }, + ], + 'DestinationBucketName': { + 'Ref': 'DestC383B82A', + }, + }); +}); + +test('fails if local asset is a non-zip file', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // THEN + expect(() => new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website', 'index.html'))], + destinationBucket: bucket, + })).toThrow(/Asset path must be either a \.zip file or a directory/); +}); + +test('deploy from a local .zip file', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], + destinationBucket: bucket, + }); + +}); + +test('honors passed asset options', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'), { + exclude: ['*', '!index.html'], + })], + destinationBucket: bucket, + }); + + // THEN + expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + 'ServiceToken': { + 'Fn::GetAtt': [ + 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', + 'Arn', + ], + }, + 'SourceBucketNames': [{ + 'Ref': 'AssetParameters86f8bca4f28a0bcafef0a98fe4cea25c0071aca27401e35cfaecd06313373bcaS3BucketB41AE64D', + }], + 'SourceObjectKeys': [{ + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + 'Ref': 'AssetParameters86f8bca4f28a0bcafef0a98fe4cea25c0071aca27401e35cfaecd06313373bcaS3VersionKeyF3CBA38F', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + 'Ref': 'AssetParameters86f8bca4f28a0bcafef0a98fe4cea25c0071aca27401e35cfaecd06313373bcaS3VersionKeyF3CBA38F', + }, + ], + }, + ], + }, + ], + ], + }], + 'DestinationBucketName': { + 'Ref': 'DestC383B82A', + }, + }); +}); + +test('retainOnDelete can be used to retain files when resource is deleted', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], + destinationBucket: bucket, + retainOnDelete: true, + }); + + // THEN + expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + RetainOnDelete: true, + }); +}); + +test('user metadata is correctly transformed', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], + destinationBucket: bucket, + metadata: { + A: '1', + B: '2', + }, + }); + + // THEN + expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + UserMetadata: { 'x-amzn-meta-a': '1', 'x-amzn-meta-b': '2' }, + }); +}); + +test('system metadata is correctly transformed', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], + destinationBucket: bucket, + contentType: 'text/html', + contentLanguage: 'en', + storageClass: s3deploy.StorageClass.INTELLIGENT_TIERING, + contentDisposition: 'inline', + serverSideEncryption: s3deploy.ServerSideEncryption.AWS_KMS, + serverSideEncryptionAwsKmsKeyId: 'mykey', + serverSideEncryptionCustomerAlgorithm: 'rot13', + websiteRedirectLocation: 'example', + cacheControl: [s3deploy.CacheControl.setPublic(), s3deploy.CacheControl.maxAge(cdk.Duration.hours(1))], + expires: s3deploy.Expires.after(cdk.Duration.hours(12)), + }); + + // THEN + expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + SystemMetadata: { + 'content-type': 'text/html', + 'content-language': 'en', + 'content-disposition': 'inline', + 'storage-class': 'INTELLIGENT_TIERING', + 'sse': 'aws:kms', + 'sse-kms-key-id': 'mykey', + 'cache-control': 'public, max-age=3600', + 'expires': s3deploy.Expires.after(cdk.Duration.hours(12)).value, + 'sse-c-copy-source': 'rot13', + 'website-redirect': 'example', + }, + }); +}); + +test('expires type has correct values', () => { + expect(s3deploy.Expires.atDate(new Date('Sun, 26 Jan 2020 00:53:20 GMT')).value).toEqual('Sun, 26 Jan 2020 00:53:20 GMT'); + expect(s3deploy.Expires.atTimestamp(1580000000000).value).toEqual('Sun, 26 Jan 2020 00:53:20 GMT'); + expect(Math.abs(new Date(s3deploy.Expires.after(cdk.Duration.minutes(10)).value).getTime() - (Date.now() + 600000)) < 15000).toBeTruthy(); + expect(s3deploy.Expires.fromString('Tue, 04 Feb 2020 08:45:33 GMT').value).toEqual('Tue, 04 Feb 2020 08:45:33 GMT'); + +}); + +test('cache control type has correct values', () => { + expect(s3deploy.CacheControl.mustRevalidate().value).toEqual('must-revalidate'); + expect(s3deploy.CacheControl.noCache().value).toEqual('no-cache'); + expect(s3deploy.CacheControl.noTransform().value).toEqual('no-transform'); + expect(s3deploy.CacheControl.setPublic().value).toEqual('public'); + expect(s3deploy.CacheControl.setPrivate().value).toEqual('private'); + expect(s3deploy.CacheControl.proxyRevalidate().value).toEqual('proxy-revalidate'); + expect(s3deploy.CacheControl.maxAge(cdk.Duration.minutes(1)).value).toEqual('max-age=60'); + expect(s3deploy.CacheControl.sMaxAge(cdk.Duration.minutes(1)).value).toEqual('s-maxage=60'); + expect(s3deploy.CacheControl.fromString('only-if-cached').value).toEqual('only-if-cached'); +}); + +test('storage class type has correct values', () => { + expect(s3deploy.StorageClass.STANDARD).toEqual('STANDARD'); + expect(s3deploy.StorageClass.REDUCED_REDUNDANCY).toEqual('REDUCED_REDUNDANCY'); + expect(s3deploy.StorageClass.STANDARD_IA).toEqual('STANDARD_IA'); + expect(s3deploy.StorageClass.ONEZONE_IA).toEqual('ONEZONE_IA'); + expect(s3deploy.StorageClass.INTELLIGENT_TIERING).toEqual('INTELLIGENT_TIERING'); + expect(s3deploy.StorageClass.GLACIER).toEqual('GLACIER'); + expect(s3deploy.StorageClass.DEEP_ARCHIVE).toEqual('DEEP_ARCHIVE'); +}); + +test('server side encryption type has correct values', () => { + expect(s3deploy.ServerSideEncryption.AES_256).toEqual('AES256'); + expect(s3deploy.ServerSideEncryption.AWS_KMS).toEqual('aws:kms'); +}); + +test('distribution can be used to provide a CloudFront distribution for invalidation', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + const distribution = new cloudfront.CloudFrontWebDistribution(stack, 'Distribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: bucket, + }, + behaviors: [{ isDefaultBehavior: true }], + }, + ], + }); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], + destinationBucket: bucket, + distribution, + distributionPaths: ['/images/*'], + }); + + expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + DistributionId: { + 'Ref': 'DistributionCFDistribution882A7313', + }, + DistributionPaths: ['/images/*'], + }); +}); + +test('invalidation can happen without distributionPaths provided', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + const distribution = new cloudfront.CloudFrontWebDistribution(stack, 'Distribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: bucket, + }, + behaviors: [{ isDefaultBehavior: true }], + }, + ], + }); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], + destinationBucket: bucket, + distribution, + }); + + expect(stack).toHaveResource('Custom::CDKBucketDeployment', { + DistributionId: { + 'Ref': 'DistributionCFDistribution882A7313', + }, + }); + +}); + +test('fails if distribution paths provided but not distribution ID', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // THEN + expect(() => new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website', 'index.html'))], + destinationBucket: bucket, + distributionPaths: ['/images/*'], + })).toThrow(/Distribution must be specified if distribution paths are specified/); + +}); + +test('lambda execution role gets permissions to read from the source bucket and read/write in destination', () => { + // GIVEN + const stack = new cdk.Stack(); + const source = new s3.Bucket(stack, 'Source'); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.bucket(source, 'file.zip')], + destinationBucket: bucket, + }); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + 'Effect': 'Allow', + 'Resource': [ + { + 'Fn::GetAtt': [ + 'Source71E471F1', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'Source71E471F1', + 'Arn', + ], + }, + '/*', + ], + ], + }, + ], + }, + { + 'Action': [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + 's3:PutObject*', + 's3:Abort*', + ], + 'Effect': 'Allow', + 'Resource': [ + { + 'Fn::GetAtt': [ + 'DestC383B82A', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'DestC383B82A', + 'Arn', + ], + }, + '/*', + ], + ], + }, + ], + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF', + 'Roles': [ + { + 'Ref': 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265', + }, + ], + }); +}); + +test('memoryLimit can be used to specify the memory limit for the deployment resource handler', () => { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + + // we define 3 deployments with 2 different memory configurations + + new s3deploy.BucketDeployment(stack, 'Deploy256-1', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: bucket, + memoryLimit: 256, + }); + + new s3deploy.BucketDeployment(stack, 'Deploy256-2', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: bucket, + memoryLimit: 256, + }); + + new s3deploy.BucketDeployment(stack, 'Deploy1024', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: bucket, + memoryLimit: 1024, + }); + + // THEN + + // we expect to find only two handlers, one for each configuration + + expect(stack).toCountResources('AWS::Lambda::Function', 2); + expect(stack).toHaveResource('AWS::Lambda::Function', { MemorySize: 256 }); + expect(stack).toHaveResource('AWS::Lambda::Function', { MemorySize: 1024 }); +}); + +test('deployment allows custom role to be supplied', () => { + + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + const existingRole = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazon.com'), + }); + + // WHEN + new s3deploy.BucketDeployment(stack, 'DeployWithRole', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: bucket, + role: existingRole, + }); + + // THEN + expect(stack).toCountResources('AWS::IAM::Role', 1); + expect(stack).toCountResources('AWS::Lambda::Function', 1); + expect(stack).toHaveResource('AWS::Lambda::Function', { + 'Role': { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }); +}); + +test('deploy without deleting missing files from destination', () => { + + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: bucket, + prune: false, + }); + + expect(stack).toHaveResourceLike('Custom::CDKBucketDeployment', { + 'Prune': false, + }); +}); + +test('Deployment role gets KMS permissions when using assets from new style synthesizer', () => { + const stack = new cdk.Stack(undefined, undefined, { + synthesizer: new cdk.DefaultStackSynthesizer(), + }); + const bucket = new s3.Bucket(stack, 'Dest'); + + // WHEN + new s3deploy.BucketDeployment(stack, 'Deploy', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket: bucket, + }); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: arrayWith({ + Action: ['kms:Decrypt', 'kms:DescribeKey'], + Effect: 'Allow', + Resource: { 'Fn::ImportValue': 'CdkBootstrap-hnb659fds-FileAssetKeyArn' }, + }), + }, + }); + +}); diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json index d0e61e14a23ae..e5e0dfe714f00 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json @@ -104,6 +104,7 @@ "Ref": "Destination3E3DC043D" }, "RetainOnDelete": false, + "Prune": true, "DistributionId": { "Ref": "DistributionCFDistribution882A7313" }, @@ -248,7 +249,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3Bucket88A20322" + "Ref": "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3BucketB6159468" }, "S3Key": { "Fn::Join": [ @@ -261,7 +262,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8" + "Ref": "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3VersionKey2060CAC0" } ] } @@ -274,7 +275,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8" + "Ref": "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3VersionKey2060CAC0" } ] } @@ -301,17 +302,17 @@ } }, "Parameters": { - "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3Bucket88A20322": { + "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3BucketB6159468": { "Type": "String", - "Description": "S3 bucket for asset \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" + "Description": "S3 bucket for asset \"4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50\"" }, - "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8": { + "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3VersionKey2060CAC0": { "Type": "String", - "Description": "S3 key for asset version \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" + "Description": "S3 key for asset version \"4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50\"" }, - "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0ArtifactHash877EFA91": { + "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50ArtifactHash846130E4": { "Type": "String", - "Description": "Artifact hash for asset \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" + "Description": "Artifact hash for asset \"4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50\"" }, "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json index e3308f63a431d..a3b8a6d1c81c9 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json @@ -10,38 +10,6 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "DestinationPolicy7982387E": { - "Type": "AWS::S3::BucketPolicy", - "Properties": { - "Bucket": { - "Ref": "Destination920A3C57" - }, - "PolicyDocument": { - "Statement": [ - { - "Action": "s3:GetObject", - "Effect": "Allow", - "Principal": "*", - "Resource": { - "Fn::Join": [ - "", - [ - { - "Fn::GetAtt": [ - "Destination920A3C57", - "Arn" - ] - }, - "/*" - ] - ] - } - } - ], - "Version": "2012-10-17" - } - } - }, "DeployMeCustomResource4455EE35": { "Type": "Custom::CDKBucketDeployment", "Properties": { @@ -94,7 +62,8 @@ "DestinationBucketName": { "Ref": "Destination920A3C57" }, - "RetainOnDelete": false + "RetainOnDelete": false, + "Prune": true }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -291,7 +260,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3Bucket88A20322" + "Ref": "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3BucketB6159468" }, "S3Key": { "Fn::Join": [ @@ -304,7 +273,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8" + "Ref": "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3VersionKey2060CAC0" } ] } @@ -317,7 +286,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8" + "Ref": "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3VersionKey2060CAC0" } ] } @@ -400,7 +369,8 @@ "Ref": "Destination281A09BDF" }, "DestinationBucketKeyPrefix": "deploy/here/", - "RetainOnDelete": false + "RetainOnDelete": false, + "Prune": true }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -463,6 +433,7 @@ "Ref": "Destination3E3DC043D" }, "RetainOnDelete": false, + "Prune": true, "UserMetadata": { "x-amzn-meta-a": "aaa", "x-amzn-meta-b": "bbb", @@ -475,20 +446,78 @@ }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" + }, + "DeployMeWithoutDeletingFilesOnDestinationCustomResourceA390B02B": { + "Type": "Custom::CDKBucketDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", + "Arn" + ] + }, + "SourceBucketNames": [ + { + "Ref": "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A" + } + ], + "SourceObjectKeys": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C" + } + ] + } + ] + } + ] + ] + } + ], + "DestinationBucketName": { + "Ref": "Destination920A3C57" + }, + "RetainOnDelete": false, + "Prune": false + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" } }, "Parameters": { - "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3Bucket88A20322": { + "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3BucketB6159468": { "Type": "String", - "Description": "S3 bucket for asset \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" + "Description": "S3 bucket for asset \"4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50\"" }, - "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0S3VersionKey5726B1E8": { + "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50S3VersionKey2060CAC0": { "Type": "String", - "Description": "S3 key for asset version \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" + "Description": "S3 key for asset version \"4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50\"" }, - "AssetParameters85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0ArtifactHash877EFA91": { + "AssetParameters4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50ArtifactHash846130E4": { "Type": "String", - "Description": "Artifact hash for asset \"85263806834b4abe18b7438876d0e408b131a41c86272285f069bb9fa96666f0\"" + "Description": "Artifact hash for asset \"4184245adc1f2ed71e1f0ae5719f8fd7f34324b750f1bf06b2fb5cf1f4014f50\"" }, "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.ts index 41460bb1af538..ee9918709b0b5 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.ts @@ -9,7 +9,7 @@ class TestBucketDeployment extends cdk.Stack { const destinationBucket = new s3.Bucket(this, 'Destination', { websiteIndexDocument: 'index.html', - publicReadAccess: true, + publicReadAccess: false, removalPolicy: cdk.RemovalPolicy.DESTROY, }); @@ -38,6 +38,14 @@ class TestBucketDeployment extends cdk.Stack { contentType: 'text/html', metadata: { A: 'aaa', B: 'bbb', C: 'ccc' }, }); + + new s3deploy.BucketDeployment(this, 'DeployMeWithoutDeletingFilesOnDestination', { + sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], + destinationBucket, + prune: false, + retainOnDelete: false, + }); + } } diff --git a/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts deleted file mode 100644 index fa4fad93d681e..0000000000000 --- a/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts +++ /dev/null @@ -1,661 +0,0 @@ -import { arrayWith, countResources, expect, haveResource } from '@aws-cdk/assert'; -import * as cloudfront from '@aws-cdk/aws-cloudfront'; -import * as iam from '@aws-cdk/aws-iam'; -import * as s3 from '@aws-cdk/aws-s3'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as path from 'path'; -import * as s3deploy from '../lib'; - -// tslint:disable:max-line-length -// tslint:disable:object-literal-key-quotes - -export = { - 'deploy from local directory asset'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], - destinationBucket: bucket, - }); - - // THEN - expect(stack).to(haveResource('Custom::CDKBucketDeployment', { - 'ServiceToken': { - 'Fn::GetAtt': [ - 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', - 'Arn', - ], - }, - 'SourceBucketNames': [{ - 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A', - }], - 'SourceObjectKeys': [{ - 'Fn::Join': [ - '', - [ - { - 'Fn::Select': [ - 0, - { - 'Fn::Split': [ - '||', - { - 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C', - }, - ], - }, - ], - }, - { - 'Fn::Select': [ - 1, - { - 'Fn::Split': [ - '||', - { - 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C', - }, - ], - }, - ], - }, - ], - ], - }], - 'DestinationBucketName': { - 'Ref': 'DestC383B82A', - }, - })); - test.done(); - }, - - 'deploy from local directory assets'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [ - s3deploy.Source.asset(path.join(__dirname, 'my-website')), - s3deploy.Source.asset(path.join(__dirname, 'my-website-second')), - ], - destinationBucket: bucket, - }); - - // THEN - expect(stack).to(haveResource('Custom::CDKBucketDeployment', { - 'ServiceToken': { - 'Fn::GetAtt': [ - 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', - 'Arn', - ], - }, - 'SourceBucketNames': [ - { - 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A', - }, - { - 'Ref': 'AssetParametersa94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cbS3Bucket99793559', - }, - ], - 'SourceObjectKeys': [ - { - 'Fn::Join': [ - '', - [ - { - 'Fn::Select': [ - 0, - { - 'Fn::Split': [ - '||', - { - 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C', - }, - ], - }, - ], - }, - { - 'Fn::Select': [ - 1, - { - 'Fn::Split': [ - '||', - { - 'Ref': 'AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3VersionKeyA58D380C', - }, - ], - }, - ], - }, - ], - ], - }, - { - 'Fn::Join': [ - '', - [ - { - 'Fn::Select': [ - 0, - { - 'Fn::Split': [ - '||', - { - 'Ref': 'AssetParametersa94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cbS3VersionKeyD9ACE665', - }, - ], - }, - ], - }, - { - 'Fn::Select': [ - 1, - { - 'Fn::Split': [ - '||', - { - 'Ref': 'AssetParametersa94977ede0211fd3b45efa33d6d8d1d7bbe0c5a96d977139d8b16abfa96fe9cbS3VersionKeyD9ACE665', - }, - ], - }, - ], - }, - ], - ], - }, - ], - 'DestinationBucketName': { - 'Ref': 'DestC383B82A', - }, - })); - test.done(); - }, - - 'fails if local asset is a non-zip file'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // THEN - test.throws(() => new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website', 'index.html'))], - destinationBucket: bucket, - }), /Asset path must be either a \.zip file or a directory/); - - test.done(); - }, - - 'deploy from a local .zip file'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], - destinationBucket: bucket, - }); - - test.done(); - }, - - 'honors passed asset options'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'), { - exclude: ['*', '!index.html'], - })], - destinationBucket: bucket, - }); - - // THEN - expect(stack).to(haveResource('Custom::CDKBucketDeployment', { - 'ServiceToken': { - 'Fn::GetAtt': [ - 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536', - 'Arn', - ], - }, - 'SourceBucketNames': [{ - 'Ref': 'AssetParameters86f8bca4f28a0bcafef0a98fe4cea25c0071aca27401e35cfaecd06313373bcaS3BucketB41AE64D', - }], - 'SourceObjectKeys': [{ - 'Fn::Join': [ - '', - [ - { - 'Fn::Select': [ - 0, - { - 'Fn::Split': [ - '||', - { - 'Ref': 'AssetParameters86f8bca4f28a0bcafef0a98fe4cea25c0071aca27401e35cfaecd06313373bcaS3VersionKeyF3CBA38F', - }, - ], - }, - ], - }, - { - 'Fn::Select': [ - 1, - { - 'Fn::Split': [ - '||', - { - 'Ref': 'AssetParameters86f8bca4f28a0bcafef0a98fe4cea25c0071aca27401e35cfaecd06313373bcaS3VersionKeyF3CBA38F', - }, - ], - }, - ], - }, - ], - ], - }], - 'DestinationBucketName': { - 'Ref': 'DestC383B82A', - }, - })); - test.done(); - }, - 'retainOnDelete can be used to retain files when resource is deleted'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], - destinationBucket: bucket, - retainOnDelete: true, - }); - - // THEN - expect(stack).to(haveResource('Custom::CDKBucketDeployment', { - RetainOnDelete: true, - })); - - test.done(); - }, - - 'user metadata is correctly transformed'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], - destinationBucket: bucket, - metadata: { - A: '1', - B: '2', - }, - }); - - // THEN - expect(stack).to(haveResource('Custom::CDKBucketDeployment', { - UserMetadata: { 'x-amzn-meta-a': '1', 'x-amzn-meta-b': '2' }, - })); - - test.done(); - }, - - 'system metadata is correctly transformed'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], - destinationBucket: bucket, - contentType: 'text/html', - contentLanguage: 'en', - storageClass: s3deploy.StorageClass.INTELLIGENT_TIERING, - contentDisposition: 'inline', - serverSideEncryption: s3deploy.ServerSideEncryption.AWS_KMS, - serverSideEncryptionAwsKmsKeyId: 'mykey', - serverSideEncryptionCustomerAlgorithm: 'rot13', - websiteRedirectLocation: 'example', - cacheControl: [s3deploy.CacheControl.setPublic(), s3deploy.CacheControl.maxAge(cdk.Duration.hours(1))], - expires: s3deploy.Expires.after(cdk.Duration.hours(12)), - }); - - // THEN - expect(stack).to(haveResource('Custom::CDKBucketDeployment', { - SystemMetadata: { - 'content-type': 'text/html', - 'content-language': 'en', - 'content-disposition': 'inline', - 'storage-class': 'INTELLIGENT_TIERING', - 'sse': 'aws:kms', - 'sse-kms-key-id': 'mykey', - 'cache-control': 'public, max-age=3600', - 'expires': s3deploy.Expires.after(cdk.Duration.hours(12)).value, - 'sse-c-copy-source': 'rot13', - 'website-redirect': 'example', - }, - })); - - test.done(); - }, - - 'expires type has correct values'(test: Test) { - test.equal(s3deploy.Expires.atDate(new Date('Sun, 26 Jan 2020 00:53:20 GMT')).value, 'Sun, 26 Jan 2020 00:53:20 GMT'); - test.equal(s3deploy.Expires.atTimestamp(1580000000000).value, 'Sun, 26 Jan 2020 00:53:20 GMT'); - test.ok(Math.abs(new Date(s3deploy.Expires.after(cdk.Duration.minutes(10)).value).getTime() - (Date.now() + 600000)) < 15000, 'Expires.after accurate to within 15 seconds'); - test.equal(s3deploy.Expires.fromString('Tue, 04 Feb 2020 08:45:33 GMT').value, 'Tue, 04 Feb 2020 08:45:33 GMT'); - - test.done(); - }, - - 'cache control type has correct values'(test: Test) { - test.equal(s3deploy.CacheControl.mustRevalidate().value, 'must-revalidate'); - test.equal(s3deploy.CacheControl.noCache().value, 'no-cache'); - test.equal(s3deploy.CacheControl.noTransform().value, 'no-transform'); - test.equal(s3deploy.CacheControl.setPublic().value, 'public'); - test.equal(s3deploy.CacheControl.setPrivate().value, 'private'); - test.equal(s3deploy.CacheControl.proxyRevalidate().value, 'proxy-revalidate'); - test.equal(s3deploy.CacheControl.maxAge(cdk.Duration.minutes(1)).value, 'max-age=60'); - test.equal(s3deploy.CacheControl.sMaxAge(cdk.Duration.minutes(1)).value, 's-maxage=60'); - test.equal(s3deploy.CacheControl.fromString('only-if-cached').value, 'only-if-cached'); - - test.done(); - }, - - 'storage class type has correct values'(test: Test) { - test.equal(s3deploy.StorageClass.STANDARD, 'STANDARD'); - test.equal(s3deploy.StorageClass.REDUCED_REDUNDANCY, 'REDUCED_REDUNDANCY'); - test.equal(s3deploy.StorageClass.STANDARD_IA, 'STANDARD_IA'); - test.equal(s3deploy.StorageClass.ONEZONE_IA, 'ONEZONE_IA'); - test.equal(s3deploy.StorageClass.INTELLIGENT_TIERING, 'INTELLIGENT_TIERING'); - test.equal(s3deploy.StorageClass.GLACIER, 'GLACIER'); - test.equal(s3deploy.StorageClass.DEEP_ARCHIVE, 'DEEP_ARCHIVE'); - - test.done(); - }, - - 'server side encryption type has correct values'(test: Test) { - test.equal(s3deploy.ServerSideEncryption.AES_256, 'AES256'); - test.equal(s3deploy.ServerSideEncryption.AWS_KMS, 'aws:kms'); - - test.done(); - }, - - 'distribution can be used to provide a CloudFront distribution for invalidation'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - const distribution = new cloudfront.CloudFrontWebDistribution(stack, 'Distribution', { - originConfigs: [ - { - s3OriginSource: { - s3BucketSource: bucket, - }, - behaviors: [{ isDefaultBehavior: true }], - }, - ], - }); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], - destinationBucket: bucket, - distribution, - distributionPaths: ['/images/*'], - }); - - expect(stack).to(haveResource('Custom::CDKBucketDeployment', { - DistributionId: { - 'Ref': 'DistributionCFDistribution882A7313', - }, - DistributionPaths: ['/images/*'], - })); - - test.done(); - }, - - 'invalidation can happen without distributionPaths provided'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - const distribution = new cloudfront.CloudFrontWebDistribution(stack, 'Distribution', { - originConfigs: [ - { - s3OriginSource: { - s3BucketSource: bucket, - }, - behaviors: [{ isDefaultBehavior: true }], - }, - ], - }); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website.zip'))], - destinationBucket: bucket, - distribution, - }); - - expect(stack).to(haveResource('Custom::CDKBucketDeployment', { - DistributionId: { - 'Ref': 'DistributionCFDistribution882A7313', - }, - })); - - test.done(); - }, - - 'fails if distribution paths provided but not distribution ID'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // THEN - test.throws(() => new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website', 'index.html'))], - destinationBucket: bucket, - distributionPaths: ['/images/*'], - }), /Distribution must be specified if distribution paths are specified/); - - test.done(); - }, - - 'lambda execution role gets permissions to read from the source bucket and read/write in destination'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const source = new s3.Bucket(stack, 'Source'); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.bucket(source, 'file.zip')], - destinationBucket: bucket, - }); - - // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { - 'PolicyDocument': { - 'Statement': [ - { - 'Action': [ - 's3:GetObject*', - 's3:GetBucket*', - 's3:List*', - ], - 'Effect': 'Allow', - 'Resource': [ - { - 'Fn::GetAtt': [ - 'Source71E471F1', - 'Arn', - ], - }, - { - 'Fn::Join': [ - '', - [ - { - 'Fn::GetAtt': [ - 'Source71E471F1', - 'Arn', - ], - }, - '/*', - ], - ], - }, - ], - }, - { - 'Action': [ - 's3:GetObject*', - 's3:GetBucket*', - 's3:List*', - 's3:DeleteObject*', - 's3:PutObject*', - 's3:Abort*', - ], - 'Effect': 'Allow', - 'Resource': [ - { - 'Fn::GetAtt': [ - 'DestC383B82A', - 'Arn', - ], - }, - { - 'Fn::Join': [ - '', - [ - { - 'Fn::GetAtt': [ - 'DestC383B82A', - 'Arn', - ], - }, - '/*', - ], - ], - }, - ], - }, - ], - 'Version': '2012-10-17', - }, - 'PolicyName': 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF', - 'Roles': [ - { - 'Ref': 'CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265', - }, - ], - })); - test.done(); - }, - - 'Deployment role gets KMS permissions when using assets from new style synthesizer'(test: Test) { - const stack = new cdk.Stack(undefined, undefined, { - synthesizer: new cdk.DefaultStackSynthesizer(), - }); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - new s3deploy.BucketDeployment(stack, 'Deploy', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], - destinationBucket: bucket, - }); - - // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: arrayWith({ - Action: ['kms:Decrypt', 'kms:DescribeKey'], - Effect: 'Allow', - Resource: { 'Fn::ImportValue': 'CdkBootstrap-hnb659fds-FileAssetKeyArn' }, - }), - }, - })); - - test.done(); - }, - - 'memoryLimit can be used to specify the memory limit for the deployment resource handler'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - - // WHEN - - // we define 3 deployments with 2 different memory configurations - - new s3deploy.BucketDeployment(stack, 'Deploy256-1', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], - destinationBucket: bucket, - memoryLimit: 256, - }); - - new s3deploy.BucketDeployment(stack, 'Deploy256-2', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], - destinationBucket: bucket, - memoryLimit: 256, - }); - - new s3deploy.BucketDeployment(stack, 'Deploy1024', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], - destinationBucket: bucket, - memoryLimit: 1024, - }); - - // THEN - - // we expect to find only two handlers, one for each configuration - - expect(stack).to(countResources('AWS::Lambda::Function', 2)); - expect(stack).to(haveResource('AWS::Lambda::Function', { MemorySize: 256 })); - expect(stack).to(haveResource('AWS::Lambda::Function', { MemorySize: 1024 })); - test.done(); - }, - - 'deployment allows custom role to be supplied'(test: Test) { - - // GIVEN - const stack = new cdk.Stack(); - const bucket = new s3.Bucket(stack, 'Dest'); - const existingRole = new iam.Role(stack, 'Role', { - assumedBy: new iam.ServicePrincipal('lambda.amazon.com'), - }); - - // WHEN - new s3deploy.BucketDeployment(stack, 'DeployWithRole', { - sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))], - destinationBucket: bucket, - role: existingRole, - }); - - // THEN - expect(stack).to(countResources('AWS::IAM::Role', 1)); - expect(stack).to(countResources('AWS::Lambda::Function', 1)); - expect(stack).to(haveResource('AWS::Lambda::Function', { - 'Role': { - 'Fn::GetAtt': [ - 'Role1ABCC5F0', - 'Arn', - ], - }, - })); - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index d5142e2159d2b..eb391390b2056 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -54,6 +54,8 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Create Transform Job](#create-transform-job) - [SNS](#sns) - [Step Functions](#step-functions) + - [Start Execution](#start-execution) + - [Invoke Activity Worker](#invoke-activity) - [SQS](#sqs) ## Task @@ -750,6 +752,8 @@ const task2 = new tasks.SnsPublish(this, 'Publish2', { ## Step Functions +### Start Execution + You can manage [AWS Step Functions](https://docs.aws.amazon.com/step-functions/latest/dg/connect-stepfunctions.html) executions. AWS Step Functions supports it's own [`StartExecution`](https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartExecution.html) API as a service integration. @@ -777,6 +781,33 @@ new sfn.StateMachine(stack, 'ParentStateMachine', { }); ``` +### Invoke Activity + +You can invoke a [Step Functions Activity](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-activities.html) which enables you to have +a task in your state machine where the work is performed by a *worker* that can +be hosted on Amazon EC2, Amazon ECS, AWS Lambda, basically anywhere. Activities +are a way to associate code running somewhere (known as an activity worker) with +a specific task in a state machine. + +When Step Functions reaches an activity task state, the workflow waits for an +activity worker to poll for a task. An activity worker polls Step Functions by +using GetActivityTask, and sending the ARN for the related activity. + +After the activity worker completes its work, it can provide a report of its +success or failure by using `SendTaskSuccess` or `SendTaskFailure`. These two +calls use the taskToken provided by GetActivityTask to associate the result +with that task. + +The following example creates an activity and creates a task that invokes the activity. + +```ts +const submitJobActivity = new sfn.Activity(this, 'SubmitJob'); + +new tasks.StepFunctionsInvokeActivity(this, 'Submit Job', { + activity: submitJobActivity, +}); +``` + ## SQS Step Functions supports [Amazon SQS](https://docs.aws.amazon.com/step-functions/latest/dg/connect-sqs.html) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 68d8c30b93997..ee034e389ad96 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -16,6 +16,7 @@ export * from './sagemaker/create-training-job'; export * from './sagemaker/create-transform-job'; export * from './start-execution'; export * from './stepfunctions/start-execution'; +export * from './stepfunctions/invoke-activity'; export * from './evaluate-expression'; export * from './emr/emr-create-cluster'; export * from './emr/emr-set-cluster-termination-protection'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-activity.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-activity.ts index e9d6342fefcc4..a0c78b5cf073c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-activity.ts @@ -19,6 +19,8 @@ export interface InvokeActivityProps { * A Step Functions Task to invoke an Activity worker. * * An Activity can be used directly as a Resource. + * + * @deprecated - use `StepFunctionsInvokeActivity` */ export class InvokeActivity implements sfn.IStepFunctionsTask { constructor(private readonly activity: sfn.IActivity, private readonly props: InvokeActivityProps = {}) { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/invoke-activity.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/invoke-activity.ts new file mode 100644 index 0000000000000..829a35eb03658 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/invoke-activity.ts @@ -0,0 +1,44 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; + +/** + * Properties for invoking an Activity worker + */ +export interface StepFunctionsInvokeActivityProps extends sfn.TaskStateBaseProps { + + /** + * Step Functions Activity to invoke + */ + readonly activity: sfn.IActivity +} + +/** + * A Step Functions Task to invoke an Activity worker. + * + * An Activity can be used directly as a Resource. + */ +export class StepFunctionsInvokeActivity extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + // No IAM permissions necessary, execution role implicitly has Activity permissions. + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: cdk.Construct, id: string, private readonly props: StepFunctionsInvokeActivityProps) { + super(scope, id, props); + + this.taskMetrics = { + metricDimensions: { ActivityArn: this.props.activity.activityArn }, + metricPrefixSingular: 'Activity', + metricPrefixPlural: 'Activities', + }; + } + + /** + * @internal + */ + protected _renderTask(): any { + return { + Resource: this.props.activity.activityArn, + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.invoke-activity.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.invoke-activity.expected.json new file mode 100644 index 0000000000000..e58efc93007de --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.invoke-activity.expected.json @@ -0,0 +1,85 @@ +{ + "Resources": { + "SubmitJobFB773A16": { + "Type": "AWS::StepFunctions::Activity", + "Properties": { + "Name": "awsstepfunctionsintegSubmitJobA2508960" + } + }, + "CheckJob5FFC1D6F": { + "Type": "AWS::StepFunctions::Activity", + "Properties": { + "Name": "awsstepfunctionsintegCheckJobC4AC762D" + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Submit Job\",\"States\":{\"Submit Job\":{\"Next\":\"Wait X Seconds\",\"Type\":\"Task\",\"ResultPath\":\"$.guid\",\"Resource\":\"", + { + "Ref": "SubmitJobFB773A16" + }, + "\"},\"Wait X Seconds\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.wait_time\",\"Next\":\"Get Job Status\"},\"Get Job Status\":{\"Next\":\"Job Complete?\",\"Type\":\"Task\",\"InputPath\":\"$.guid\",\"ResultPath\":\"$.status\",\"Resource\":\"", + { + "Ref": "CheckJob5FFC1D6F" + }, + "\"},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Get Final Job Status\"}],\"Default\":\"Wait X Seconds\"},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"DescribeJob returned FAILED\",\"Cause\":\"AWS Batch Job Failed\"},\"Get Final Job Status\":{\"End\":true,\"Type\":\"Task\",\"InputPath\":\"$.guid\",\"Resource\":\"", + { + "Ref": "CheckJob5FFC1D6F" + }, + "\"}},\"TimeoutSeconds\":300}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.invoke-activity.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.invoke-activity.ts new file mode 100644 index 0000000000000..918c31d809aaf --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.invoke-activity.ts @@ -0,0 +1,69 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +/* + * Creates a state machine with a job poller sample project + * https://docs.aws.amazon.com/step-functions/latest/dg/sample-project-job-poller.html + * + * Stack verification steps: + * The generated State Machine can be executed from the CLI (or Step Functions console) + * and runs with an execution status of `Running`. + * + * An external process can call the state machine to send a heartbeat or response before it times out. + * + * -- aws stepfunctions start-execution --state-machine-arn provides execution arn + * -- aws stepfunctions describe-execution --execution-arn returns a status of `Running` + * + * CHANGEME: extend this test to create the external resources to report heartbeats + */ +class InvokeActivityStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props: cdk.StackProps = {}) { + super(scope, id, props); + + const submitJobActivity = new sfn.Activity(this, 'SubmitJob'); + const checkJobActivity = new sfn.Activity(this, 'CheckJob'); + + const submitJob = new tasks.StepFunctionsInvokeActivity(this, 'Submit Job', { + activity: submitJobActivity, + resultPath: '$.guid', + }); + const waitX = new sfn.Wait(this, 'Wait X Seconds', { time: sfn.WaitTime.secondsPath('$.wait_time') }); + const getStatus = new tasks.StepFunctionsInvokeActivity(this, 'Get Job Status', { + activity: checkJobActivity, + inputPath: '$.guid', + resultPath: '$.status', + }); + const isComplete = new sfn.Choice(this, 'Job Complete?'); + const jobFailed = new sfn.Fail(this, 'Job Failed', { + cause: 'AWS Batch Job Failed', + error: 'DescribeJob returned FAILED', + }); + const finalStatus = new tasks.StepFunctionsInvokeActivity(this, 'Get Final Job Status', { + activity: checkJobActivity, + inputPath: '$.guid', + }); + + const chain = sfn.Chain + .start(submitJob) + .next(waitX) + .next(getStatus) + .next(isComplete + .when(sfn.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .when(sfn.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) + .otherwise(waitX)); + + const sm = new sfn.StateMachine(this, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(300), + }); + + new cdk.CfnOutput(this, 'stateMachineArn', { + value: sm.stateMachineArn, + }); + } +} + +const app = new cdk.App(); +new InvokeActivityStack(app, 'aws-stepfunctions-integ'); +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/invoke-activity.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/invoke-activity.ts new file mode 100644 index 0000000000000..940517e6bdbf7 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/invoke-activity.ts @@ -0,0 +1,64 @@ +import '@aws-cdk/assert/jest'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Stack } from '@aws-cdk/core'; +import { StepFunctionsInvokeActivity } from '../../lib/stepfunctions/invoke-activity'; + +test('Activity can be used in a Task', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const activity = new sfn.Activity(stack, 'Activity'); + const task = new StepFunctionsInvokeActivity(stack, 'Task', { activity }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { + DefinitionString: { + 'Fn::Join': ['', [ + '{"StartAt":"Task","States":{"Task":{"End":true,"Type":"Task","Resource":"', + { Ref: 'Activity04690B0A' }, + '"}}}', + ]], + }, + }); +}); + +test('Activity Task metrics and Activity metrics are the same', () => { + // GIVEN + const stack = new Stack(); + const activity = new sfn.Activity(stack, 'Activity'); + const task = new StepFunctionsInvokeActivity(stack, 'Invoke', {activity }); + + // WHEN + const activityMetrics = [ + activity.metricFailed(), + activity.metricHeartbeatTimedOut(), + activity.metricRunTime(), + activity.metricScheduled(), + activity.metricScheduleTime(), + activity.metricStarted(), + activity.metricSucceeded(), + activity.metricTime(), + activity.metricTimedOut(), + ]; + + const taskMetrics = [ + task.metricFailed(), + task.metricHeartbeatTimedOut(), + task.metricRunTime(), + task.metricScheduled(), + task.metricScheduleTime(), + task.metricStarted(), + task.metricSucceeded(), + task.metricTime(), + task.metricTimedOut(), + ]; + + // THEN + for (let i = 0; i < activityMetrics.length; i++) { + expect(activityMetrics[i]).toEqual(taskMetrics[i]); + } +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 1d275d3fb25f8..29a1d6845aeb9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -11,6 +11,8 @@ import { renderJsonPath, State } from './state'; /** * Props that are common to all tasks + * + * @deprecated - replaced by service integration specific classes (i.e. LambdaInvoke, SnsPublish) */ export interface TaskProps { /** @@ -98,6 +100,8 @@ export interface TaskProps { * * For some resource types, more specific subclasses of Task may be available * which are more convenient to use. + * + * @deprecated - replaced by service integration specific classes (i.e. LambdaInvoke, SnsPublish) */ export class Task extends State implements INextable { public readonly endStates: INextable[]; diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index e078153386c6e..6ec85c43af22e 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -78,14 +78,14 @@ export class BundlingDockerImage { const buildArgs = options.buildArgs || {}; const dockerArgs: string[] = [ - 'build', + 'build', '-q', ...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])), path, ]; const docker = dockerExec(dockerArgs); - const match = docker.stdout.toString().match(/Successfully built ([a-z0-9]+)/); + const match = docker.stdout.toString().match(/sha256:([a-z0-9]+)/); if (!match) { throw new Error('Failed to extract image ID from Docker build output'); diff --git a/packages/@aws-cdk/core/test/test.bundling.ts b/packages/@aws-cdk/core/test/test.bundling.ts index 558765010ccfe..500401101d22d 100644 --- a/packages/@aws-cdk/core/test/test.bundling.ts +++ b/packages/@aws-cdk/core/test/test.bundling.ts @@ -49,7 +49,7 @@ export = { const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ status: 0, stderr: Buffer.from('stderr'), - stdout: Buffer.from(`Successfully built ${imageId}`), + stdout: Buffer.from(`sha256:${imageId}`), pid: 123, output: ['stdout', 'stderr'], signal: null, @@ -63,7 +63,7 @@ export = { image._run(); test.ok(spawnSyncStub.firstCall.calledWith('docker', [ - 'build', + 'build', '-q', '--build-arg', 'TEST_ARG=cdk-test', 'docker-path', ])); diff --git a/tools/cdk-build-tools/bin/cdk-test.ts b/tools/cdk-build-tools/bin/cdk-test.ts index 4e89f9a562570..5a9ad332568ef 100644 --- a/tools/cdk-build-tools/bin/cdk-test.ts +++ b/tools/cdk-build-tools/bin/cdk-test.ts @@ -46,7 +46,7 @@ async function main() { if (useJest) { if (testFiles.length > 0) { - throw new Error(`Jest is enabled, but ${testFiles.length} nodeunit tests were found!`); + throw new Error(`Jest is enabled, but ${testFiles.length} nodeunit tests were found!: ${testFiles.map(f => f.filename)}`); } await shell([args.jest], defaultShellOptions); } else if (testFiles.length > 0) { diff --git a/yarn.lock b/yarn.lock index 982df84b248d3..4a0c668c6b587 100644 --- a/yarn.lock +++ b/yarn.lock @@ -529,10 +529,10 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jsii/spec@^1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.7.0.tgz#2a70ee5753aab1711a5e65161a1988845eb91043" - integrity sha512-gvj0vEvKWSo89ywclzb0OfFDSOqwTpvk0VQp2F3UEHewvR+hjJMgLjo7+ycpQF2bTLLni99KLmapMg/huxfshA== +"@jsii/spec@^1.7.0", "@jsii/spec@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.8.0.tgz#6860d3ca3461bfc6a568a7418b11d8fa52b98888" + integrity sha512-c9pLt+vYCwHP/lXoVK10nE572ekNo1e1U9osMOlXODW188Ca/ytP12VLCReypt3H8OMqebyK7ea7QdcszR+19w== dependencies: jsonschema "^1.2.6" @@ -1427,6 +1427,11 @@ 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" @@ -1441,7 +1446,16 @@ dependencies: "@types/node" "*" -"@types/glob@*", "@types/glob@^7.1.1", "@types/glob@^7.1.2": +"@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== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@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== @@ -1476,6 +1490,14 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest@^25.2.3": + version "25.2.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.2.3.tgz#33d27e4c4716caae4eced355097a47ad363fdcaf" + integrity sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + "@types/jest@^26.0.3": version "26.0.3" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.3.tgz#79534e0e94857171c0edc596db0ebe7cb7863251" @@ -1901,6 +1923,11 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +app-root-path@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" + integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== + append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -2019,7 +2046,7 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= -array-includes@^3.1.1: +array-includes@^3.0.3, array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== @@ -2045,7 +2072,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.3: +array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== @@ -2129,7 +2156,7 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: dependencies: array-filter "^1.0.0" -aws-sdk-mock@^5.1.0: +aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-mock/-/aws-sdk-mock-5.1.0.tgz#6f2c0bd670d7f378c906a8dd806f812124db71aa" integrity sha512-Wa5eCSo8HX0Snqb7FdBylaXMmfrAWoWZ+d7MFhiYsgHPvNvMEGjV945FF2qqE1U0Tolr1ALzik1fcwgaOhqUWQ== @@ -2138,7 +2165,37 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.709.0: +aws-sdk@^2.596.0: + version "2.685.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.685.0.tgz#ba5add21e98cc785b3c05ceb9f3fcb8ab046aa8a" + integrity sha512-mAOj7b4PuXRxIZkNdSkBWZ28lS2wYUY7O9u33nH9a7BawlttMNbxOgE/wDCPMrTLfj+RLQx0jvoIYj8BKCTRFw== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sdk@^2.637.0: + version "2.681.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.681.0.tgz#09eeedb5ca49813dfc637908abe408ae114a6824" + integrity sha512-/p8CDJ7LZvB1i4WrJrb32FUbbPdiZFZSN6FI2lv7s/scKypmuv/iJ9kpx6QWSWQZ72kJ3Njk/0o7GuVlw0jHXw== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sdk@^2.709.0: version "2.709.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.709.0.tgz#33b0c0fe8b9420c65610545394be047ac2d93c8f" integrity sha512-F3sKXsCiutj9RglVXdqb/XJ3Ko3G+pX081Nf1YjVJpLydwE2v16FGxrLqE5pqyWMDeUf5nZHnBoMuOYD8ip+Kw== @@ -2354,6 +2411,15 @@ buffer-from@1.x, buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@4.9.2: version "4.9.2" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" @@ -2693,13 +2759,13 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemaker@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.7.0.tgz#bcad7226740319dcdc99c23ba908a06c0e05ea35" - integrity sha512-TypUtZ56Au+BEvf0lSs37S/H5p2tpYfzF1FwE7hPNNasBffrAkqfryz0GFyOKPK7Svla5h5qTxRXFQWJ+g9Ciw== +codemaker@^1.7.0, codemaker@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.8.0.tgz#84e1fca0526cce078f04f2aa05ac4751a8ea4543" + integrity sha512-6ND1K9SIBgKVL2qXSZy0HdKKqvmpBQ71U5fwuHasmdFfzyylb8mx/hhVBkzBYpBH3EVecSqh3LUyZu1YZuGNwA== dependencies: camelcase "^6.0.0" - decamelize "^1.2.0" + decamelize "^4.0.0" fs-extra "^9.0.1" collect-v8-coverage@^1.0.0: @@ -3660,6 +3726,16 @@ dot-prop@^4.2.0: dependencies: is-obj "^1.0.0" +dotenv-json@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dotenv-json/-/dotenv-json-1.0.0.tgz#fc7f672aafea04bed33818733b9f94662332815c" + integrity sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ== + +dotenv@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + dotgitignore@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" @@ -3842,7 +3918,20 @@ escodegen@1.x.x, escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" -eslint-import-resolver-node@^0.3.3, eslint-import-resolver-node@^0.3.4: +eslint-config-standard@^14.1.0: + version "14.1.1" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" + integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== + +eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" + integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + +eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== @@ -3861,7 +3950,7 @@ eslint-import-resolver-typescript@^2.0.0: tiny-glob "^0.2.6" tsconfig-paths "^3.9.0" -eslint-module-utils@^2.6.0: +eslint-module-utils@^2.4.1, eslint-module-utils@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== @@ -3869,6 +3958,32 @@ eslint-module-utils@^2.6.0: debug "^2.6.9" pkg-dir "^2.0.0" +eslint-plugin-es@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" + integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== + dependencies: + eslint-utils "^1.4.2" + regexpp "^3.0.0" + +eslint-plugin-import@^2.19.1: + version "2.20.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" + integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== + dependencies: + array-includes "^3.0.3" + array.prototype.flat "^1.2.1" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.2" + eslint-module-utils "^2.4.1" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.0" + read-pkg-up "^2.0.0" + resolve "^1.12.0" + eslint-plugin-import@^2.22.0: version "2.22.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" @@ -3888,6 +4003,28 @@ eslint-plugin-import@^2.22.0: resolve "^1.17.0" tsconfig-paths "^3.9.0" +eslint-plugin-node@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" + integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== + dependencies: + eslint-plugin-es "^2.0.0" + eslint-utils "^1.4.2" + ignore "^5.1.1" + minimatch "^3.0.4" + resolve "^1.10.1" + semver "^6.1.0" + +eslint-plugin-promise@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" + integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== + +eslint-plugin-standard@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" + integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== + eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -3896,7 +4033,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.3: +eslint-utils@^1.4.2, eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== @@ -4156,7 +4293,12 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -4729,7 +4871,12 @@ globrex@^0.1.1: resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -4954,6 +5101,11 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.6.tgz#643194ad4bf2712f37852e386b6998eff0db2106" + integrity sha512-cgXgkypZBcCnOgSihyeqbo6gjIaIyDqPQB7Ra4vhE9m6kigdGoQDMHjviFhRZo3IMlRy6yElosoviMs5YxZXUA== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -5916,7 +6068,7 @@ jest-worker@^25.5.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^25.4.0, jest@^25.5.2, jest@^25.5.3, jest@^25.5.4: +jest@^25.4.0, jest@^25.5.0, jest@^25.5.2, jest@^25.5.3, jest@^25.5.4: version "25.5.4" resolved "https://registry.yarnpkg.com/jest/-/jest-25.5.4.tgz#f21107b6489cfe32b076ce2adcadee3587acb9db" integrity sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ== @@ -5941,9 +6093,9 @@ js-tokens@^4.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1, js-yaml@^3.2.7: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -5991,64 +6143,64 @@ jsesc@^2.5.1: integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== jsii-diff@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.7.0.tgz#f63a36ebcb0c948ef2480015a5cc6137de6d451d" - integrity sha512-EqZWSnHL6Qe7p3rfMIIV2h4a2bdx8Q1mB48uHa9ORZPEPiXwKjmrmY5TKk7XRcR9GSamEoKEcxILmC1ld4CfdQ== + version "1.8.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.8.0.tgz#d913a845b5b80d0a134f664e10a99069542c9ccf" + integrity sha512-yucKS75pidO3hRMK1ZzUZ1lBrzdI+GI6ij5BilrkT/fmmQXlIi+OQrHu4TUkMTjl6S+s8W/cxnNC+cUOqzoIAw== dependencies: - "@jsii/spec" "^1.7.0" + "@jsii/spec" "^1.8.0" fs-extra "^9.0.1" - jsii-reflect "^1.7.0" + jsii-reflect "^1.8.0" log4js "^6.3.0" - typescript "~3.9.5" + typescript "~3.9.6" yargs "^15.3.1" jsii-pacmak@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.7.0.tgz#4e9da8cf33916a149d7e3c90088df9f9c9fd228d" - integrity sha512-HFAR4/ySCqUAkDu++tEjIK5c5eiRoqUa43KahAy0nBqcyg3TrPqqKiK2m6nUbB+A6tQ0n4zO1EWm8yE0WlvY0w== + version "1.8.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.8.0.tgz#de463913ac694298aa81ad08ce2e4061b85cbd4b" + integrity sha512-vR6A5ulEjvWLmY4Fuvrw978Yv+STeTFKH8yU6OLO4q05BC9dRXo32l0h30GGJ+Uyi0YaDYtm5BnFk4p4vOVhWQ== dependencies: - "@jsii/spec" "^1.7.0" + "@jsii/spec" "^1.8.0" clone "^2.1.2" - codemaker "^1.7.0" + codemaker "^1.8.0" commonmark "^0.29.1" escape-string-regexp "^4.0.0" fs-extra "^9.0.1" - jsii-reflect "^1.7.0" - jsii-rosetta "^1.7.0" + jsii-reflect "^1.8.0" + jsii-rosetta "^1.8.0" semver "^7.3.2" spdx-license-list "^6.2.0" xmlbuilder "^15.1.1" yargs "^15.3.1" -jsii-reflect@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.7.0.tgz#4e01f803d80babb4a5125a72793e4dc48316bbd4" - integrity sha512-2r/GC+Ka0rghcCDGjlcg2RUjLTtYvT5z5GpyewCRP2Ss/5wwHyCo8xc/MjpDulzFzFozVa2xBtrckcux1seSKA== +jsii-reflect@^1.7.0, jsii-reflect@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.8.0.tgz#edea43fa83903b46c65c9f2d22b6246610460568" + integrity sha512-M+ai0nQNE0I/osOW6n1UywRFMJWMv5NI49XZUfUknBoO6c6tlDTiKLRmqrxzpJx9f7yoaV2tbf0kAMYNRedwGw== dependencies: - "@jsii/spec" "^1.7.0" + "@jsii/spec" "^1.8.0" colors "^1.4.0" fs-extra "^9.0.1" - oo-ascii-tree "^1.7.0" + oo-ascii-tree "^1.8.0" yargs "^15.3.1" -jsii-rosetta@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.7.0.tgz#eabcb159a2230a1e69cc1503c2afae18316513e8" - integrity sha512-DFoWSaVYtHJuxUmBohabsBtmdVPyRAaHkKRXOerRLEwdnS/WIix+FgbtxeVTeWMOHuw7C928DjW6Gwbo+lJu8w== +jsii-rosetta@^1.7.0, jsii-rosetta@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.8.0.tgz#0dd8e4fc7a862c6655c99a7c1f6392e20f256ed6" + integrity sha512-04DCDDO3jD4/xBDYsUlAh4x+BlYehtkyvhtvDUKZvGVjKvpU0iK7P/6258/8C19mpRYhBeFO6Ot1UW6NGxVYBA== dependencies: - "@jsii/spec" "^1.7.0" + "@jsii/spec" "^1.8.0" commonmark "^0.29.1" fs-extra "^9.0.1" - typescript "~3.9.5" + typescript "~3.9.6" xmldom "^0.3.0" yargs "^15.3.1" jsii@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.7.0.tgz#a9e8267fcb7f964f60ed400851a78d47543eee5e" - integrity sha512-isvI0v39OGzPYK3TYA0tg6H8FOKoq7GS+bYYWrhI3YWXNrVl9ZZNQW0hz6sUWN1cla3W9KqMT7HFcRf3ttt0Sg== + version "1.8.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.8.0.tgz#fc12a7a727879132b5eac1459c69f565c4ac4b1e" + integrity sha512-kvd0Afm50QnKOlGNeNnTHsKWXTeX+QTqHjRM2GkQYzKK7U/vVKUAU/eahbYUWyEHIC8VJd8rnUa0JLfOL/1jCA== dependencies: - "@jsii/spec" "^1.7.0" + "@jsii/spec" "^1.8.0" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.3" @@ -6058,7 +6210,7 @@ jsii@^1.7.0: semver-intersect "^1.4.0" sort-json "^2.0.0" spdx-license-list "^6.2.0" - typescript "~3.9.5" + typescript "~3.9.6" yargs "^15.3.1" json-diff@^0.5.4: @@ -6157,7 +6309,17 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jszip@*, jszip@^3.5.0: +jszip@*: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.4.0.tgz#1a69421fa5f0bb9bc222a46bca88182fba075350" + integrity sha512-gZAOYuPl4EhPTXT0GjhI3o+ZAz3su6EhLrKUoAivcKqyqC7laS5JEv4XWZND9BgcDcF83vI85yGbDmDR6UhrIg== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + +jszip@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== @@ -6201,6 +6363,24 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +lambda-leak@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lambda-leak/-/lambda-leak-2.0.0.tgz#771985d3628487f6e885afae2b54510dcfb2cd7e" + integrity sha1-dxmF02KEh/boha+uK1RRDc+yzX4= + +lambda-tester@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/lambda-tester/-/lambda-tester-3.6.0.tgz#ceb7d4f4f0da768487a05cff37dcd088508b5247" + integrity sha512-F2ZTGWCLyIR95o/jWK46V/WnOCFAEUG/m/V7/CLhPJ7PCM+pror1rZ6ujP3TkItSGxUfpJi0kqwidw+M/nEqWw== + dependencies: + app-root-path "^2.2.1" + dotenv "^8.0.0" + dotenv-json "^1.0.0" + lambda-leak "^2.0.0" + semver "^6.1.1" + uuid "^3.3.2" + vandium-utils "^1.1.1" + lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -6806,7 +6986,7 @@ mkdirp@*, mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -6937,6 +7117,17 @@ nise@^4.0.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" +nock@^11.7.0: + version "11.9.1" + resolved "https://registry.yarnpkg.com/nock/-/nock-11.9.1.tgz#2b026c5beb6d0dbcb41e7e4cefa671bc36db9c61" + integrity sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash "^4.17.13" + mkdirp "^0.5.0" + propagate "^2.0.0" + nock@^13.0.2: version "13.0.2" resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.2.tgz#3e50f88348edbb90cce1bbbf0a3ea6a068993983" @@ -7275,7 +7466,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.1: +object.values@^1.1.0, object.values@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== @@ -7311,10 +7502,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.7.0.tgz#6d804ffd0971105900379e6a1091c0fa58a545ae" - integrity sha512-Kfz5r6vEtUTZV1J8jIQVOIsfNujk/Rk2ngUgHKDwDOliycLytI9Bg55iCUxUoeiuy9NCefx7ZaLAbzM0CkjaOA== +oo-ascii-tree@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.8.0.tgz#47c040a99045bb162281c7603dea0c3fe22591fd" + integrity sha512-vo/cRakWcK/UeGGYQ7ByvgVWzCTchYkogldL3i2ZiHZt1etw/yh1YwimCfUM9rjf/pgBoy1Xe0pJuB+WQ3Ojcw== opener@^1.5.1: version "1.5.1" @@ -8307,13 +8498,20 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.10.1, resolve@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" +resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2: + version "1.16.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.16.1.tgz#49fac5d8bacf1fd53f200fa51247ae736175832c" + integrity sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig== + dependencies: + path-parse "^1.0.6" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -8458,6 +8656,11 @@ semver-intersect@^1.4.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@6.x, semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + semver@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667" @@ -8468,11 +8671,6 @@ semver@7.x, semver@^7.2.2, semver@^7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -9446,6 +9644,22 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= +ts-jest@^25.3.1: + version "25.5.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7" + integrity sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + micromatch "4.x" + mkdirp "0.x" + semver "6.x" + yargs-parser "18.x" + ts-jest@^26.1.1: version "26.1.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.1.tgz#b98569b8a4d4025d966b3d40c81986dd1c510f8d" @@ -9467,7 +9681,18 @@ ts-mock-imports@^1.2.6, ts-mock-imports@^1.3.0: resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.0.tgz#ed9b743349f3c27346afe5b7454ffd2bcaa2302d" integrity sha512-cCrVcRYsp84eDvPict0ZZD/D7ppQ0/JSx4ve6aEU8DjlsaWRJWV6ADMovp2sCuh6pZcduLFoIYhKTDU2LARo7Q== -ts-node@^8.0.2, ts-node@^8.10.2: +ts-node@^8.0.2: + version "8.8.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f" + integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "3.1.1" + +ts-node@^8.10.2: version "8.10.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== @@ -9613,10 +9838,10 @@ typescript@^3.3.3, typescript@^3.5.3, typescript@~3.8.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== -typescript@~3.9.5: - version "3.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" - integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== +typescript@~3.9.5, typescript@~3.9.6: + version "3.9.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" + integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== uglify-js@^3.1.4: version "3.9.1" @@ -9795,6 +10020,11 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +vandium-utils@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vandium-utils/-/vandium-utils-1.2.0.tgz#44735de4b7641a05de59ebe945f174e582db4f59" + integrity sha1-RHNd5LdkGgXeWevpRfF05YLbT1k= + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"